# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the Rez Project


from rez.utils._version import _rez_version
from rez.utils.schema import Required, extensible_schema_dict
from rez.utils.filesystem import retain_cwd
from rez.utils.formatting import PackageRequest
from rez.utils.data_utils import AttrDictWrapper
from rez.utils.logging_ import print_warning
from rez.exceptions import PackageMetadataError
from rez.package_resources import help_schema, _commands_schema, \
    _function_schema, late_bound
from rez.package_repository import create_memory_package_repository
from rez.packages import Package
from rez.package_py_utils import expand_requirement
from rez.vendor.schema.schema import Schema, Optional, Or, Use, And
from rez.vendor.six import six
from rez.vendor.version.version import Version
from contextlib import contextmanager
import os


basestring = six.string_types[0]


# this schema will automatically harden request strings like 'python-*'; see
# the 'expand_requires' function for more info.
#
package_request_schema = Or(And(basestring, Use(expand_requirement)),
                            And(PackageRequest, Use(str)))

tests_schema = Schema({
    Optional(basestring): Or(
        Or(basestring, [basestring]),
        extensible_schema_dict({
            "command": Or(basestring, [basestring]),
            Optional("requires"): [package_request_schema],
            Optional("run_on"): Or(basestring, [basestring]),
            Optional("on_variants"): Or(
                bool,
                {
                    "type": "requires",
                    "value": [package_request_schema]
                }
            )
        })
    )
})


package_schema = Schema({
    Optional("requires_rez_version"):   And(basestring, Use(Version)),

    Required("name"):                   basestring,
    Optional("base"):                   basestring,
    Optional("version"):                Or(basestring,
                                           And(Version, Use(str))),
    Optional('description'):            basestring,
    Optional('authors'):                [basestring],

    Optional('requires'):               late_bound([package_request_schema]),
    Optional('build_requires'):         late_bound([package_request_schema]),
    Optional('private_build_requires'): late_bound([package_request_schema]),

    # deliberately not possible to late bind
    Optional('variants'):               [[package_request_schema]],

    Optional('hashed_variants'):        bool,

    Optional('relocatable'):            late_bound(Or(None, bool)),
    Optional('cachable'):               late_bound(Or(None, bool)),

    Optional('uuid'):                   basestring,
    Optional('config'):                 dict,
    Optional('tools'):                  late_bound([basestring]),
    Optional('help'):                   late_bound(help_schema),

    Optional('tests'):                  late_bound(tests_schema),

    Optional('pre_commands'):           _commands_schema,
    Optional('commands'):               _commands_schema,
    Optional('post_commands'):          _commands_schema,
    Optional('pre_build_commands'):     _commands_schema,
    Optional('pre_test_commands'):      _commands_schema,

    # attributes specific to pre-built packages
    Optional("build_system"):           basestring,
    Optional("build_command"):          Or([basestring], basestring, False),
    Optional("preprocess"):             _function_schema,

    # arbitrary fields
    Optional(basestring):               object
})


class PackageMaker(AttrDictWrapper):
    """Utility class for creating packages."""
    def __init__(self, name, data=None, package_cls=None):
        """Create a package maker.

        Args:
            name (str): Package name.
        """
        super(PackageMaker, self).__init__(data)
        self.name = name
        self.package_cls = package_cls or Package

        # set by `make_package`
        self.installed_variants = []
        self.skipped_variants = []

    def get_package(self):
        """Create the analogous package.

        Returns:
            `Package` object.
        """
        # get and validate package data
        package_data = self._get_data()
        package_data = package_schema.validate(package_data)

        # check compatibility with rez version
        if "requires_rez_version" in package_data:
            ver = package_data.pop("requires_rez_version")

            if Version(_rez_version) < ver:
                raise PackageMetadataError(
                    "Failed reading package definition file: rez version >= %s "
                    "needed (current version is %s)" % (ver, _rez_version)
                )

        # create a 'memory' package repository containing just this package
        version_str = package_data.get("version") or "_NO_VERSION"
        repo_data = {self.name: {version_str: package_data}}
        repo = create_memory_package_repository(repo_data)

        # retrieve the package from the new repository
        family_resource = repo.get_package_family(self.name)
        it = repo.iter_packages(family_resource)
        package_resource = next(it)

        package = self.package_cls(package_resource)

        # revalidate the package for extra measure
        package.validate_data()
        return package

    def _get_data(self):
        data = self._data.copy()

        data.pop("installed_variants", None)
        data.pop("skipped_variants", None)
        data.pop("package_cls", None)

        data = dict((k, v) for k, v in data.items() if v is not None)
        return data


@contextmanager
def make_package(name, path, make_base=None, make_root=None, skip_existing=True,
                 warn_on_skip=True):
    """Make and install a package.

    Example:

        >>> def make_root(variant, path):
        >>>     os.symlink("/foo_payload/misc/python27", "ext")
        >>>
        >>> with make_package('foo', '/packages', make_root=make_root) as pkg:
        >>>     pkg.version = '1.0.0'
        >>>     pkg.description = 'does foo things'
        >>>     pkg.requires = ['python-2.7']

    Args:
        name (str): Package name.
        path (str): Package repository path to install package into.
        make_base (callable): Function that is used to create the package
            payload, if applicable.
        make_root (callable): Function that is used to create the package
            variant payloads, if applicable.
        skip_existing (bool): If True, detect if a variant already exists, and
            skip with a warning message if so.
        warn_on_skip (bool): If True, print warning when a variant is skipped.

    Yields:
        `PackageMaker` object.

    Note:
        Both `make_base` and `make_root` are called once per variant install,
        and have the signature (variant, path).

    Note:
        The 'installed_variants' attribute on the `PackageMaker` instance will
        be appended with variant(s) created by this function, if any.
    """
    maker = PackageMaker(name)
    yield maker

    # post-with-block:
    #

    package = maker.get_package()
    src_variants = []

    # skip those variants that already exist
    if skip_existing:
        for variant in package.iter_variants():
            variant_ = variant.install(path, dry_run=True)
            if variant_ is None:
                src_variants.append(variant)
            else:
                maker.skipped_variants.append(variant_)
                if warn_on_skip:
                    print_warning("Skipping installation: Package variant already "
                                  "exists: %s" % variant_.uri)
    else:
        src_variants = package.iter_variants()

    with retain_cwd():
        # install the package variant(s) into the filesystem package repo at `path`
        for variant in src_variants:
            variant_ = variant.install(path)

            base = variant_.base
            if make_base and base:
                if not os.path.exists(base):
                    os.makedirs(base)
                os.chdir(base)
                make_base(variant_, base)

            root = variant_.root
            if make_root and root:
                if not os.path.exists(root):
                    os.makedirs(root)
                os.chdir(root)
                make_root(variant_, root)

            maker.installed_variants.append(variant_)
