# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import absolute_import, print_function

import os

from pex.dist_metadata import Requirement
from pex.orderedset import OrderedSet
from pex.pip.version import PipVersionValue
from pex.requirements import LocalProjectRequirement
from pex.resolve.locked_resolve import LocalProjectArtifact, LockedResolve, LockStyle, TargetSystem
from pex.resolve.resolved_requirement import Pin
from pex.resolve.resolver_configuration import ResolverVersion
from pex.sorted_tuple import SortedTuple
from pex.typing import TYPE_CHECKING

if TYPE_CHECKING:
    from typing import Dict, Iterable, Mapping, Optional, Union

    import attr  # vendor:skip

    from pex.requirements import ParsedRequirement
else:
    from pex.third_party import attr


@attr.s(frozen=True)
class Lockfile(object):
    @classmethod
    def create(
        cls,
        pex_version,  # type: str
        style,  # type: LockStyle.Value
        requires_python,  # type: Iterable[str]
        target_systems,  # type: Iterable[TargetSystem.Value]
        pip_version,  # type: PipVersionValue
        resolver_version,  # type: ResolverVersion.Value
        requirements,  # type: Iterable[Union[Requirement, ParsedRequirement]]
        constraints,  # type: Iterable[Requirement]
        allow_prereleases,  # type: bool
        allow_wheels,  # type: bool
        allow_builds,  # type: bool
        prefer_older_binary,  # type: bool
        use_pep517,  # type: Optional[bool]
        build_isolation,  # type: bool
        transitive,  # type: bool
        locked_resolves,  # type: Iterable[LockedResolve]
        source=None,  # type: Optional[str]
    ):
        # type: (...) -> Lockfile

        pin_by_local_project_directory = {}  # type: Dict[str, Pin]
        requirement_by_local_project_directory = {}  # type: Dict[str, Requirement]
        for locked_resolve in locked_resolves:
            for locked_requirement in locked_resolve.locked_requirements:
                if isinstance(locked_requirement.artifact, LocalProjectArtifact):
                    local_directory = locked_requirement.artifact.directory
                    local_pin = locked_requirement.pin
                    pin_by_local_project_directory[local_directory] = local_pin
                    requirement_by_local_project_directory[
                        local_directory
                    ] = local_pin.as_requirement()

        def extract_requirement(req):
            # type: (Union[Requirement, ParsedRequirement]) -> Requirement
            if isinstance(req, Requirement):
                return req
            if isinstance(req, LocalProjectRequirement):
                local_project_directory = os.path.abspath(req.path)
                pin = pin_by_local_project_directory[local_project_directory]
                requirement = Requirement.parse(
                    "{project_name}{extras}=={version}{marker}".format(
                        project_name=pin.project_name,
                        extras="[{extras}]".format(extras=",".join(req.extras))
                        if req.extras
                        else "",
                        version=pin.version,
                        marker="; {marker}".format(marker=req.marker) if req.marker else "",
                    )
                )
                # N.B.: We've already mapped all available local projects above, but the user may
                # have supplied the local project requirement with more specific constraints (
                # extras and / or marker restrictions) and we need to honor those; so we over-write.
                requirement_by_local_project_directory[local_project_directory] = requirement
                return requirement
            return req.requirement

        resolve_requirements = OrderedSet(extract_requirement(req) for req in requirements)

        return cls(
            pex_version=pex_version,
            style=style,
            requires_python=SortedTuple(requires_python),
            target_systems=SortedTuple(target_systems),
            pip_version=pip_version,
            resolver_version=resolver_version,
            requirements=SortedTuple(resolve_requirements, key=str),
            constraints=SortedTuple(constraints, key=str),
            allow_prereleases=allow_prereleases,
            allow_wheels=allow_wheels,
            allow_builds=allow_builds,
            prefer_older_binary=prefer_older_binary,
            use_pep517=use_pep517,
            build_isolation=build_isolation,
            transitive=transitive,
            locked_resolves=SortedTuple(locked_resolves),
            local_project_requirement_mapping=requirement_by_local_project_directory,
            source=source,
        )

    pex_version = attr.ib()  # type: str
    style = attr.ib()  # type: LockStyle.Value
    requires_python = attr.ib()  # type: SortedTuple[str]
    target_systems = attr.ib()  # type: SortedTuple[TargetSystem.Value]
    pip_version = attr.ib()  # type: PipVersionValue
    resolver_version = attr.ib()  # type: ResolverVersion.Value
    requirements = attr.ib()  # type: SortedTuple[Requirement]
    constraints = attr.ib()  # type: SortedTuple[Requirement]
    allow_prereleases = attr.ib()  # type: bool
    allow_wheels = attr.ib()  # type: bool
    allow_builds = attr.ib()  # type: bool
    prefer_older_binary = attr.ib()  # type: bool
    use_pep517 = attr.ib()  # type: Optional[bool]
    build_isolation = attr.ib()  # type: bool
    transitive = attr.ib()  # type: bool
    locked_resolves = attr.ib()  # type: SortedTuple[LockedResolve]
    local_project_requirement_mapping = attr.ib(eq=False)  # type: Mapping[str, Requirement]
    source = attr.ib(default=None, eq=False)  # type: Optional[str]
