"""
library for file opertations
"""
import ast
import glob
import json
import logging
import os
import re
import shutil
from pathlib import Path, PurePath, PurePosixPath

import chardet
import errno

import lxml
import requests

from amilib.util import TextUtil, Util

# wildcards
STARS = "**"
STAR = "*"

# suffixes
S_PDF = "pdf"
S_PNG = "png"
S_SVG = "pdf"
S_TXT = "txt"
S_XML = "xml"

# markers for processing
# _NULL = "_NULL"
# _REQD = "_REQD"

# known section names
# SVG = "svg"
# PDFIMAGES = "pdfimages"
# RESULTS = "results"
# SECTIONS = "sections"

# subsects
# IMAGE_STAR = "image*"

# subsects
# OCTREE = "*octree"

# results
# SEARCH = "search"
# WORD = "word"
# EMPTY = "empty"

# files
# FULLTEXT_PAGE = "fulltext-page*"
# CHANNEL_STAR = "channel*"
# RAW = "raw"



# def get_logger_old(cls, filename, file_level=2, suffix=".py", level=logging.INFO):
#     """creates logger for module, uses modulae name
#     removes .py
#     retains level of hierarchy
#     e.g. foo/bar/junk.py with levels = 2 => bar.junk
#     :param filename: to act as logger name
#     :param file_level: to include in hierarchy
#     :param level: logging level (default INFO)
#     :param suffix: suffix to remove, e.g. ".py"
#     """
#     # logger.debug(f"============= file {__file__} name {__name__} =============")
#     if filename:
#         if filename[-len(suffix):] == suffix:
#             filename = filename[:-len(suffix)]
#         module = '.'.join(filename.split(os.path.sep)[-file_level:])
#         logger = logging.getLogger(module)
#
#         # Create handlers for logging to the standard output and a file
#         stdoutHandler = logging.StreamHandler(stream=sys.stdout)
#         errHandler = logging.FileHandler("error.log")
#
#         # Set the log levels on the handlers
#         stdoutHandler.setLevel(logging.DEBUG)
#         errHandler.setLevel(logging.ERROR)
#
#         # Create a log format using Log Record attributes
#         fmt = logging.Formatter(
#             "%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s"
#         )
#
#         # Set the log format on each handler
#         stdoutHandler.setFormatter(fmt)
#         errHandler.setFormatter(fmt)
#
#         # Add each handler to the Logger object
#         logger.addHandler(stdoutHandler)
#         logger.addHandler(errHandler)
#
#         logger.info("Server started listening on port 8080")
#         logger.warning(
#             "Disk space on drive '/var/log' is running low. Consider freeing up space"
#         )
#
#         try:
#             raise Exception("Failed to connect to database: 'my_db'")
#         except Exception as e:
#             # exc_info=True ensures that a Traceback is included
#             logger.error(e, exc_info=True)
#
#         logger.info(f"created logger {module} {logger}")
#         return logger
#
logger = Util.get_logger( __name__)


