from __future__ import unicode_literals, absolute_import

import re
import sys
import zipp
import itertools

from .api import Distribution

if sys.version_info >= (3,):  # pragma: nocover
    from contextlib import suppress
    from pathlib import Path
else:  # pragma: nocover
    from contextlib2 import suppress  # noqa
    from itertools import imap as map  # type: ignore
    from pathlib2 import Path

    FileNotFoundError = IOError, OSError
    __metaclass__ = type


def install(cls):
    """Class decorator for installation on sys.meta_path."""
    sys.meta_path.append(cls)
    return cls


class NullFinder:
    @staticmethod
    def find_spec(*args, **kwargs):
        return None

    # In Python 2, the import system requires finders
    # to have a find_module() method, but this usage
    # is deprecated in Python 3 in favor of find_spec().
    # For the purposes of this finder (i.e. being present
    # on sys.meta_path but having no other import
    # system functionality), the two methods are identical.
    find_module = find_spec


@install
class MetadataPathFinder(NullFinder):
    """A degenerate finder for distribution packages on the file system.

    This finder supplies only a find_distributions() method for versions
    of Python that do not have a PathFinder find_distributions().
    """
    search_template = r'{pattern}(-.*)?\.(dist|egg)-info'

    @classmethod
    def find_distributions(cls, name=None, path=None):
        """Return an iterable of all Distribution instances capable of
        loading the metadata for packages matching the name
        (or all names if not supplied) along the paths in the list
        of directories ``path`` (defaults to sys.path).
        """
        if path is None:
            path = sys.path
        pattern = '.*' if name is None else re.escape(name)
        found = cls._search_paths(pattern, path)
        return map(PathDistribution, found)

    @classmethod
    def _search_paths(cls, pattern, paths):
        """
        Find metadata directories in paths heuristically.
        """
        return itertools.chain.from_iterable(
            cls._search_path(path, pattern)
            for path in map(Path, paths)
            )

    @classmethod
    def _search_path(cls, root, pattern):
        if not root.is_dir():
            return ()
        normalized = pattern.replace('-', '_')
        return (
            item
            for item in root.iterdir()
            if item.is_dir()
            and re.match(
                cls.search_template.format(pattern=normalized),
                str(item.name),
                flags=re.IGNORECASE,
                )
            )


class PathDistribution(Distribution):
    def __init__(self, path):
        """Construct a distribution from a path to the metadata directory."""
        self._path = path

    def read_text(self, filename):
        with suppress(FileNotFoundError):
            with self._path.joinpath(filename).open(encoding='utf-8') as fp:
                return fp.read()
        return None
    read_text.__doc__ = Distribution.read_text.__doc__

    def locate_file(self, path):
        return self._path.parent / path


@install
class WheelMetadataFinder(NullFinder):
    """A degenerate finder for distribution packages in wheels.

    This finder supplies only a find_distributions() method for versions
    of Python that do not have a PathFinder find_distributions().
    """
    search_template = r'{pattern}(-.*)?\.whl'

    @classmethod
    def find_distributions(cls, name=None, path=None):
        """Return an iterable of all Distribution instances capable of
        loading the metadata for packages matching the name
        (or all names if not supplied) along the paths in the list
        of directories ``path`` (defaults to sys.path).
        """
        if path is None:
            path = sys.path
        pattern = '.*' if name is None else re.escape(name)
        found = cls._search_paths(pattern, path)
        return map(WheelDistribution, found)

    @classmethod
    def _search_paths(cls, pattern, paths):
        return (
            path
            for path in map(Path, paths)
            if re.match(
                cls.search_template.format(pattern=pattern),
                str(path.name),
                flags=re.IGNORECASE,
                )
            )


class WheelDistribution(Distribution):
    def __init__(self, archive):
        self._archive = zipp.Path(archive)
        name, version = archive.name.split('-')[0:2]
        self._dist_info = '{}-{}.dist-info'.format(name, version)

    def read_text(self, filename):
        target = self._archive / self._dist_info / filename
        return target.read_text() if target.exists() else None
    read_text.__doc__ = Distribution.read_text.__doc__

    def locate_file(self, path):
        return self._archive / path
