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


from rez.version._version import Version, VersionRange
from rez.version._util import _Common
import re


class VersionedObject(_Common):
    """Definition of a versioned object, eg ``foo-1.0``.

    ``foo`` is also a valid object definiton. When there is no version part, we
    are defining an unversioned object.

    .. note::
        Note that ``-``, ``@`` or ``#`` can be used as the seperator between object name
        and version, however this is purely cosmetic. ``foo-1`` is the same as ``foo@1``.
    """
    sep_regex_str = r'[-@#]'
    sep_regex = re.compile(sep_regex_str)

    def __init__(self, s):
        """
        Args:
            s (str):
        """
        self.name_ = None
        self.version_ = None
        self.sep_ = '-'
        if s is None:
            return

        m = self.sep_regex.search(s)
        if m:
            i = m.start()
            self.name_ = s[:i]
            self.sep_ = s[i]
            ver_str = s[i + 1:]
            self.version_ = Version(ver_str)
        else:
            self.name_ = s
            self.version_ = Version()

    @classmethod
    def construct(cls, name, version=None):
        """Create a VersionedObject directly from an object name and version.

        Args:
            name (str): Object name string.
            version (typing.Optional[Version]): Version object.
        """
        other = VersionedObject(None)
        other.name_ = name
        other.version_ = Version() if version is None else version
        return other

    @property
    def name(self):
        """Name of the object.

        Returns:
            str:
        """
        return self.name_

    @property
    def version(self):
        """Version of the object.

        Returns:
            Version:
        """
        return self.version_

    def as_exact_requirement(self):
        """Get the versioned object, as an exact requirement string.

        Returns:
            str: Equivalent requirement string, eg ``maya==2016.1``
        """
        sep_str = ''
        ver_str = ''
        if self.version_:
            sep_str = "=="
            ver_str = str(self.version_)
        return self.name_ + sep_str + ver_str

    def __eq__(self, other):
        return (isinstance(other, VersionedObject)
                and (self.name_ == other.name_)
                and (self.version_ == other.version_))

    def __hash__(self):
        return hash((self.name_, self.version_))

    def __str__(self):
        sep_str = ''
        ver_str = ''
        if self.version_:
            sep_str = self.sep_
            ver_str = str(self.version_)
        return self.name_ + sep_str + ver_str


class Requirement(_Common):
    """
    Defines a requirement for an object. For example, ``foo-5+`` means that you
    require any version of ``foo``, version 5 or greater. An unversioned
    requirement can also be used (``foo``), this means you require any version of
    foo. You can drop the hyphen between object name and version range if the
    version range starts with a non-alphanumeric character - eg ``foo<2``.

    There are two different prefixes that can be applied to a requirement:

    - ``!``: The conflict requirement. This means that you require this version
      range of an object NOT to be present. To conflict with all versions of an
      object, use "!foo".
    - ``~``: This is known as a "weak reference", and means, "I do not require this
      object, but if present, it must be within this range." It is equivalent to
      the *conflict of the inverse* of the given version range.

    There is one subtle case to be aware of. ``~foo`` is a requirement that has no
    effect. It means "I do not require foo, but if foo is present, it can
    be any version." This statement is still valid, but will produce a
    Requirement object with a None range.

    Examples of valid requirement strings:

    - ``foo-1.0``
    - ``foo@1.0``
    - ``foo#1.0``
    - ``foo-1+``
    - ``foo-1+<4.3``
    - ``foo<3``
    - ``foo==1.0.1``
    """
    sep_regex = re.compile(r'[-@#=<>]')

    def __init__(self, s, invalid_bound_error=True):
        """
        Args:
            s (str): Requirement string
            invalid_bound_error (bool): If True, raise :exc:`VersionError` if an
                impossible range is given, such as ``3+<2``.
        """
        self.name_ = None
        self.range_ = None
        self.negate_ = False
        self.conflict_ = False
        self._str = None
        self.sep_ = '-'
        if s is None:
            return

        self.conflict_ = s.startswith('!')
        if self.conflict_:
            s = s[1:]
        elif s.startswith('~'):
            s = s[1:]
            self.negate_ = True
            self.conflict_ = True

        m = self.sep_regex.search(s)
        if m:
            i = m.start()
            self.name_ = s[:i]
            req_str = s[i:]
            if req_str[0] in ('-', '@', '#'):
                self.sep_ = req_str[0]
                req_str = req_str[1:]

            self.range_ = VersionRange(
                req_str, invalid_bound_error=invalid_bound_error)
            if self.negate_:
                self.range_ = ~self.range_
        elif self.negate_:
            self.name_ = s
            # rare case - '~foo' equates to no effect
            self.range_ = None
        else:
            self.name_ = s
            self.range_ = VersionRange()

    @classmethod
    def construct(cls, name, range=None):
        """Create a requirement directly from an object name and VersionRange.

        Args:
            name (str): Object name string.
            range (typing.Optional[VersionRange]): If None, an unversioned requirement is
                created.
        """
        other = Requirement(None)
        other.name_ = name
        other.range_ = VersionRange() if range is None else range
        return other

    @property
    def name(self):
        """Name of the required object.

        Returns:
            str:
        """
        return self.name_

    @property
    def range(self):
        """Version range of the requirement.

        Returns:
            VersionRange:
        """
        return self.range_

    @property
    def conflict(self):
        """True if the requirement is a conflict requirement, eg "!foo", "~foo-1".

        Returns:
            bool:
        """
        return self.conflict_

    @property
    def weak(self):
        """True if the requirement is weak, eg "~foo".

        .. note::
            Note that weak requirements are also conflict requirements, but not
            necessarily the other way around.

        Returns:
            bool:
        """
        return self.negate_

    def safe_str(self):
        """Return a string representation that is safe for the current filesystem,
        and guarantees that no two different Requirement objects will encode to
        the same value.

        Returns:
            str:
        """
        return str(self)

    def conflicts_with(self, other):
        """Returns True if this requirement conflicts with another :class:`Requirement`
        or :class:`VersionedObject`.

        Returns:
            bool:
        """
        if isinstance(other, Requirement):
            if (self.name_ != other.name_) or (self.range is None) \
                    or (other.range is None):
                return False
            elif self.conflict:
                return False if other.conflict \
                    else self.range_.issuperset(other.range_)
            elif other.conflict:
                return other.range_.issuperset(self.range_)
            else:
                return not self.range_.intersects(other.range_)
        else:  # VersionedObject
            if (self.name_ != other.name_) or (self.range is None):
                return False
            if self.conflict:
                return (other.version_ in self.range_)
            else:
                return (other.version_ not in self.range_)

    def merged(self, other):
        """Merge two requirements.

        Two requirements can be in conflict and if so, this function returns
        None. For example, requests for ``foo-4`` and ``foo-6`` are in conflict,
        since both cannot be satisfied with a single version of foo.

        Some example successful requirements merges are:

        - ``foo-3+`` and ``!foo-5+`` == ``foo-3+<5``
        - ``foo-1`` and ``foo-1.5`` == ``foo-1.5``
        - ``!foo-2`` and ``!foo-5`` == ``!foo-2|5``

        Returns:
            Requirement: the merged result of two requirements.
        """
        if self.name_ != other.name_:
            return None  # cannot merge across object names

        def _r(r_):
            r = Requirement(None)
            r.name_ = r_.name_
            r.negate_ = r_.negate_
            r.conflict_ = r_.conflict_
            r.sep_ = r_.sep_
            return r

        if self.range is None:
            return other
        elif other.range is None:
            return self
        elif self.conflict:
            if other.conflict:
                r = _r(self)
                r.range_ = self.range_ | other.range_
                r.negate_ = (self.negate_ and other.negate_
                             and not r.range_.is_any())
                return r
            else:
                range_ = other.range - self.range
                if range_ is None:
                    return None
                else:
                    r = _r(other)
                    r.range_ = range_
                    return r
        elif other.conflict:
            range_ = self.range_ - other.range_
            if range_ is None:
                return None
            else:
                r = _r(self)
                r.range_ = range_
                return r
        else:
            range_ = self.range_ & other.range_
            if range_ is None:
                return None
            else:
                r = _r(self)
                r.range_ = range_
                return r

    def __eq__(self, other):
        return (isinstance(other, Requirement)
                and (self.name_ == other.name_)
                and (self.range_ == other.range_)
                and (self.conflict_ == other.conflict_))

    def __hash__(self):
        return hash(str(self))

    def __str__(self):
        if self._str is None:
            pre_str = '~' if self.negate_ else ('!' if self.conflict_ else '')
            range_str = ''
            sep_str = ''

            range_ = self.range_
            if self.negate_:
                range_ = ~range_ if range_ else VersionRange()

            if not range_.is_any():
                range_str = str(range_)
                if range_str[0] not in ('=', '<', '>'):
                    sep_str = self.sep_

            self._str = pre_str + self.name_ + sep_str + range_str
        return self._str


