#!/usr/bin/python
#
# Copyright(c) 2009, Gentoo Foundation
#
# Licensed under the GNU General Public License, v2

"""Subclasses portage.dep.Atom to provide methods on a Gentoo atom string."""

__all__ = ("Atom",)

# =======
# Imports
# =======

import weakref

import portage

from gentoolkit.cpv import CPV
from gentoolkit.versionmatch import VersionMatch
from gentoolkit import errors

# =======
# Classes
# =======

_UNSET = object()


class Atom(portage.dep.Atom, CPV):
    """Portage's Atom class with improvements from pkgcore.

    portage.dep.Atom provides the following instance variables:

    @type operator: str
    @ivar operator: one of ('=', '=*', '<', '>', '<=', '>=', '~', None)
    @type cp: str
    @ivar cp: cat/pkg
    @type cpv: str
    @ivar cpv: cat/pkg-ver (if ver)
    @type slot: str or None (modified to tuple if not None)
    @ivar slot: slot passed in as cpv:#
    """

    # Necessary for Portage versions < 2.1.7
    _atoms = weakref.WeakValueDictionary()

    @property
    def operator(self):
        # Old portage stored operator as a plain instance attribute in
        # __dict__; new portage exposes it as a read-only @property backed
        # by _operator.  super().operator only searches class __dict__
        # entries, so check the instance dict first.
        op = self.__dict__.get("operator", _UNSET)
        if op is _UNSET:
            op = super().operator
        return "" if op is None else op

    @property
    def version(self):
        # New portage's version property includes the revision; ours doesn't.
        if self._bare_version is None:
            self._set_cpv_chunks()
        return self._bare_version

    def __init__(self, atom):
        self.atom = atom

        try:
            portage.dep.Atom.__init__(self, atom)
        except portage.exception.InvalidAtom:
            raise errors.GentoolkitInvalidAtom(atom)

        # Seed _cp only if portage didn't already set it (old portage never
        # sets it; new portage's __slots__ value must not be clobbered).
        if not hasattr(self, "_cp"):
            self._cp = None
        self._category = None
        self._name = None
        self._bare_version = None
        self._revision = None
        self._fullversion = None
        self.validate = False

        # use_conditional is USE flag condition for this Atom to be required:
        # For: !build? ( >=sys-apps/sed-4.0.5 ), use_conditional = '!build'
        self.use_conditional = None

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            err = "other isn't of %s type, is %s"
            raise TypeError(err % (self.__class__, other.__class__))

        if self.operator != other.operator:
            return False

        if not CPV.__eq__(self, other):
            return False

        if bool(self.blocker) != bool(other.blocker):
            return False

        if self.blocker and other.blocker:
            if self.blocker.overlap.forbid != other.blocker.overlap.forbid:
                return False

        if self.use_conditional != other.use_conditional:
            return False

        # Don't believe Portage has something like this
        # c = cmp(self.negate_vers, other.negate_vers)
        # if c:
        #   return c

        if self.slot != other.slot:
            return False

        this_use = None
        if self.use is not None:
            this_use = sorted(self.use.tokens)
        that_use = None
        if other.use is not None:
            that_use = sorted(other.use.tokens)
        if this_use != that_use:
            return False

        # Not supported by Portage Atom yet
        # return cmp(self.repo_name, other.repo_name)
        return True

    def __hash__(self):
        return hash(self.atom)

    def __ne__(self, other):
        return not self == other

    def __lt__(self, other):
        if not isinstance(other, self.__class__):
            err = "other isn't of %s type, is %s"
            raise TypeError(err % (self.__class__, other.__class__))

        if self.operator != other.operator:
            return self.operator < other.operator

        if not CPV.__eq__(self, other):
            return CPV.__lt__(self, other)

        if bool(self.blocker) != bool(other.blocker):
            # We want non blockers, then blockers, so only return True
            # if self.blocker is True and other.blocker is False.
            return bool(self.blocker) > bool(other.blocker)

        if self.blocker and other.blocker:
            if self.blocker.overlap.forbid != other.blocker.overlap.forbid:
                # we want !! prior to !
                return self.blocker.overlap.forbid < other.blocker.overlap.forbid

        # Don't believe Portage has something like this
        # c = cmp(self.negate_vers, other.negate_vers)
        # if c:
        #   return c

        if self.slot != other.slot:
            if self.slot is None:
                return False
            elif other.slot is None:
                return True
            return self.slot < other.slot

        this_use = []
        if self.use is not None:
            this_use = sorted(self.use.tokens)
        that_use = []
        if other.use is not None:
            that_use = sorted(other.use.tokens)
        if this_use != that_use:
            return this_use < that_use

        # Not supported by Portage Atom yet
        # return cmp(self.repo_name, other.repo_name)

        return False

    def __gt__(self, other):
        if not isinstance(other, self.__class__):
            err = "other isn't of %s type, is %s"
            raise TypeError(err % (self.__class__, other.__class__))

        return not self <= other

    def __le__(self, other):
        if not isinstance(other, self.__class__):
            raise TypeError(
                f"other isn't of {self.__class__} type, is {other.__class__}"
            )
        return self < other or self == other

    def __ge__(self, other):
        if not isinstance(other, self.__class__):
            raise TypeError(
                f"other isn't of {self.__class__} type, is {other.__class__}"
            )
        return self > other or self == other

    def __repr__(self):
        uc = self.use_conditional
        uc = "%s? " % uc if uc is not None else ""
        return "<{} {!r}>".format(self.__class__.__name__, f"{uc}{self.atom}")

    def __setattr__(self, name, value):
        object.__setattr__(self, name, value)

    def intersects(self, other):
        """Check if a passed in package atom "intersects" this atom.

        Lifted from pkgcore.

        Two atoms "intersect" if a package can be constructed that
        matches both:
          - if you query for just "dev-lang/python" it "intersects" both
                "dev-lang/python" and ">=dev-lang/python-2.4"
          - if you query for "=dev-lang/python-2.4" it "intersects"
                ">=dev-lang/python-2.4" and "dev-lang/python" but not
                "<dev-lang/python-2.3"

        @type other: L{gentoolkit.atom.Atom} or
                L{gentoolkit.versionmatch.VersionMatch}
        @param other: other package to compare
        @see: L{pkgcore.ebuild.atom}
        """
        # Our "cp" (cat/pkg) must match exactly:
        if self.cp != other.cp:
            # Check to see if one is name only:
            # We don't bother checking if self.category is None: it can't be
            # because we're an Atom subclass and that would be invalid.
            return not other.category and self.name == other.name

        # Slot dep only matters if we both have one. If we do they
        # must be identical:
        this_slot = getattr(self, "slot", None)
        that_slot = getattr(other, "slot", None)
        if this_slot is not None and that_slot is not None and this_slot != that_slot:
            return False

        if self.repo is not None and other.repo is not None and self.repo != other.repo:
            return False

        # Use deps are similar: if one of us forces a flag on and the
        # other forces it off we do not intersect. If only one of us
        # cares about a flag it is irrelevant.

        # Skip the (very common) case of one of us not having use deps:
        this_use = getattr(self, "use", None)
        that_use = getattr(other, "use", None)
        if this_use and that_use:
            # Set of flags we do not have in common:
            flags = set(this_use.tokens) ^ set(that_use.tokens)
            for flag in flags:
                # If this is unset and we also have the set version we fail:
                if flag[0] == "-" and flag[1:] in flags:
                    return False

        # Remaining thing to check is version restrictions. Get the
        # ones we can check without actual version comparisons out of
        # the way first.

        # If one of us is unversioned we intersect:
        if not self.operator or not other.operator:
            return True

        # If we are both "unbounded" in the same direction we intersect:
        if ("<" in self.operator and "<" in other.operator) or (
            ">" in self.operator and ">" in other.operator
        ):
            return True

        # If one of us is an exact match we intersect if the other matches it:
        if self.operator == "=":
            if other.operator == "=*":
                return self.fullversion.startswith(other.fullversion)
            return VersionMatch(other, op=other.operator).match(self)
        if other.operator == "=":
            if self.operator == "=*":
                return other.fullversion.startswith(self.fullversion)
            return VersionMatch(self, op=self.operator).match(other)

        # If we are both ~ matches we match if we are identical:
        if self.operator == other.operator == "~":
            return self.version == other.version and self.revision == other.revision

        # If we are both glob matches we match if one of us matches the other.
        if self.operator == other.operator == "=*":
            return self.fullversion.startswith(
                other.fullversion
            ) or other.fullversion.startswith(self.fullversion)

        # If one of us is a glob match and the other a ~ we match if the glob
        # matches the ~ (ignoring a revision on the glob):
        if self.operator == "=*" and other.operator == "~":
            return other.fullversion.startswith(self.version)
        if other.operator == "=*" and self.operator == "~":
            return self.fullversion.startswith(other.version)

        # If we get here at least one of us is a <, <=, > or >=:
        if self.operator in ("<", "<=", ">", ">="):
            ranged = self
        else:
            ranged = other
            other = self

        if "<" in other.operator or ">" in other.operator:
            # We are both ranged, and in the opposite "direction" (or
            # we would have matched above). We intersect if we both
            # match the other's endpoint (just checking one endpoint
            # is not enough, it would give a false positive on <=2 vs >2)
            return VersionMatch(other, op=other.operator).match(
                ranged
            ) and VersionMatch(ranged, op=ranged.operator).match(other)

        if other.operator == "~":
            # Other definitely matches its own version. If ranged also
            # does we're done:
            if VersionMatch(ranged, op=ranged.operator).match(other):
                return True
            # The only other case where we intersect is if ranged is a
            # > or >= on other's version and a nonzero revision. In
            # that case other will match ranged. Be careful not to
            # give a false positive for ~2 vs <2 here:
            return ranged.operator in (">", ">=") and VersionMatch(
                other, op=other.operator
            ).match(ranged)

        if other.operator == "=*":
            # a glob match definitely matches its own version, so if
            # ranged does too we're done:
            if VersionMatch(ranged, op=ranged.operator).match(other):
                return True
            if "<" in ranged.operator:
                # If other.revision is not defined then other does not
                # match anything smaller than its own fullversion:
                if other.revision:
                    return False

                # If other.revision is defined then we can always
                # construct a package smaller than other.fullversion by
                # tagging e.g. an _alpha1 on.
                return ranged.fullversion.startswith(other.version)
            else:
                # Remaining cases where this intersects: there is a
                # package greater than ranged.fullversion and
                # other.fullversion that they both match.
                return ranged.fullversion.startswith(other.version)

        # Handled all possible ops.
        raise NotImplementedError(
            "Someone added an operator without adding it to intersects"
        )

    def get_depstr(self):
        """Returns a string representation of the original dep"""
        uc = self.use_conditional
        uc = "%s? " % uc if uc is not None else ""
        return f"{uc}{self.atom}"


# vim: set ts=4 sw=4 tw=79:
