import os


class SingleFile(object):
    """
    When prospector is run in 'single file mode' - that is,
    the argument is a python module rather than a directory -
    then we'll use this object instead of the FoundFiles to
    give all the functionality needed to check a single file.
    """
    def __init__(self, filepath):
        self.filepath = filepath
        self.rootpath = os.getcwd()

    def _check(self, checkpath, abspath=True):
        if abspath:
            checkpath = os.path.abspath(checkpath)
            return checkpath == os.path.abspath(self.filepath)
        return checkpath == self.filepath

    def check_module(self, filepath, abspath=True, even_if_ignored=False):
        return self._check(filepath, abspath)

    def check_package(self, filepath, abspath=True, even_if_ignored=False):
        return self._check(filepath, abspath)

    def check_file(self, filepath, abspath=True, even_if_ignored=False):
        return self._check(filepath, abspath)

    def iter_file_paths(self, abspath=True, include_ignored=False):
        yield os.path.abspath(self.filepath) if abspath else self.filepath

    def iter_package_paths(self, abspath=True, include_ignored=False):
        yield os.path.abspath(self.filepath) if abspath else self.filepath

    def iter_directory_paths(self, abspath=True, include_ignored=False):
        yield os.path.abspath(self.filepath) if abspath else self.filepath

    def iter_module_paths(self, abspath=True, include_ignored=False):
        yield os.path.abspath(self.filepath) if abspath else self.filepath

    def to_absolute_path(self, path):
        return os.path.abspath(os.path.join(self.rootpath, path))

    def get_minimal_syspath(self, absolute_paths=True):
        path = os.path.dirname(self.filepath)
        if absolute_paths:
            path = os.path.abspath(path)
        return [path]


class FoundFiles(object):
    def __init__(self, rootpath, files, modules, packages, directories, ignores):
        self.rootpath = rootpath
        self._files = files
        self._modules = modules
        self._packages = packages
        self._directories = directories
        self._ignores = ignores

    def _check(self, filepath, pathlist, abspath=True, even_if_ignored=False):
        path = os.path.relpath(filepath, self.rootpath) if abspath else filepath
        for checkpath, ignored in pathlist:
            if path == checkpath:
                if ignored and not even_if_ignored:
                    break
                return True
        return False

    def check_module(self, filepath, abspath=True, even_if_ignored=False):
        return self._check(filepath, self._modules, abspath, even_if_ignored)

    def check_package(self, filepath, abspath=True, even_if_ignored=False):
        return self._check(filepath, self._packages, abspath, even_if_ignored)

    def check_file(self, filepath, abspath=True, even_if_ignored=False):
        return self._check(filepath, self._files, abspath, even_if_ignored)

    def _iter_paths(self, pathlist, abspath=True, include_ignored=False):
        for path, ignored in pathlist:
            if ignored and not include_ignored:
                continue
            if abspath:
                path = self.to_absolute_path(path)
            yield path

    def iter_file_paths(self, abspath=True, include_ignored=False):
        return self._iter_paths(self._files, abspath, include_ignored)

    def iter_package_paths(self, abspath=True, include_ignored=False):
        return self._iter_paths(self._packages, abspath, include_ignored)

    def iter_directory_paths(self, abspath=True, include_ignored=False):
        return self._iter_paths(self._directories, abspath, include_ignored)

    def iter_module_paths(self, abspath=True, include_ignored=False):
        return self._iter_paths(self._modules, abspath, include_ignored)

    def to_absolute_path(self, path):
        return os.path.abspath(os.path.join(self.rootpath, path))

    def get_minimal_syspath(self, absolute_paths=True):
        """
        Provide a list of directories that, when added to sys.path, would enable
        any of the discovered python modules to be found
        """
        # firstly, gather a list of the minimum path to each package
        package_list = set()
        packages = [p[0] for p in self._packages if not p[1]]
        for package in sorted(packages, key=len):
            parent = os.path.split(package)[0]
            if parent not in packages and parent not in package_list:
                package_list.add(parent)

        # now add the directory containing any modules who are not in packages
        module_list = []
        modules = [m[0] for m in self._modules if not m[1]]
        for module in modules:
            dirname = os.path.dirname(module)
            if dirname not in packages:
                module_list.append(dirname)

        full_list = sorted(set(module_list) | package_list, key=len)
        if absolute_paths:
            full_list = [os.path.join(self.rootpath, p).rstrip(os.path.sep) for p in full_list]
        return full_list


def is_virtualenv(path):
    if os.name == 'nt':
        # Windows!
        clues = ('Scripts', 'lib', 'include')
    else:
        clues = ('bin', 'lib', 'include')

    dircontents = os.listdir(path)

    if not all([clue in dircontents for clue in clues]):
        # we don't have the 3 directories which would imply
        # this is a virtualenvironment
        return False

    if not all([os.path.isdir(os.path.join(path, clue)) for clue in clues]):
        # some of them are not actually directories
        return False

    # if we do have all three directories, make sure that it's not
    # just a coincidence by doing some heuristics on the rest of
    # the directory
    if len(dircontents) > 7:
        # if there are more than 7 things it's probably not a virtualenvironment
        return False

    return True


def _find_paths(ignore, curpath, rootpath):
    files, modules, packages, directories = [], [], [], []

    for filename in os.listdir(curpath):
        if filename.startswith('.'):
            continue

        fullpath = os.path.join(curpath, filename)
        relpath = os.path.relpath(fullpath, rootpath)

        if os.path.islink(fullpath):
            continue

        ignored = any([m.search(relpath) for m in ignore])

        if os.path.isdir(fullpath):

            # is it probably a virtualenvironment?
            if is_virtualenv(fullpath):
                continue

            # this is a directory
            directories.append((relpath, ignored))

            # is it also a package?
            initpy = os.path.join(fullpath, '__init__.py')
            if os.path.exists(initpy) and os.path.isfile(initpy):
                packages.append((relpath, ignored))

            # do the same for this directory
            recurse = _find_paths(ignore, os.path.join(curpath, filename), rootpath)
            files += recurse[0]
            modules += recurse[1]
            packages += recurse[2]
            directories += recurse[3]

        else:
            # this is a file, is it a python module?
            if fullpath.endswith('.py'):
                modules.append((relpath, ignored))
            files.append((relpath, ignored))

    return files, modules, packages, directories


def find_python(ignores, path_argument):
    """
    Returns a FoundFiles class containing a list of files, packages, directories,
    where files are simply all python (.py) files, packages are directories
    containing an `__init__.py` file, and directories is a list of all directories.
    All paths are relative to the dirpath argument.
    """
    if os.path.isdir(path_argument):
        files, modules, directories, packages = _find_paths(ignores, path_argument, path_argument)
        return FoundFiles(path_argument, files, modules, directories, packages, ignores)
    else:
        return SingleFile(path_argument)
