# Copyright 2019-2020 Portmod Authors
# Distributed under the terms of the GNU General Public License v3

import re
from collections import namedtuple
from typing import AbstractSet, Optional, Set, TypeVar

flag_re = r"[A-Za-z0-9][A-Za-z0-9+_-]*"
useflag_re = re.compile(r"^" + flag_re + r"$")
usedep_re = (
    r"(?P<prefix>[!-]?)(?P<flag>"
    + flag_re
    + r")(?P<default>(\(\+\)|\(\-\))?)(?P<suffix>[?=]?)"
)
_usedep_re = re.compile("^" + usedep_re + "$")

op_re = r"(?P<B>(!!))?(?P<OP>([<>]=?|[<>=~]))?"
cat_re = r"((?P<C>[A-Za-z0-9][A-Za-z0-9\-]*)/)?"
ver_re = r"(\d+)((\.\d+)*)([a-z]?)((_(pre|p|beta|alpha|rc)\d*)*)"
rev_re = r"(-(?P<PR>r[0-9]+))?"
repo_re = r"(::(?P<R>[A-Za-z0-9_][A-Za-z0-9_-]*(::installed)?))?"
_atom_re = re.compile(
    op_re
    + cat_re
    + r"(?P<P>(?P<PN>[A-Za-z0-9+_-]+?)(-(?P<PV>"
    + ver_re
    + r"))?)"
    + rev_re
    + repo_re
    + r"(\[(?P<USE>.*)\])?$"
)


class InvalidAtom(Exception):
    "Exception indicating an atom has invalid syntax"


class UnqualifiedAtom(Exception):
    """
    Exception indicating an atom, which was expected to be
    qualified with a category, has no category
    """

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

    def __str__(self):
        return f"Atom {self.atom} was expected to have a category!"


T = TypeVar("T", bound="Atom")


class Atom(str):
    CP: Optional[str]
    CPN: Optional[str]
    USE: Set[str] = set()
    P: str
    PN: str
    PF: str
    PV: Optional[str]
    PR: Optional[str]
    C: Optional[str]
    R: Optional[str]
    OP: Optional[str]
    BLOCK: bool
    PVR: Optional[str]
    CPF: str

    def __init__(self, atom: str):
        match = _atom_re.match(atom)
        if not match:
            raise InvalidAtom("Invalid atom %s. Cannot parse" % (atom))

        if match.group("P") and match.group("C"):
            self.CP = match.group("C") + "/" + match.group("P")
            self.CPN = match.group("C") + "/" + match.group("PN")
        else:
            self.CP = None
            self.CPN = None

        if match.group("USE"):
            self.USE = set(match.group("USE").split(","))
            for x in self.USE:
                m = _usedep_re.match(x)
                if not m:
                    raise InvalidAtom(
                        "Invalid use dependency {} in atom {}".format(atom, x)
                    )

        if match.group("PR"):
            self.PF = match.group("P") + "-" + match.group("PR")
        else:
            self.PF = match.group("P")

        self.P = match.group("P")
        self.PN = match.group("PN")
        self.PV = match.group("PV")
        self.PR = match.group("PR")
        self.C = match.group("C")
        self.R = match.group("R")
        self.OP = match.group("OP")
        self.BLOCK = match.group("B") is not None
        self.PVR = self.PV
        if self.PR:
            self.PVR += "-" + self.PR

        if self.C:
            self.CPF = self.C + "/" + self.PF
        else:
            self.CPF = self.PF

        if self.OP is not None and self.PV is None:
            raise InvalidAtom(
                "Atom %s has a comparison operator but no version!" % (atom)
            )

    def evaluate_conditionals(self, use: AbstractSet[str]) -> "Atom":
        """
        Create an atom instance with any USE conditionals evaluated.
        @param use: The set of enabled USE flags
        @return: an atom instance with any USE conditionals evaluated
        """
        tokens = set()

        for x in self.USE:
            m = _usedep_re.match(x)

            if m is not None:
                operator = m.group("prefix") + m.group("suffix")
                flag = m.group("flag")
                default = m.group("default")
                if default is None:
                    default = ""

                if operator == "?":
                    if flag in use:
                        tokens.add(flag + default)
                elif operator == "=":
                    if flag in use:
                        tokens.add(flag + default)
                    else:
                        tokens.add("-" + flag + default)
                elif operator == "!=":
                    if flag in use:
                        tokens.add("-" + flag + default)
                    else:
                        tokens.add(flag + default)
                elif operator == "!?":
                    if flag not in use:
                        tokens.add("-" + flag + default)
                else:
                    tokens.add(x)
            else:
                raise Exception("Internal Error when processing atom conditionals")

        atom = Atom(self)
        atom.USE = tokens
        return atom

    def strip_use(self: T) -> T:
        """Returns the equivalent of this atom with the USE dependencies removed"""
        return self.__class__(re.sub(r"\[.*\]", "", str(self)))


class QualifiedAtom(Atom):
    """Atoms that include categories"""

    CP: str
    CPN: str
    CPF: str
    C: str

    def __init__(self, atom: str):
        super().__init__(atom)

        if not self.C:
            raise UnqualifiedAtom(atom)


