# SPDX-License-Identifier: MIT

"""
A minimal PEP517 builder for traditional distributions.
"""

__version__ = "0.2"

import os
import pathlib

try:
    import tomllib
except ModuleNotFoundError:
    try:
        import tomli as tomllib
    except ModuleNotFoundError:
        from .vendored import tomli as tomllib

try:
    import pep517
except ModuleNotFoundError:
    from .vendored import pep517

try:
    import packaging.requirements as requirements
except ModuleNotFoundError:
    from .vendored.packaging import requirements


# Mostly stolen brazenly from pypa/build, loose dependency checking added.
def _check_dependency(
    req_string, ancestral_req_strings=(), parent_extras=frozenset(), loose=False
):
    """
    Verify that a dependency and all of its dependencies are met.

    :param req_string: Requirement string
    :param parent_extras: Extras (eg. "test" in myproject[test])
    :yields: Unmet dependencies
    """
    import importlib.metadata

    # The ~= operator is called "compatible with", ~=1.2 is expanded to >=1.2,
    # ==1.*. This works for virtual environments but can be troublesome in
    # distributions where the requirements may be very old. For example at the
    # time of writing, pytest-forked has setuptools ~=41.4 but OpenEmbedded has
    # 62.3.2, so the dependency fails.
    #
    # We can work around this by transforming ~= into >= if requested.
    if loose:
        req_string = req_string.replace("~=", ">=")

    req = requirements.Requirement(req_string)

    if req.marker:
        extras = frozenset(("",)).union(parent_extras)
        # a requirement can have multiple extras but ``evaluate`` can
        # only check one at a time.
        if all(not req.marker.evaluate(environment={"extra": e}) for e in extras):
            # if the marker conditions are not met, we pretend that the
            # dependency is satisfied.
            return

    try:
        dist = importlib.metadata.distribution(req.name)
    except importlib.metadata.PackageNotFoundError:
        # dependency is not installed in the environment.
        yield ancestral_req_strings + (req_string,)
    else:
        if req.specifier and not req.specifier.contains(dist.version, prereleases=True):
            # the installed version is incompatible.
            yield ancestral_req_strings + (req_string,)
        elif dist.requires:
            for other_req_string in dist.requires:
                # yields transitive dependencies that are not satisfied.
                yield from _check_dependency(
                    other_req_string,
                    ancestral_req_strings + (req_string,),
                    req.extras,
                    loose,
                )


class Builder:
    def __init__(self, source, loose_depends=False):
        source = pathlib.Path(source)

        self.loose_depends = loose_depends

        if (source / "pyproject.toml").exists():
            with open(source / "pyproject.toml", "rb") as f:
                pyproject = tomllib.load(f)

            self.build_sys = pyproject["build-system"]

        elif (source / "setup.py").exists():
            self.build_sys = {}
        else:
            raise RuntimeError("Cannot find pyproject.toml or setup.py")

        build_backend = self.build_sys.get(
            "build-backend", "setuptools.build_meta:__legacy__"
        )
        backend_path = self.build_sys.get("backend-path")

        self.hooks = pep517.wrappers.Pep517HookCaller(
            source,
            build_backend=build_backend,
            backend_path=backend_path,
        )

    def _check_dependencies(self, hook_name, config_options=None):
        get_requires_fn = getattr(self.hooks, hook_name)
        requires = set(get_requires_fn(config_options))
        # TODO this was copied from build and I'm not entirely sure what the return value is
        return {
            u for d in requires for u in _check_dependency(d, loose=self.loose_depends)
        }

    def check_bootstrap_dependencies(self):
        requires = set(self.build_sys.get("requires", []))
        # TODO this was copied from build and I'm not entirely sure what the return value is
        return {
            u for d in requires for u in _check_dependency(d, loose=self.loose_depends)
        }

    def check_sdist_dependencies(self, config_options=None):
        return self._check_dependencies("get_requires_for_build_sdist", config_options)

    def check_wheel_dependencies(self, config_options=None):
        return self._check_dependencies("get_requires_for_build_wheel", config_options)

    def build_sdist(self, destination, config_options=None):
        os.makedirs(destination, exist_ok=True)
        return self.hooks.build_sdist(destination, config_options)

    def build_wheel(self, destination, config_options=None):
        os.makedirs(destination, exist_ok=True)
        return self.hooks.build_wheel(destination, config_options)
