"""

    AUTHOR:

        Khushal P Soonderji

    DATE:

        Friday, 26th Apr., 2024

    OBJECTIVE:

        To provide an easy way to work with files and directories.

    REFERENCES:

        N/A

    DOWNLOADS:

        N/A

"""

#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                   IMPORT                                                      ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")

# Other system-level dependencies:
import os
import sys
import inspect
import pathlib
import platform

# For working with datatypes:
from typing import List, Literal


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                           MACROS / ONE-TIME INIT                                              ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                VARIABLES                                                      ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                  FUNCTIONS                                                    ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


def get_cwd():

    """
    Returns the full path of the file from which the current execution flow started. No matter where this function is
    called from, the path returned will be of the file where the execution flow started.
    :return: The full file path of the script where the execution flow began.
    """

    return os.getcwd()


# ---------------------------------------------------------------------------------------------------------------------


def get_file_directory(include_filename: bool = False):

    """
    Returns the absolute path of the file that called this function, regardless of where this file is placed, and
    regardless of which other file called this function.
    :param include_filename: Whether you want the file's name in the response or not.
    :return: The full path of the file from which this function was called.
    """

    # Get the current stack frame (the one just before this function)
    caller_frame = inspect.stack()[1]  # [1] gets the caller's frame
    file_name = caller_frame.filename  # Get the filename (relative path)

    # Get the absolute path of the file
    full_path = os.path.abspath(file_name)
    if not include_filename: full_path = os.path.split(full_path)[0]

    # Done here:
    return full_path


# ---------------------------------------------------------------------------------------------------------------------


def get_parent_directory(path: str, depth: int = 1) -> str:

    """
    Takes a path and gives you its parent path 'n' no. of directories up.
    :param path: The path whose parent is needed.
    :param depth: How many dirs you want to come up.
    :return: The parent path 'n' levels up.
    """

    for _ in range(depth): path = os.path.split(path)[0]
    return path or "/"


# ---------------------------------------------------------------------------------------------------------------------


def make_directory(path: str) -> bool:

    """
    Creates a directory if you have sufficient permissions.
    :param path: The path you want to create.
    :return: True if successful, else False.
    """

    if not os.path.exists(path):
        try: os.makedirs(path)
        except Exception as excp:
            print("FILE UTILS EXCEPTION (make dir):", excp)
            return False

    if os.path.exists(path):return True
    else: return False


# ---------------------------------------------------------------------------------------------------------------------


def get_directory_for_file_path(file_path: str) -> str:

    """
    Given a file's path, this function gives you that file's parent directory's path.
    :param file_path: The file's full path whose parent is needed.
    :return: The file's parent directory's path if the file exists, else None.
    """

    if os.path.exists(file_path):
        path_components = os.path.split(file_path)
        file_name = path_components[-1]
        if file_name.find(".") > -1: file_path = os.path.join("", *path_components[:-1])
        return file_path


# ---------------------------------------------------------------------------------------------------------------------


def write_file(
        file_path: str,
        file_data: str | bytes,
        mode: Literal["w", "wb", "a", "ab"] = "w",
        raise_exception: bool = False
) -> bool:

    """
    Writes/overwrites a file with the data provided.
    :param file_path: The file to which you want to add data.
    :param file_data: The data you want to add to the end of the file.
    :param mode: The mode with which data will be written.
    :param raise_exception: If set to True, exceptions will be propagated; if set to False, exceptions will be
        suppressed.
    :return: True if successful, else False.
    """

    try:

        file = open(file_path, mode)
        file.write(file_data)
        file.close()
        return True

    except Exception as excp:
        print("FILE UTILS EXCEPTION (write file):", excp)
        return False


# ---------------------------------------------------------------------------------------------------------------------


def append_file(
        file_path: str,
        file_data: str,
        mode: Literal["a", "ab"] = "a",
        raise_exception: bool = False
) -> bool:

    """
    Adds data to the end of a file.
    :param file_path: The file to which you want to add data.
    :param file_data: The data you want to add to the end of the file.
    :param mode: The mode with which data will be written.
    :param raise_exception: If set to True, exceptions will be propagated; if set to False, exceptions will be
        suppressed.
    :return: True if successful, else False.
    """

    try: return write_file(file_path, file_data, mode = mode)
    except Exception as excp:
        if raise_exception: raise
        return False


# ---------------------------------------------------------------------------------------------------------------------


def read_file(
        file_path: str,
        mode: Literal["r", "rb"] = "r",
        encoding = "utf8",
        raise_exception: bool = False
) -> str | bytes | None:

    """
    Reads the contents of one file.
    :param file_path: The path of the file whose contents must be read.
    :param mode: The mode of reading the file.
    :param encoding: The encoding of the file.
    :param raise_exception: If set to True, exceptions will be propagated; if set to False, exceptions will be
        suppressed.
    :return: The contents of the file or None.
    """

    try:

        file = open(file_path, mode, encoding = encoding)
        file_contents = file.read()
        file.close()
        return file_contents

    except Exception as excp:
        if raise_exception: raise
        return None


# ---------------------------------------------------------------------------------------------------------------------


def rename_file(
        current: str,
        new: str,
        raise_exception: bool = False
) -> bool:

    """
    Renames one file. USE CAREFULLY.
    WARNING: rename_directory("old_file", "/path/to/new_location/new_file") will move 'old_file' to
    '/path/to/new_location/' and rename it to 'new_file'.
    :param current: The current name/path.
    :param new: The new name/path.
    :param raise_exception: If set to True, exceptions will be propagated; if set to False, exceptions will be
        suppressed.
    :return: True if renamed, else False.
    """

    try:
        if os.path.isfile(current):
            os.rename(current, new)
            return True
        return False

    except Exception as excp:
        if raise_exception: raise
        return False