class FileLib:

    @classmethod
    def force_mkdir(cls, dirx):
        """ensure dirx and its parents exist
        :dirx: directory
        """
        path = Path(dirx)
        if not path.exists():
            try:
                path.mkdir(parents=True, exist_ok=True)
                assert (f := path).exists(), f"dir {path} should now exist"
            except Exception as e:
                logger.error(f"cannot make dirx {dirx} , {e}")
                logger.debug(f"cannot make dirx {dirx}, {e}")



    @classmethod
    def force_mkparent(cls, file):
        """ensure parent directory exists

        :path: whose parent directory is to be created if absent
        """
        if file is not None:
            cls.force_mkdir(cls.get_parent_dir(file))

    @classmethod
    def force_write(cls, file, data, overwrite=True):
        """:write path, creating directory if necessary
        :path: path to write to
        :data: str data to write
        :overwrite: force write if path exists

        may throw exception from write
        """
        if file is not None:
            if os.path.exists(file) and not overwrite:
                logging.warning(f"not overwriting existsnt path {file}")
            else:
                cls.force_mkparent(file)
                with open(file, "w", encoding="utf-8") as f:
                    f.write(data)

    @classmethod
    def copy_file_or_directory(cls, dest_path, src_path, overwrite):
        if dest_path.exists():
            if not overwrite:
                file_type = "dirx" if dest_path.is_dir() else "path"
                raise TypeError(
                    str(dest_path), f"cannot overwrite existing {file_type} (str({dest_path})")

        else:
            # assume directory
            logger.warning(f"create directory {dest_path}")
            dest_path.mkdir(parents=True, exist_ok=True)
            logger.info(f"created directory {dest_path}")
        if src_path.is_dir():
            if os.path.exists(dest_path):
                shutil.rmtree(dest_path)
            shutil.copytree(src_path, dest_path)
            logger.info(f"copied directory {src_path} to {dest_path}")
        else:
            try:
                shutil.copy(src_path, dest_path)  # will overwrite
                logger.info(f"copied path {src_path} to {dest_path}")
            except Exception as e:
                logger.fatal(f"Cannot copy direcctory {src_path} to {dest_path} because {e}")

    @staticmethod
    def create_absolute_name(file):
        """create absolute/relative name for a path relative to amilib

        TODO this is messy
        """
        absolute_file = None
        if file is not None:
            file_dir = FileLib.get_parent_dir(__file__)
            absolute_file = os.path.join(os.path.join(file_dir, file))
        return absolute_file

    @classmethod
    def get_parent_dir(cls, file):
        return None if file is None else PurePath(file).parent

    @classmethod
    def read_pydictionary(cls, file):
        """read a JSON path into a python dictionary
        :param file: JSON file to read
        :return: JSON dictionary (created by ast.literal_eval)
        """
        with open(file, "r") as f:
            pydict = ast.literal_eval(f.read())
        return pydict

    @classmethod
    def punct2underscore(cls, text):
        """ replace all ASCII punctuation except '.' , '-', '_' by '_'

        usually used for filenames
        :param text: input string
        :return: substituted string

        """
        # this is non-trivial https://stackoverflow.com/questions/10017147/removing-a-list-of-characters-in-string

        non_file_punct = '\t \n{}!@#$%^&*()[]:;\'",|\\~+=/`'
        # [unicode(x.strip()) if x is not None else '' for x in row]

        text0 = TextUtil.replace_chars(text, non_file_punct, "_")
        return text0

    @classmethod
    def get_suffix(cls, file):
        """get suffix of filename
        :param file: filename
        :return: suffix including the '.'

        """
        _suffix = None if file is None else Path(file).suffix
        return _suffix

    @staticmethod
    def check_exists(file):
        """
        raise exception on null value or non-existent path
        """
        if file is None:
            raise Exception("null path")

        if os.path.isdir(file):
            # logger.debug(path, "is directory")
            pass
        elif os.path.isfile(file):
            # logger.debug(path, "is path")
            pass
        else:
            try:
                f = open(file, "r")
                logger.debug("tried to open", file)
                f.close()
            except Exception:
                raise FileNotFoundError(str(file) + " should exist")

    @classmethod
    def copyanything(cls, src, dst):
        """copy file or directory
        (from StackOverflow)
        :param src: source file/directory
        :param dst: destination
        """
        try:
            shutil.copytree(src, dst)
        except OSError as exc:  # python >2.5
            if exc.errno in (errno.ENOTDIR, errno.EINVAL):
                shutil.copy(src, dst)
            else:
                raise exc

    @classmethod
    def copy_file(cls, file, src, dst):
        """
        :param file: filename in src dir
        :param src: source directory
        :oaram dst: destinatiom diecrtory
        """
        FileLib.copyanything(Path(src, file), Path(dst, file))

    @classmethod
    def delete_directory_contents(cls, dirx, delete_directory=False):
        """
        deletes directories recursively using shutil.rmtree
        :param dirx: directory tree to delete
        :param delete_directory: If True, delete dirx
        :return None:
        """
        if not dirx or not Path(dirx).exists():
            print (f"no directory given or found {dirx}")
            return
        if delete_directory:
            shutil.rmtree(dirx)
        else:
            for path in Path(dirx).glob("**/*"):
                if path.is_file():
                    path.unlink()
                elif path.is_dir():
                    shutil.rmtree(path)

    @classmethod
    def delete_files(cls, dirx, globstr):
        """
        delete files in directory
        :param dirx: directory containing files
        :param globstr: glob string, e.g. "*.html"
        :return: list of deleted files (Paths)

        """
        files = []
        for path in Path(dirx).glob(globstr):
            if path.is_file():
                path.unlink()
                files.append(path)
        return files

    @classmethod
    def list_files(cls, dirx, globstr):
        """
        list files (not dirs) in directory
        :param dirx: directory containing files
        :param globstr: glob string, e.g. "*.html"
        :return: list of files (Paths)
        """
        return [path for path in Path(dirx).glob(globstr) if path.is_file()]

    @classmethod
    def size(cls, file):
        """
        get size of file
        :param file:
        :return: file size bytes else None if not exist
        """
        return None if file is None or not file.exists() else os.path.getsize(file)

    @classmethod
    def get_encoding(cls, file):
        """tries to guess (text) encoding
        :param file: to read
        :return: {'encoding': Guess, 'confidence': d.d, 'language': Lang}"""
        with open(file, "rb") as f:
            rawdata = f.read()
            return cls.get_encoding_from_bytes(rawdata)

    @classmethod
    def get_encoding_from_bytes(cls, rawdata):
        chardet.detect(rawdata)
        encode = chardet.UniversalDetector()
        encode.close()
        return encode.result

    @classmethod
    def expand_glob_list(cls, file_list):
        """
        :param file_list: list of paths including globs
        :return: flattened globbed list wwith posix names
        """
        if type(file_list) is not list:
            file_list = [file_list]
        files = []
        for file in file_list:
            globbed_files = FileLib.posix_glob(str(file))
            files.extend(globbed_files)
        return cls.convert_files_to_posix(files)

    @classmethod
    def convert_files_to_posix(cls, file_list):
        """converts list of files to posix form (i.e. all files have / not backslash)
        """
        if file_list is None:
            return None
        posix_files = [PurePosixPath(f) for f in file_list]
        return posix_files


    @classmethod
    def delete_file(cls, file):
        """delete file (uses unlink) and asserts it has worked
        ;param file: to delete"""
        file = Path(file)
        if file.exists():
            file.unlink()
        assert not file.exists()

    @classmethod
    def write_dict(cls, dikt, path, debug=False, indent=2):
        """write dictionary as JSON object
        :param dikt: python dictionary
        :param path: path to write to
        :param debug:
        :param indent:
        """

        with open(str(path), "w") as f:
            json.dump(dikt, f, indent=indent)
        if debug:
            logger.debug(f"wrote dictionary to {path}")

    @classmethod
    def read_string_with_user_agent(self, url, user_agent='my-app/0.0.1', encoding="UTF-8", encoding_scheme="chardet", debug=False):
        """
        allows request.get() to use a user_agent
        :param url: url to read
        :param encoding_scheme: id "chardet uses chardet else response.appenent_encoding
        :return: decoded string
        """
        if not url:
            return None
        if debug:
            logger.debug(f"reading {url}")
        response = requests.get(url, headers={'user-agent': user_agent})
        if debug:
            logger.debug(f"response: {response} content: {response.content[:400]}")
        content = response.content
        if debug:
            logger.debug(f"apparent encoding: {response.apparent_encoding}")
        if encoding is None:
            encoding = chardet.detect(content)['encoding'] if encoding_scheme == "chardet" else response.apparent_encoding
        content = content.decode(encoding)
        return content, encoding

    @classmethod
    def join_dir_and_file_as_posix(cls, indir, input):
        """
        joins indir (directory) and input (descendants) to make a list of full filenames
        if indir or input is null, no action
        if indir is a list no action, returns input unchanged
        if input is absolute (starts with "/") no action

        if input is string, creates PosixPath(indir, input) VIA PosixPath
        if input is list of strings creates:
            f"{indir}/{input1}"
            f"{indir}/{input2}"
            ...
            it skips any input strings starting with "/"
        :param indir: input directory
        :param input: single file or list
        :return: single filke or files AS posix strings
        """
        if not indir or not input:
            return input
        # cannot manage multiple directories (?yet)
        if type(indir) is list and len(indir) > 1:
            return input

        if type(input) is str:
            # single input
            if input[0] != "/":
                output = PurePosixPath(indir, input)
                return str(output)
        elif type(input) is list:
            # list of inputs
            outputs = []
            for input_item in input:
                if input_item[0] != "/":
                    posix = PurePosixPath(indir, input_item)
                    outputs.append(str(posix))
            return outputs

    @classmethod
    def get_children(cls, parent, dirx=None, hidden=None):
        """
        convenience method
        gets some or all child files
        :param file: parent dir
        :param dirx:  True=include only directories, False=ignore directories, None=no action
        :param hidden:  True=include only hidden files, False=ignore hidden, None=no action (NYI)
        :return:list of files (may be empty)
        """
        files = []
        if parent is None or not Path(parent).is_dir():
            return files
        parent = Path(parent)
        files = list(parent.iterdir())
        if dirx is not None:
            files = [f for f in files if dirx==f.is_dir()]
        if hidden is not None:
            logger.warning(f"hidden option {hidden} is NYI")
        return files

    @classmethod
    def posix_glob(cls, glob_str, recursive = True):
        """expands glob and ensure all output is posix
        :param glob_str: glob or list of globs to expand
        :param recursive: use recursive glob
        :return: list of files in posix format"""
        files = []
        if glob_str is None:
            return files
        if type(glob_str) is str:
            glob_str = [glob_str]
        for globx in glob_str:
            ff = glob.glob(globx, recursive=recursive)
            files.extend(ff)
        files = FileLib.convert_files_to_posix(files)
        return files

    @classmethod
    def assert_exist_size(cls, file, minsize, abort=True, debug=True):
        """asserts a file exists and is of sufficient size
        :param file: file or path
        :param minsize: minimum size
        :param abort: throw exception if fails (not sure what this does)
        :param debug: output filename
        """
        path = Path(file)
        if debug:
            logger.debug(f"checking {file}")
        try:
            assert path.exists(), f"file {path} must exist"
            assert (s := path.stat().st_size) > minsize, f"file {file} size = {s} must be above {minsize}"
        except AssertionError as e:
            if abort:
                raise e


    @classmethod
    def get_home(cls):
        """
        gets home directory os.path.expanduser("~")
        """
        home = os.path.expanduser("~")
        return home

    # @classmethod
    # def get_logger_old(cls, filename, file_level=2, suffix=".py", level=logging.INFO):
    #     """creates logger for module, uses modulae name
    #     removes .py
    #     retains level of hierarchy
    #     e.g. foo/bar/junk.py with levels = 2 => bar.junk
    #     :param filename: to act as logger name
    #     :param file_level: to include in hierarchy
    #     :param level: logging level (default INFO)
    #     :param suffix: suffix to remove, e.g. ".py"
    #     """
    #     # logger.debug(f"============= file {__file__} name {__name__} =============")
    #     if filename:
    #         if filename[-len(suffix):] == suffix:
    #             filename = filename[:-len(suffix)]
    #         module = '.'.join(filename.split(os.path.sep)[-file_level:])
    #         logger = logging.getLogger(module)
    #
    #         # Create handlers for logging to the standard output and a file
    #         stdoutHandler = logging.StreamHandler(stream=sys.stdout)
    #         errHandler = logging.FileHandler("error.log")
    #
    #         # Set the log levels on the handlers
    #         stdoutHandler.setLevel(logging.DEBUG)
    #         errHandler.setLevel(logging.ERROR)
    #
    #         # Create a log format using Log Record attributes
    #         fmt = logging.Formatter(
    #             "%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s"
    #         )
    #
    #         # Set the log format on each handler
    #         stdoutHandler.setFormatter(fmt)
    #         errHandler.setFormatter(fmt)
    #
    #         # Add each handler to the Logger object
    #         logger.addHandler(stdoutHandler)
    #         logger.addHandler(errHandler)
    #
    #         logger.info("Server started listening on port 8080")
    #         logger.warning(
    #             "Disk space on drive '/var/log' is running low. Consider freeing up space"
    #         )
    #
    #         try:
    #             raise Exception("Failed to connect to database: 'my_db'")
    #         except Exception as e:
    #             # exc_info=True ensures that a Traceback is included
    #             logger.error(e, exc_info=True)
    #
    #         logger.info(f"created logger {module} {logger}")
    #         return logger

    @classmethod
    def get_input_strings(cls, strings_in, split=False):
        """
        reads strings from a varietyb of inputs, e.g
        "foo"
        "foo bar" (with/out split)
        ["foo", "bar"]
        ["foo bar", "plugh xyzzy"] (with/out split)
        "/Users/my/file.txt"
        "https://not.yet/implemented"
        :param string_in: single word, list of words, or file/s with words (url not yet supported)
        :param split: splist all strings by spaces (default False)
        :return: list of strings or empty list
        """
        strings_out = []
        if strings_in is None or strings_in == []:
            return strings_out
        # ensure a list
        logger.debug(f"strings_in {type(strings_in)} .. {strings_in}")
        if type(strings_in) is str and strings_in[:1] == "[":
            strings_in = ast.literal_eval(strings_in)
        logger.debug(f"after literal eval strings_in {type(strings_in)} .. {strings_in}")
        strings_in = strings_in if type(strings_in) is list else [strings_in]
        for string_in in strings_in:
            # is it a file
            strings_read = cls.read_strings_from_path(string_in)
            if strings_read is None:
                # not a file
                strings_read = [string_in]
            if split:
                sss = []
                for string_read in strings_read:
                    sss.extend(string_read.strip().split())
                strings_read = sss
            strings_out.extend(strings_read)


        # else:
        #     strings_out = strings_in
        return strings_out

    @classmethod
    def read_strings_from_path(cls, file_with_strings, strip=True, ignore_blank=True):
        """
        reads a file with list of strings. Lines can be stripped and blank lines ignored
        :param file_with_strings: file with list of strimgs , one per line
        :return: list of strings (may be empty) or None (file not exists)
        """
        if file_with_strings is None:
            return None
        path_in = Path(file_with_strings)
        if not path_in.exists():
            return None
        lines = []
        with open(path_in, "r") as f:
            for line in f:
                if strip:
                    line = line.rstrip()
                if ignore_blank and line == "":
                    continue
                lines.append(line)
        return lines

    @classmethod
    def log_exception(self, e, logger):
        """
        formats exception message with hyperlinks and line_no
        :param e: exceptiom
        :param logger: standard logger
        """
        logger.error(e, exc_info=True)



    @classmethod
    def write_temp_html(cls, htmlx, temp_dir, temp_file="junk.html", pretty_print=True, debug=True):
        """
        writes a chunk of html to a temporary file

        """
        pptext = lxml.etree.tostring(htmlx, pretty_print=pretty_print)
        path = Path(temp_dir, temp_file)
        if path.exists():
            FileLib.delete_file(path)
        print(f"file exists {path.exists()}")
        # path.parent.mkdir(exist_ok=False, parents=False)
        print(f"file exists {path.exists()}")
        with open(str(path), "w") as f:
            f.write(pptext.decode())
        if debug:
            print(f"wrote {path}")



# see https://realpython.com/python-pathlib/

def main():
    logger.debug("started file_lib")
    # test_templates()

    logger.debug("finished file_lib")


if __name__ == "__main__":
    logger.debug("running file_lib main")
    main()
else:
    #    logger.debug("running file_lib main anyway")
    #    main()
    pass

# examples of regex for filenames


def glob_re(pattern, strings):
    return filter(re.compile(pattern).match, strings)


filenames = glob_re(r'.*(abc|123|a1b).*\.txt', os.listdir())
