from __future__ import unicode_literals

import json
import os
from collections import namedtuple

import six
from packaging.requirements import Requirement
from packaging.utils import canonicalize_name

from tox import reporter
from tox.config import DepConfig, get_py_project_toml
from tox.constants import BUILD_ISOLATED, BUILD_REQUIRE_SCRIPT

BuildInfo = namedtuple(
    "BuildInfo",
    ["requires", "backend_module", "backend_object", "backend_paths"],
)


def build(config, session):
    build_info = get_build_info(config.setupdir)
    package_venv = session.getvenv(config.isolated_build_env)
    package_venv.envconfig.deps_matches_subset = True

    # we allow user specified dependencies so the users can write extensions to
    # install additional type of dependencies (e.g. binary)
    user_specified_deps = package_venv.envconfig.deps
    package_venv.envconfig.deps = [DepConfig(r, None) for r in build_info.requires]
    package_venv.envconfig.deps.extend(user_specified_deps)

    if package_venv.setupenv():
        package_venv.finishvenv()
    if isinstance(package_venv.status, Exception):
        raise package_venv.status

    build_requires = get_build_requires(build_info, package_venv, config.setupdir)
    # we need to filter out requirements already specified in pyproject.toml or user deps
    base_build_deps = {
        canonicalize_name(Requirement(r.name).name) for r in package_venv.envconfig.deps
    }
    build_requires_dep = [
        DepConfig(r, None)
        for r in build_requires
        if canonicalize_name(Requirement(r).name) not in base_build_deps
    ]
    if build_requires_dep:
        with package_venv.new_action("build_requires", package_venv.envconfig.envdir) as action:
            package_venv.run_install_command(packages=build_requires_dep, action=action)
        package_venv.finishvenv()
    return perform_isolated_build(build_info, package_venv, config.distdir, config.setupdir)


def get_build_info(folder):
    toml_file = folder.join("pyproject.toml")

    # as per https://www.python.org/dev/peps/pep-0517/

    def abort(message):
        reporter.error("{} inside {}".format(message, toml_file))
        raise SystemExit(1)

    if not toml_file.exists():
        reporter.error("missing {}".format(toml_file))
        raise SystemExit(1)

    config_data = get_py_project_toml(toml_file)

    if "build-system" not in config_data:
        abort("build-system section missing")

    build_system = config_data["build-system"]

    if "requires" not in build_system:
        abort("missing requires key at build-system section")
    if "build-backend" not in build_system:
        abort("missing build-backend key at build-system section")

    requires = build_system["requires"]
    if not isinstance(requires, list) or not all(isinstance(i, six.text_type) for i in requires):
        abort("requires key at build-system section must be a list of string")

    backend = build_system["build-backend"]
    if not isinstance(backend, six.text_type):
        abort("build-backend key at build-system section must be a string")

    args = backend.split(":")
    module = args[0]
    obj = args[1] if len(args) > 1 else ""

    backend_paths = build_system.get("backend-path", [])
    if not isinstance(backend_paths, list):
        abort("backend-path key at build-system section must be a list, if specified")
    backend_paths = [folder.join(p) for p in backend_paths]

    normalized_folder = os.path.normcase(str(folder.realpath()))
    normalized_paths = (os.path.normcase(str(path.realpath())) for path in backend_paths)

    if not all(
        os.path.commonprefix((normalized_folder, path)) == normalized_folder
        for path in normalized_paths
    ):
        abort("backend-path must exist in the project root")

    return BuildInfo(requires, module, obj, backend_paths)


def perform_isolated_build(build_info, package_venv, dist_dir, setup_dir):
    with package_venv.new_action(
        "perform-isolated-build",
        package_venv.envconfig.envdir,
    ) as action:
        # need to start with an empty (but existing) source distribution folder
        if dist_dir.exists():
            dist_dir.remove(rec=1, ignore_errors=True)
        dist_dir.ensure_dir()

        result = package_venv._pcall(
            [
                package_venv.envconfig.envpython,
                BUILD_ISOLATED,
                str(dist_dir),
                build_info.backend_module,
                build_info.backend_object,
                os.path.pathsep.join(str(p) for p in build_info.backend_paths),
            ],
            returnout=True,
            capture_err=False,
            action=action,
            cwd=setup_dir,
        )
        reporter.verbosity2(result)
        return dist_dir.join(result.split("\n")[-2])


def get_build_requires(build_info, package_venv, setup_dir):
    with package_venv.new_action("get-build-requires", package_venv.envconfig.envdir) as action:
        result = package_venv._pcall(
            [
                package_venv.envconfig.envpython,
                BUILD_REQUIRE_SCRIPT,
                build_info.backend_module,
                build_info.backend_object,
                os.path.pathsep.join(str(p) for p in build_info.backend_paths),
            ],
            returnout=True,
            action=action,
            cwd=setup_dir,
            capture_err=False,
        )
        return json.loads(result.split("\n")[-2])