# ---------------------------------------------------------------------------------------------------------------------


def rename_directory(
        current: str,
        new: str,
        raise_exception: bool = False
) -> bool:

    """
    Renames the directory. USE CAREFULLY.
    WARNING: rename_directory("old_directory", "/path/to/new_location/new_directory") will move 'old_directory' to
    '/path/to/new_location/' and rename it to 'new_directory'.
    :param current: The current name/path.
    :param new: The new name/path.
    :param raise_exception: If set to True, exceptions will be propagated; if set to False, exceptions will be
        suppressed.
    :return: True if renamed, else False.
    """

    try:
        if os.path.isdir(current):
            os.rename(current, new)
            return True
        return False

    except Exception as excp:
        if raise_exception: raise
        return False


# ---------------------------------------------------------------------------------------------------------------------


def delete_file(
        file_path: str,
        raise_exception: bool = False
) -> bool:

    """
    Deletes one file. USE CAREFULLY.
    :param file_path: The path of the file that you want to delete.
    :param raise_exception: If set to True, exceptions will be propagated; if set to False, exceptions will be
        suppressed.
    :return: True if deleted, else False.
    """

    try:
        if os.path.isfile(file_path):
            os.remove(file_path)
            return True
        return False

    except Exception as excp:
        if raise_exception: raise
        return False


# ---------------------------------------------------------------------------------------------------------------------


def delete_directory(
        directory_path: str,
        raise_exception: bool = False
) -> bool:

    """
    Deletes a whole directory with all its contents. USE CAREFULLY.
    :param directory_path: The path that you want to delete.
    :param raise_exception: If set to True, exceptions will be propagated; if set to False, exceptions will be
        suppressed.
    :return: True if deleted, else False.
    """

    try:
        if os.path.exists(directory_path):
            for root, directories, files in os.walk(directory_path, topdown = False):
                for file in files:
                    os.remove(os.path.join(root, file))
                for directory in directories:
                    os.rmdir(os.path.join(root, directory))
            os.rmdir(directory_path)
            return True
        return False

    except Exception as excp:
        if raise_exception: raise
        return False


# ---------------------------------------------------------------------------------------------------------------------


def move_file(
        file_path: str,
        new_dir: str,
        raise_exception: bool = False
) -> bool:

    """
    To move a file from one place to another.
    :param file_path: The path of the current file.
    :param new_dir: The destination directory.
    :param raise_exception: If set to True, exceptions will be propagated; if set to False, exceptions will be
        suppressed.
    :return: True if moved, else False.
    """

    try:
        destination = os.path.join(new_dir, os.path.split(file_path)[-1])
        os.replace(file_path, destination)
        return True

    except Exception as excp:
        if raise_exception: raise
        return False


# ---------------------------------------------------------------------------------------------------------------------


def copy_file(
        file_path: str,
        new_dir: str,
        raise_exception: bool = False
) -> bool:

    """
    To copy a file from one place to another.
    :param file_path: The path of the current file.
    :param new_dir: The destination directory.
    :param raise_exception: If set to True, exceptions will be propagated; if set to False, exceptions will be
        suppressed.
    :return: True if copied, else False.
    """

    try:

        destination = os.path.join(new_dir, os.path.split(file_path)[-1])
        handle_src = open(file_path, mode = "r")
        handle_dst = open(destination, mode = "w")
        stat_src = os.stat(file_path)
        n_bytes = stat_src.st_size
        fd_src = handle_src.fileno()
        fd_dst = handle_dst.fileno()
        os.sendfile(fd_dst, fd_src, 0, n_bytes)
        return True

    except Exception as excp:
        if raise_exception: raise
        return False


# ---------------------------------------------------------------------------------------------------------------------


def list_files(
        directory_path: str,
        full_path = False,
        raise_exception: bool = False
) -> List[str]:

    """
    Lists all the files in a path.
    :param directory_path: The path inside which you want to list all the files.
    :param full_path: Whether, or not, you would like to receive the list of full paths, or just the names of the files.
    :param raise_exception: If set to True, exceptions will be propagated; if set to False, exceptions will be
        suppressed.
    :return: The list of files inside the given path.
    """

    try:
        path = pathlib.Path(directory_path)
        files = [entry.name for entry in path.iterdir() if entry.is_file()]
        if full_path: files = [os.path.join(directory_path, file) for file in files]
        return files

    except Exception as excp:
        if raise_exception: raise
        return []


# ---------------------------------------------------------------------------------------------------------------------


def list_dirs(
        directory_path: str,
        full_path = False,
        raise_exception: bool = False
) -> List[str]:

    """
    Lists all the subdirectories in a path.
    :param directory_path: The path inside which you want to list all the directories.
    :param full_path: Whether, or not, you would like to receive the list of full paths, or just the names of the
        directories.
    :param raise_exception: If set to True, exceptions will be propagated; if set to False, exceptions will be
        suppressed.
    :return: The list of subdirectories inside the given path.
    """

    try:

        path = pathlib.Path(directory_path)
        files = [entry.name for entry in path.iterdir() if entry.is_dir()]
        if full_path: files = [os.path.join(directory_path, file) for file in files]
        return files

    except Exception as excp:
        if raise_exception: raise
        return []


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                               MAIN PROGRAM                                                    ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


if __name__ == "__main__":

    pass
