from __future__ import unicode_literals

import contextlib
import os
import sys

import pre_commit.constants as C
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import UNSET
from pre_commit.envcontext import Var
from pre_commit.languages import helpers
from pre_commit.parse_shebang import find_executable
from pre_commit.util import CalledProcessError
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output


ENVIRONMENT_DIR = 'py_env'


def bin_dir(venv):
    """On windows there's a different directory for the virtualenv"""
    bin_part = 'Scripts' if os.name == 'nt' else 'bin'
    return os.path.join(venv, bin_part)


def get_env_patch(venv):
    return (
        ('PYTHONHOME', UNSET),
        ('VIRTUAL_ENV', venv),
        ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))),
    )


def _find_by_py_launcher(version):  # pragma: no cover (windows only)
    if version.startswith('python'):
        try:
            return cmd_output(
                'py', '-{}'.format(version[len('python'):]),
                '-c', 'import sys; print(sys.executable)',
            )[1].strip()
        except CalledProcessError:
            pass


def _get_default_version():  # pragma: no cover (platform dependent)
    def _norm(path):
        _, exe = os.path.split(path.lower())
        exe, _, _ = exe.partition('.exe')
        if find_executable(exe) and exe not in {'python', 'pythonw'}:
            return exe

    # First attempt from `sys.executable` (or the realpath)
    # On linux, I see these common sys.executables:
    #
    # system `python`: /usr/bin/python -> python2.7
    # system `python2`: /usr/bin/python2 -> python2.7
    # virtualenv v: v/bin/python (will not return from this loop)
    # virtualenv v -ppython2: v/bin/python -> python2
    # virtualenv v -ppython2.7: v/bin/python -> python2.7
    # virtualenv v -ppypy: v/bin/python -> v/bin/pypy
    for path in {sys.executable, os.path.realpath(sys.executable)}:
        exe = _norm(path)
        if exe:
            return exe

    # Next try the `pythonX.X` executable
    exe = 'python{}.{}'.format(*sys.version_info)
    if find_executable(exe):
        return exe

    if _find_by_py_launcher(exe):
        return exe

    # Give a best-effort try for windows
    if os.path.exists(r'C:\{}\python.exe'.format(exe.replace('.', ''))):
        return exe

    # We tried!
    return C.DEFAULT


def get_default_version():
    # TODO: when dropping python2, use `functools.lru_cache(maxsize=1)`
    try:
        return get_default_version.cached_version
    except AttributeError:
        get_default_version.cached_version = _get_default_version()
        return get_default_version()


def _sys_executable_matches(version):
    if version == 'python':
        return True
    elif not version.startswith('python'):
        return False

    try:
        info = tuple(int(p) for p in version[len('python'):].split('.'))
    except ValueError:
        return False

    return sys.version_info[:len(info)] == info


def norm_version(version):
    if os.name == 'nt':  # pragma: no cover (windows)
        # first see if our current executable is appropriate
        if _sys_executable_matches(version):
            return sys.executable

        version_exec = _find_by_py_launcher(version)
        if version_exec:
            return version_exec

        # Try looking up by name
        version_exec = find_executable(version)
        if version_exec and version_exec != version:
            return version_exec

        # If it is in the form pythonx.x search in the default
        # place on windows
        if version.startswith('python'):
            return r'C:\{}\python.exe'.format(version.replace('.', ''))

    # Otherwise assume it is a path
    return os.path.expanduser(version)


def py_interface(_dir, _make_venv):
    @contextlib.contextmanager
    def in_env(prefix, language_version):
        envdir = prefix.path(helpers.environment_dir(_dir, language_version))
        with envcontext(get_env_patch(envdir)):
            yield

    def healthy(prefix, language_version):
        with in_env(prefix, language_version):
            retcode, _, _ = cmd_output(
                'python', '-c',
                'import ctypes, datetime, io, os, ssl, weakref',
                retcode=None,
                encoding=None,
            )
        return retcode == 0

    def run_hook(hook, file_args):
        with in_env(hook.prefix, hook.language_version):
            return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)

    def install_environment(prefix, version, additional_dependencies):
        additional_dependencies = tuple(additional_dependencies)
        directory = helpers.environment_dir(_dir, version)

        env_dir = prefix.path(directory)
        with clean_path_on_failure(env_dir):
            if version != C.DEFAULT:
                python = norm_version(version)
            else:
                python = os.path.realpath(sys.executable)
            _make_venv(env_dir, python)
            with in_env(prefix, version):
                helpers.run_setup_cmd(
                    prefix, ('pip', 'install', '.') + additional_dependencies,
                )

    return in_env, healthy, run_hook, install_environment


def make_venv(envdir, python):
    env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1')
    cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python)
    cmd_output(*cmd, env=env, cwd='/')


_interface = py_interface(ENVIRONMENT_DIR, make_venv)
in_env, healthy, run_hook, install_environment = _interface
