"""
GateKeeper finder / loader implementations imported from here.

Things change quite a lot in python3.5 where imp is deprecated.


Useful environment variables (Used for debug, may change)

VEXT_DISABLED=1                         Disable vext by not adding it to sys.path
VEXT_LOG_BLOCKS=1                       Store any blocked imports in registry.blocked_imports
VEXT_ALLOWED_MODULES=module1,module2    Extra modules to allow
VEXT_DEBUG_LOG=1                        Debug logging

"""
import glob
import imp
import logging
import os
import re
import site
import sys
from contextlib import contextmanager
from distutils.sysconfig import get_python_lib
from os.path import isdir, isfile, join, basename, abspath, splitext, relpath
from vext import registry, logger
from vext.conf import open_spec
from vext.env import findsyspy, getsyssitepackages, in_venv
from vext.helpers import get_extra_path

try:
    unicode
except NameError:
    # Python 3
    basestring = unicode = str

log_blocks = 'VEXT_LOG_BLOCKS' in os.environ
remember_blocks = 'VEXT_REMEMBER_BLOCKS' in os.environ
blocked_imports = registry.blocked_imports  # if remember_blocks is True blocked imports are added here
allowed_modules = registry.allowed_modules  # populated by .vext files
added_dirs = registry.added_dirs
disable_vext = bool(os.environ.get('VEXT_DISABLED', False))
if hasattr(sys, 'argv') and 'setup.py' in sys.argv[0:2]:  # Somehow sys doesn't always have argv here
    print("Running during setup - temporarily disable vext")
    disable_vext = True  # Hack: - need a better way to disable during setup of vext itself

if 'VEXT_ALLOWED_MODULES' in os.environ:
    allowed_modules.update(re.findall(r'[^,\s]+', os.environ['VEXT_ALLOWED_MODULES']))

if os.name == 'nt':
    # Windows needs environment variables + paths to be strings
    env_t = str
else:
    env_t = unicode


def fix_path(p):
    """
    Convert path pointing subdirectory of virtualenv site-packages
    to system site-packages.

    Destination directory must exist for this to work.

    >>> fix_path('C:\\some-venv\\Lib\\site-packages\\gnome')
    'C:\\Python27\\lib\\site-packages\\gnome'
    """
    venv_lib = get_python_lib()

    if p.startswith(venv_lib):
        subdir = p[len(venv_lib) + 1:]

        for sitedir in getsyssitepackages():
            fixed_path = join(sitedir, subdir)
            if isdir(fixed_path):
                return fixed_path

    return p


@contextmanager
def fixup_paths():
    """
    Fixup paths added in .pth file that point to the virtualenv
    instead of the system site packages.

    In depth:  .PTH can execute arbitrary code, which might
    manipulate the PATH or sys.path

    :return:
    """
    original_paths = os.environ.get('PATH', "").split(os.path.pathsep)
    original_dirs = set(added_dirs)
    yield

    # Fix PATH environment variable
    current_paths = os.environ.get('PATH', "").split(os.path.pathsep)
    if original_paths != current_paths:
        changed_paths = set(current_paths).difference(set(original_paths))
        # rebuild PATH env var
        fixed_paths = []
        for path in current_paths:
            if path in changed_paths:
                fixed_paths.append(env_t(fix_path(path)))
            else:
                fixed_paths.append(env_t(path))
        os.environ['PATH'] = os.pathsep.join(fixed_paths)

    # Fix added_dirs
    if added_dirs != original_dirs:
        for path in set(added_dirs.difference(original_dirs)):
            fixed_path = fix_path(path)
            if fixed_path != path:
                print("Fix %s >> %s" % (path, fixed_path))
                added_dirs.remove(path)
                added_dirs.add(fixed_path)

                i = sys.path.index(path)  # not efficient... but shouldn't happen often
                sys.path[i] = fixed_path

                if env_t(fixed_path) not in os.environ['PATH']:
                    os.environ['PATH'].append(os.pathsep + env_t(fixed_path))


def addpackage(sys_sitedir, pthfile, known_dirs):
    """
    Wrapper for site.addpackage

    Try and work out which directories are added by
    the .pth and add them to the known_dirs set

    :param sys_sitedir: system site-packages directory
    :param pthfile: path file to add
    :param known_dirs: set of known directories
    """
    with open(join(sys_sitedir, pthfile)) as f:
        for n, line in enumerate(f):
            if line.startswith("#"):
                continue
            line = line.rstrip()
            if line:
                if line.startswith(("import ", "import\t")):
                    exec (line, globals(), locals())
                    continue
                else:
                    p_rel = join(sys_sitedir, line)
                    p_abs = abspath(line)
                    if isdir(p_rel):
                        os.environ['PATH'] += env_t(os.pathsep + p_rel)
                        sys.path.append(p_rel)
                        added_dirs.add(p_rel)
                    elif isdir(p_abs):
                        os.environ['PATH'] += env_t(os.pathsep + p_abs)
                        sys.path.append(p_abs)
                        added_dirs.add(p_abs)

    if isfile(pthfile):
        site.addpackage(sys_sitedir, pthfile, known_dirs)
    else:
        logging.debug("pth file '%s' not found")


class VextError(Exception):
    def __init__(*args, **kwargs):
        Exception.__init__(*args, **kwargs)


def filename_to_module(filename):
    """
    convert a filename like html5lib-0.999.egg-info to html5lib
    """
    find = re.compile(r"^[^.|-]*")
    name = re.search(find, filename).group(0)
    return name


class GateKeeperLoader(object):
    """
    Only allow known modules to import from the system site packages
    """

    def __init__(self, module_info, sitedir):
        self.module_info = module_info
        self.sitedir = sitedir

    def load_module(self, name):
        """
        Only lets modules in allowed_modules be loaded, others
        will get an ImportError
        """

        # Get the name relative to SITEDIR ..
        filepath = self.module_info[1]
        fullname = splitext( \
            relpath(filepath, self.sitedir) \
            )[0].replace(os.sep, '.')

        modulename = filename_to_module(fullname)
        if modulename not in allowed_modules:
            if remember_blocks:
                blocked_imports.add(fullname)
            if log_blocks:
                raise ImportError("Vext blocked import of '%s'" % modulename)
            else:
                # Standard error message
                raise ImportError("No module named %s" % modulename)

        if name not in sys.modules:
            try:
                logger.debug("load_module %s %s", name, self.module_info)
                module = imp.load_module(name, *self.module_info)
            except Exception as e:
                logger.debug(e)
                raise
            sys.modules[fullname] = module

        return sys.modules[fullname]


class GatekeeperFinder(object):
    """
    Only allow known modules to import from the system site packages
    """
    PATH_TRIGGER = 'VextFinder.PATH_TRIGGER'

    def __init__(self, path_entry):
        self.path_entry = path_entry
        try:
            # Wrap in exception handler in case something in a .pth file causes an exception.
            sitedir = getsyssitepackages()
        except Exception as e:
            sys.stderr.write("Vext disabled:  There was an issue getting the system site packages.\n")
            raise ImportError()
        
        if path_entry in (sitedir, GatekeeperFinder.PATH_TRIGGER):
            logger.debug("handle entry %s", path_entry)
            return
        else:
            logger.debug("not handling entry %s", path_entry)
            raise ImportError()

    def find_module(self, fullname, path=None):
        # TODO Need lots of unit tests around this
        module = sys.modules.get(fullname)