class RequirementList(_Common):
    """A list of requirements.

    This class takes a Requirement list and reduces it to the equivalent
    optimal form, merging any requirements for common objects. Order of objects
    is retained.
    """
    def __init__(self, requirements):
        """
        Args:
            requirements (list[Requirement]): List of requirements.
        """
        self.requirements_ = []
        self.conflict_ = None
        self.requirements_dict = {}
        self.names_ = set()
        self.conflict_names_ = set()

        for req in requirements:
            existing_req = self.requirements_dict.get(req.name)

            if existing_req is None:
                self.requirements_dict[req.name] = req
            else:
                merged_req = existing_req.merged(req)
                if merged_req is None:
                    self.conflict_ = (existing_req, req)
                    return
                else:
                    self.requirements_dict[req.name] = merged_req

        seen = set()

        # build optimised list, this intends to match original request order
        # as closely as possible
        for req in requirements:
            if req.name not in seen:
                seen.add(req.name)
                req_ = self.requirements_dict[req.name]
                self.requirements_.append(req_)

                if req_.conflict:
                    self.conflict_names_.add(req.name)
                else:
                    self.names_.add(req.name)

    @property
    def requirements(self):
        """Returns optimised list of requirements, or None if there are
        conflicts.

        Returns:
            list[Requirement]:
        """
        return self.requirements_

    @property
    def conflict(self):
        """Get the requirement conflict, if any.

        Returns:
            typing.Optional[tuple[Requirement]]: None if there is no conflict, otherwise a
            2-tuple containing the conflicting requirement objects.
        """
        return self.conflict_

    @property
    def names(self):
        """Set of names of requirements, not including conflict requirements.

        Returns:
            set[str]:
        """
        return self.names_

    @property
    def conflict_names(self):
        """Set of conflict requirement names.

        Returns:
            set[str]:
        """
        return self.conflict_names_

    def __iter__(self):
        for requirement in (self.requirements_ or []):
            yield requirement

    def get(self, name):
        """Returns the requirement for the given object, or None.

        Args:
            name (str): requirement to get.

        Returns:
            Requirement:
        """
        return self.requirements_dict.get(name)

    def __eq__(self, other):
        return (isinstance(other, RequirementList)
                and (self.requirements_ == other.requirements_)
                and (self.conflict_ == other.conflict_))

    def __str__(self):
        if self.conflict_:
            s1 = str(self.conflict_[0])
            s2 = str(self.conflict_[1])
            return "%s <--!--> %s" % (s1, s2)
        else:
            return ' '.join(str(x) for x in self.requirements_)
