"""A module providing a functions defining how pancad opens, reads,
writes, overwrites, and appends to files by default.
"""

import os

ACCESS_MODE_OPTIONS = ["r", "w", "x", "+"]
OPERATION_TYPES = ["r", "w"]

def filepath(path: str) -> str:
    """Returns the real filepath of the file. Does not check whether the file
    exists, just whether the path is valid.

    :param filepath: a string of the name and location of the file
    :returns: The real path of the file
    """
    if path is None:
        raise InvalidFilepathError("Filepath cannot be None")

    path = os.path.expandvars(path)
    directory = os.path.dirname(path)

    if os.path.isfile(path):
        # Filepath is valid and file already exists
        return os.path.realpath(path)
    if os.path.isdir(path):
        raise IsADirectoryError(f"'Filepath: '{path}'")
    if os.path.isdir(directory) and os.path.basename(path) != "":
        # Filepath is valid but file does not exist
        return os.path.realpath(path)
    raise InvalidFilepathError(f"Filepath given: '{path}'")

def folderpath(path: str) -> str:
    """Returns the real path of the folder, only if it exists.

    :param path: Location of the folder
    :returns: The real path of the folder
    """
    if path is None:
        raise InvalidFilepathError("Folder path cannot be None")

    path = os.path.expandvars(path)

    if os.path.isdir(path):
        return os.path.realpath(path)
    if os.path.isfile(path):
        raise NotADirectoryError(f"Path: {path}")
    raise InvalidFilepathError(f"Path invalid or does not exist: {path}")

def exists(path: str) -> bool:
    """Returns whether a file exists at the given filepath. If the filepath
    is a directory or is a incorrectly formatted string it will raise errors.

    :param path: a string of the name and location of the file
    :returns: A boolean for whether the file exists
    """
    path = filepath(path)
    directory = os.path.dirname(path)
    if os.path.isfile(path):
        return True
    if os.path.isdir(directory) and os.path.basename(path) != "":
        return False
    raise InvalidFilepathError(f"Filepath: '{path}'")

def validate_mode(path:str, mode:str) -> None:
    """Checks whether the file mode is valid on the given filepath.

    :param path: a string of the name and location of the file
    :param mode: the single character string describing the file's
                 mode. read (r), write (w), exclusive creation (x), and
                 read-write (+)
    """
    path = filepath(path)
    file_exists = exists(path)
    if mode not in ACCESS_MODE_OPTIONS:
        raise InvalidAccessModeError(f"Mode: '{mode}'")
    if mode == "x" and file_exists:
        raise ExclusiveCreationFileExistsError(f"Filepath: '{path}'")
    if mode in "r" and not file_exists:
        raise FileNotFoundError(f"Filepath: '{path}'")

def validate_operation(path: str, mode: str, operation_type: str) -> None:
    """Checks whether the file mode is being violated by a specific
    file operation and will raise an errors when settings are
    violated to prevent data loss.

    :param path: a string of the name and location of the file
    :param mode: the single character string describing the file's
                 mode. read (r), write (w), exclusive creation (x), and
                 read-write (+)
    :param operation_type: a single character string describing the
                           operation type, read (r), write (w)
    """
    path = filepath(path)
    file_exists = exists(path)
    validate_mode(path, mode)
    if operation_type not in OPERATION_TYPES:
        raise InvalidOperationModeError(f"Operation: '{operation_type}',")
    if mode in ["w", "x"] and operation_type == "r":
        raise WriteOnlyError("Cannot read while in write-only"
                             + " or exclusive creation mode")
    if mode == "r" and operation_type == "w":
        raise ReadOnlyError("Cannot write while in read-only mode")
    if mode == "+" and operation_type == "r" and not file_exists:
        raise FileNotFoundError(f"Filepath: '{path}'")

class InvalidAccessModeError(ValueError):
    """Raise when a file access mode is not in ACCESS_MODE_OPTIONS"""

class InvalidOperationModeError(ValueError):
    """Raise when a file modification operation mode is not in
    OPERATION_TYPES"""

class ExclusiveCreationFileExistsError(FileExistsError):
    """Raise when trying to exclusively create an already existing file"""

class InvalidFilepathError(ValueError):
    """Raise when the filepath provided does not match a file or folder"""

class WriteOnlyError(ValueError):
    """Raise when trying to read a file when the mode is write-only"""

class ReadOnlyError(ValueError):
    """Raise when trying to write to a file when the mode is read-only"""
