# -*- coding: UTF-8 -*-
import os
import random
import shutil
from os.path import expanduser
from pathlib import Path as BasePath
from pkgutil import ImpImporter
from pygments.lexers import Python2Lexer
from tempfile import gettempdir, NamedTemporaryFile as TempFile


__all__ = ["Path", "PyFolderPath", "PyModulePath", "TempPath"]


# NB: PythonLexer.analyse_text only relies on shebang !
lexer = Python2Lexer()


class Path(BasePath):
    """ Extension of the base class Path from pathlib. """
    _flavour = BasePath()._flavour  # fix to AttributeError
    
    def __new__(cls, *args, **kwargs):
        if kwargs.pop("expand", False):
            _ = expanduser(str(BasePath(*args, **kwargs)))
            p = BasePath(_, *args[1:], **kwargs).resolve()
            args = (str(p),) + args[1:]
        if kwargs.pop("create", False):
            BasePath(*args, **kwargs).mkdir(parents=True, exist_ok=True)
        return super(Path, cls).__new__(cls, *args, **kwargs)
    
    @property
    def child(self):
        """ Get the child path relative to self's one. """
        return Path(*self.parts[1:])
    
    @property
    def filename(self):
        """ Get the file name, without the complete path. """
        return self.stem + self.suffix
    
    @property
    def size(self):
        """ Get path's size. """
        if self.is_file() or self.is_symlink():
            return self.stat().st_size
        elif self.is_dir():
            s = 4096  # include the size of the directory itself
            for root, dirs, files in os.walk(str(self)):
                s += 4096 * len(dirs)
                for f in files:
                    s += os.stat(str(Path(root).joinpath(f))).st_size
            return s
        raise AttributeError("object 'Path' has no attribute 'size'")
    
    def append_bytes(self, text):
        """ Allows to append bytes to the file, as only write_bytes is available
             in pathlib, overwritting the former bytes at each write. """
        with open(str(self), 'ab') as f:
            f.write(text)
    
    def append_line(self, line):
        """ Shortcut for appending a single line (text with newline). """
        self.append_text(line + '\n')
    
    def append_lines(self, *lines):
        """ Shortcut for appending a bunch of lines. """
        for line in lines:
            self.append_line(line)
    
    def append_text(self, text):
        """ Allows to append text to the file, as only write_text is available
             in pathlib, overwritting the former text at each write. """
        with open(str(self), 'a') as f:
            f.write(text)
    
    def choice(self, *filetypes):
        """ Return a random file from the current directory. """
        filetypes = list(filetypes)
        while len(filetypes) > 0:
            filetype = random.choice(filetypes)
            filetypes.remove(filetype)
            l = list(self.iterfiles(filetype, filename_only=True))
            try:
                return self.joinpath(random.choice(l))
            except:
                continue
    
    def expanduser(self):
        """ Fixed expanduser() method, working for both Python 2 and 3. """
        return Path(expanduser(str(self)))
    
    def generate(self, prefix="", suffix="", length=8,
                 alphabet="0123456789abcdef"):
        """ Generate a random folder name. """
        rname = "".join(random.choice(alphabet) for i in range(length))
        return self.joinpath(prefix + rname + suffix)
    
    def iterpubdir(self):
        """ List all public subdirectories from the current directory. """
        for i in self.iterdir():
            if i.is_dir() and not i.stem.startswith("."):
                yield i
    
    def iterfiles(self, filetype=None, filename_only=False, relative=False):
        """ List all files from the current directory. """
        for i in self.iterdir():
            if i.is_file():
                if filetype is None or i.suffix == filetype:
                    yield i.filename if filename_only else \
                          i.relative_to(self) if relative else i
    
    def read_text(self):
        """ Fix to non-existing method in Python 2. """
        try:
            super(Path, self).read_text()
        except AttributeError:  # occurs with Python 2 ; no write_text method
            with open(str(self), 'r') as f:
                c = f.read()
            return c
    
    def reset(self):
        """ Ensure the file exists and is empty. """
        if self.exists():
            self.unlink()
        self.touch()
    
    def rmtree(self):
        """ Extension for recursively removing a directory. """
        shutil.rmtree(str(self))
    
    def samepath(self, otherpath):
        """ Check if both paths have the same parts. """
        return self.parts == otherpath.parts
    
    def write_text(self, text):
        """ Fix to non-existing method in Python 2. """
        try:
            super(Path, self).write_text(text)
        except AttributeError:  # occurs with Python 2 ; no write_text method
            with open(str(self), 'w+') as f:
                f.write(text)


class PyFolderPath(Path):
    """ Path extension for handling the dynamic import of every Python module
         inside the given folder. """
    def __init__(self, path):
        super(PyFolderPath, self).__init__()
        self.modules = []
        if self.is_dir():
            for root, dirs, files in os.walk(path):
                for f in files:
                    if f.endswith(".py"):
                        p = PyModulePath(Path(root).joinpath(f))
                        if p.is_pymodule:
                            self.modules.append(p.module)


class PyModulePath(Path):
    """ Path extension for handling the dynamic import of a Python module. """
    def __init__(self, path):
        super(PyModulePath, self).__init__()
        self.is_pymodule = self.is_file() and \
                           lexer.analyse_text(self.open().read()) == 1.0
        if self.is_pymodule:
            self.module = ImpImporter(str(self.resolve().parent)) \
                          .find_module(self.stem).load_module(self.stem)
    
    def get_classes(self, *base_cls):
        """ Yield a list of all subclasses inheriting from the given class from
             the Python module. """
        if not self.is_pymodule:
            return
        for n in dir(self.module):
            cls = getattr(self.module, n)
            try:
                if issubclass(cls, base_cls) and cls not in base_cls:
                    yield cls
            except TypeError:
                pass
    
    def has_class(self, base_cls):
        """ Check if the Python module has the given class. """
        if not self.is_pymodule:
            return
        for n in dir(self.module):
            try:
                cls = getattr(self.module, n)
                if issubclass(cls, base_cls) and cls is not base_cls:
                    return True
            except TypeError:
                pass
        return False


class TempPath(Path):
    """ Extension of the class Path for handling a temporary path.
    
    :param length:   length for the folder name (if 0, do not generate a folder
                      name, e.g. keeping /tmp)
    :param alphabet: character set to be used for generating the folder name
    """
    def __new__(cls, **kwargs):
        kw = {}
        kw["prefix"]   = kwargs.pop("prefix", "")
        kw["suffix"]   = kwargs.pop("suffix", "")
        kw["length"]   = kwargs.pop("length", 0)
        kw["alphabet"] = kwargs.pop("alphabet", "0123456789abcdef")
        _ = Path(gettempdir())
        kwargs["create"] = True   # force creation
        kwargs["expand"] = False  # expansion is not necessary
        if kw["length"] > 0:
            while True:
                # ensure this is a newly generated path
                tmp = _.generate(**kw)
                if not tmp.exists():
                    break
            return super(TempPath, cls).__new__(cls, tmp, **kwargs)
        return super(TempPath, cls).__new__(cls, _, **kwargs)
    
    def joinpath(self, *args):
        """ Modifed joinpath to return a Path instance instead of TempPath. """
        return Path(self).joinpath(*args)
    
    def tempfile(self, **kwargs):
        """ Create a NamedTemporaryFile in the TempPath. """
        kwargs.pop("dir", None)
        tf = TempFile(dir=str(self), **kwargs)
        tf.folder = self
        return tf