class VAtom(QualifiedAtom):
    """Atoms that include version information"""

    PV: str
    PVR: str

    def __init__(self, atom: str):
        super().__init__(atom)
        assert self.PV


class FQAtom(VAtom):
    """Atoms that include all possible non-optional fields"""

    R: str

    def __init__(self, atom: str):
        super().__init__(atom)
        assert self.R


VersionMatch = namedtuple(
    "VersionMatch", ["version", "numeric", "letter", "suffix", "revision"]
)


def suffix_gt(a_suffix: str, b_suffix: str) -> bool:
    """Returns true iff a_suffix > b_suffix"""
    suffixes = ["alpha", "beta", "pre", "rc", "p"]
    return suffixes.index(a_suffix) > suffixes.index(b_suffix)


def version_gt(version1: str, version2: str) -> bool:
    """
    Version comparision function

    args:
        version1: A version string
        version2: Another version string

    returns:
        True if and only if version1 is greater than version2
    """
    vre = re.compile(
        r"(?P<NUMERIC>[\d\.]+)"
        r"(?P<LETTER>[a-z])?"
        r"(?P<SUFFIX>(_[a-z]+\d*)*)"
        r"(-r(?P<REV>\d+))?"
    )
    match1 = vre.match(version1)
    match2 = vre.match(version2)

    assert match1 is not None
    assert match2 is not None
    v_match1 = VersionMatch(
        version=version1,
        numeric=match1.group("NUMERIC").split("."),
        letter=match1.group("LETTER") or "",
        suffix=match1.group("SUFFIX") or "",
        revision=int(match1.group("REV") or "0"),
    )
    v_match2 = VersionMatch(
        version=version2,
        numeric=match2.group("NUMERIC").split("."),
        letter=match2.group("LETTER") or "",
        suffix=match2.group("SUFFIX") or "",
        revision=int(match2.group("REV") or "0"),
    )

    # Compare numeric components
    for index, val in enumerate(v_match1.numeric):
        if index >= len(v_match2.numeric):
            return True
        if int(val) > int(v_match2.numeric[index]):
            return True
        if int(val) < int(v_match2.numeric[index]):
            return False
    if len(v_match2.numeric) > len(v_match1.numeric):
        return False

    # Compare letter components
    if v_match1.letter > v_match2.letter:
        return True
    if v_match1.letter < v_match2.letter:
        return False

    # Compare suffixes
    if v_match1.suffix:
        a_suffixes = v_match1.suffix.lstrip("_").split("_")
    else:
        a_suffixes = []
    if v_match2.suffix:
        b_suffixes = v_match2.suffix.lstrip("_").split("_")
    else:
        b_suffixes = []
    for a_s, b_s in zip(a_suffixes, b_suffixes):
        asm = re.match(r"(?P<S>[a-z]+)(?P<N>\d+)?", a_s)
        bsm = re.match(r"(?P<S>[a-z]+)(?P<N>\d+)?", b_s)
        assert asm
        assert bsm
        a_suffix = asm.group("S")
        b_suffix = bsm.group("S")
        a_suffix_num = int(asm.group("N") or "0")
        b_suffix_num = int(bsm.group("N") or "0")
        if a_suffix == b_suffix:
            if b_suffix_num > a_suffix_num:
                return False
            if a_suffix_num > b_suffix_num:
                return True
        elif suffix_gt(a_suffix, b_suffix):
            return True
        else:
            return False
    # More suffixes implies an earlier version,
    # except when the suffix is _p
    if len(a_suffixes) > len(b_suffixes):
        if a_suffixes[len(b_suffixes)].startswith("p"):
            return True
        return False
    if len(a_suffixes) < len(b_suffixes):
        if b_suffixes[len(a_suffixes)].startswith("p"):
            return False
        return True

    # Compare revisions
    if v_match1.revision > v_match2.revision:
        return True
    if v_match1.revision < v_match2.revision:
        return False

    # Equal
    return False


def atom_sat(specific: Atom, generic: Atom, *, ignore_name: bool = False) -> bool:
    """
    Determines if a fully qualified atom (can only refer to a single package)
    satisfies a generic atom
    """

    if not ignore_name:
        if specific.PN != generic.PN:
            # Mods must have the same name
            return False

        if generic.C and (generic.C != specific.C):
            # If para defines category, it must match
            return False

    if generic.R and (generic.R != specific.R):
        # If para defines repo, it must match
        return False

    if not generic.OP:
        # Simple atom, either one version or all versions will satisfy

        # Check if version is correct
        if generic.PV and (specific.PV != generic.PV):
            return False

        # Check if revision is correct
        if generic.PR and (specific.PR != generic.PR):
            return False
    elif generic.PV and specific.PV:
        assert generic.PVR and specific.PVR
        equal = specific.PVR == generic.PVR
        verequal = specific.PV == generic.PV
        lessthan = version_gt(generic.PVR, specific.PVR)
        greaterthan = version_gt(specific.PVR, generic.PVR)

        if generic.OP == "=":
            return equal
        if generic.OP == "~":
            return verequal
        if generic.OP == "<":
            return lessthan
        if generic.OP == "<=":
            return equal or lessthan
        if generic.OP == ">":
            return greaterthan
        if generic.OP == ">=":
            return equal or greaterthan

    return True
