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

import re
from typing import AbstractSet, Any, Callable, List, Optional, Type

from portmod.atom import Atom, InvalidAtom, useflag_re
from portmod.l10n import l10n


def use_reduce(
    depstr: str,
    uselist: AbstractSet[str] = set(),
    masklist: AbstractSet[str] = set(),
    matchall: bool = False,
    excludeall: AbstractSet[str] = set(),
    is_src_uri: bool = False,
    opconvert: bool = False,
    flat: bool = False,
    is_valid_flag: Optional[Callable[[str], bool]] = None,
    token_class: Optional[Type] = None,
    matchnone: bool = False,
) -> List:
    """
    Takes a dep string and reduces the use? conditionals out, leaving an array
    with subarrays. All redundant brackets are removed.
    Adapted from portage's use_reduce

    args:
        depstr: depstring
        uselist: List of use enabled flags
        masklist: List of masked flags (always treated as disabled)
        param matchall: Treat all conditionals as active. Used by inquisitor.
        excludeall: List of flags for which negated conditionals are always treated
                    as inactive.
        is_src_uri: Indicates if depstr represents a SRC_URI
        opconvert: Put every operator as first element into it's argument list
        flat: Create a flat list of all tokens
        is_valid_flag: Function that decides if a given use flag might be used in
                       use conditionals
        token_class: Convert all non operator tokens into this class
        matchnone: Treat all conditionals as inactive. Used by digestgen().

    returns:
        The use reduced depend array
    """

    if opconvert and flat:
        raise ValueError(
            "portage.dep.use_reduce: 'opconvert' and 'flat' are mutually exclusive"
        )

    if matchall and matchnone:
        raise ValueError(
            "portage.dep.use_reduce: 'matchall' and 'matchnone' are mutually exclusive"
        )

    def is_active(conditional):
        """
        Decides if a given use conditional is active.
        """
        if conditional.startswith("!"):
            flag = conditional[1:-1]
            is_negated = True
        else:
            flag = conditional[:-1]
            is_negated = False

        if is_valid_flag:
            if not is_valid_flag(flag):
                msg = (
                    "USE flag '{}' referenced in conditional '{}' "
                    "is not in IUSE".format(flag, conditional)
                )
                raise Exception(msg)
        else:
            if useflag_re.match(flag) is None:
                raise Exception(
                    "invalid use flag '{}' in conditional '{}'".format(
                        flag, conditional
                    )
                )

        if is_negated and flag in excludeall:
            return False

        if flag in masklist:
            return is_negated

        if matchall:
            return True

        if matchnone:
            return False

        return (flag in uselist and not is_negated) or (
            flag not in uselist and is_negated
        )

    def missing_white_space_check(token, pos):
        """
        Used to generate good error messages for invalid tokens.
        """
        for x in (")", "(", "||"):
            if token.startswith(x) or token.endswith(x):
                raise Exception(
                    "missing whitespace around '%s' at '%s', token %s"
                    % (x, token, pos + 1)
                )

    mysplit = depstr.split()
    # Count the bracket level.
    level = 0
    # We parse into a stack.
    # Every time we hit a '(', a new empty list is appended to the stack.
    # When we hit a ')', the last list in the stack is merged with list one level up.
    stack: List[List[Any]] = [[]]
    # Set need_bracket to True after use conditionals or ||. Other tokens need to ensure
    # that need_bracket is not True.
    need_bracket = False
    # Set need_simple_token to True after a SRC_URI arrow. Other tokens need to ensure
    # that need_simple_token is not True.
    need_simple_token = False

    for pos, token in enumerate(mysplit):
        if token == "(":
            if need_simple_token:
                raise Exception(
                    "expected: file name, got: '%s', token %s" % (token, pos + 1)
                )
            if len(mysplit) >= pos + 2 and mysplit[pos + 1] == ")":
                raise Exception(
                    "expected: dependency string, got: ')', token %s" % (pos + 1,)
                )
            need_bracket = False
            stack.append([])
            level += 1
        elif token == ")":
            if need_bracket:
                raise Exception("expected: '(', got: '%s', token %s" % (token, pos + 1))
            if need_simple_token:
                raise Exception(
                    "expected: file name, got: '%s', token %s" % (token, pos + 1)
                )
            if level > 0:
                level -= 1
                ll = stack.pop()

                is_single = (
                    len(ll) == 1
                    or (opconvert and ll and ll[0] == "||")
                    or (not opconvert and len(ll) == 2 and ll[0] == "||")
                )
                ignore = False

                if flat:
                    # In 'flat' mode, we simply merge all lists into a single large one.
                    if stack[level] and stack[level][-1][-1] == "?":
                        # The last token before the '(' that matches the current ')'
                        # was a use conditional. The conditional is removed in any case.
                        # Merge the current list if needed.
                        if is_active(stack[level][-1]):
                            stack[level].pop()
                            stack[level].extend(ll)
                        else:
                            stack[level].pop()
                    else:
                        stack[level].extend(ll)
                    continue

                if stack[level] and isinstance(stack[level][-1], str):
                    if stack[level][-1] == "||" and not ll:
                        # Optimize: || ( ) -> .
                        ll.append((token_class or str)("empty-any-of"))
                        stack[level].pop()
                    elif stack[level][-1][-1] == "?":
                        # The last token before the '(' that matches the current ')'
                        # was a use conditional, remove it and decide if we
                        # have to keep the current list.
                        if not is_active(stack[level][-1]):
                            ignore = True
                        stack[level].pop()

                def ends_in_any_of_dep(k):
                    return k >= 0 and stack[k] and stack[k][-1] == "||"

                def last_any_of_operator_level(k):
                    # Returns the level of the last || operator if it is in effect for
                    # the current level. It is not in effect, if there is a level, that
                    # ends in a non-operator. This is almost equivalent to
                    # stack[level][-1]=="||", expect that it skips empty levels.
                    while k >= 0:
                        if stack[k] and isinstance(stack[k][-1], str):
                            if stack[k][-1] == "||":
                                return k
                            elif stack[k][-1][-1] != "?":
                                return -1
                        k -= 1
                    return -1

                def special_append():
                    """
                    Use extend instead of append if possible.
                    This kills all redundant brackets.
                    """
                    if is_single:
                        # Either [A], [[...]] or [|| [...]]
                        if ll[0] == "||" and ends_in_any_of_dep(level - 1):
                            if opconvert:
                                stack[level].extend(ll[1:])
                            else:
                                stack[level].extend(ll[1])
                        elif len(ll) == 1 and isinstance(ll[0], list):
                            # l = [[...]]
                            last = last_any_of_operator_level(level - 1)
                            if last == -1:
                                if (
                                    opconvert
                                    and isinstance(ll[0], list)
                                    and ll[0]
                                    and ll[0][0] == "||"
                                ):
                                    stack[level].append(ll[0])
                                else:
                                    stack[level].extend(ll[0])
                            else:
                                if opconvert and ll[0] and ll[0][0] == "||":
                                    stack[level].extend(ll[0][1:])
                                else:
                                    stack[level].append(ll[0])
                        else:
                            stack[level].extend(ll)
                    else:
                        if opconvert and stack[level] and stack[level][-1] == "||":
                            stack[level][-1] = ["||"] + ll
                        else:
                            stack[level].append(ll)

                if ll and not ignore:
                    # The current list is not empty and we don't want to ignore it
                    # because of an inactive use conditional.
                    if not ends_in_any_of_dep(level - 1) and not ends_in_any_of_dep(
                        level
                    ):
                        # Optimize: ( ( ... ) ) -> ( ... ).
                        # Make sure there is no '||' hanging around.
                        stack[level].extend(ll)
                    elif not stack[level]:
                        # An '||' in the level above forces us to keep to brackets.
                        special_append()
                    elif is_single and ends_in_any_of_dep(level):
                        # Optimize: || ( A ) -> A,  || ( || ( ... ) ) -> || ( ... )
                        stack[level].pop()
                        special_append()
                    elif ends_in_any_of_dep(level) and ends_in_any_of_dep(level - 1):
                        # Optimize: || ( A || ( B C ) ) -> || ( A B C )
                        stack[level].pop()
                        stack[level].extend(ll)
                    else:
                        if opconvert and ends_in_any_of_dep(level):
                            # In opconvert mode, we have to move the operator from the
                            # level above into the current list.
                            stack[level].pop()
                            stack[level].append(["||"] + ll)
                        else:
                            special_append()

            else:
                raise Exception(
                    "no matching '%s' for '%s', token %s" % ("(", ")", pos + 1)
                )
        elif token == "||":
            if is_src_uri:
                raise Exception(
                    "any-of dependencies are not allowed in SRC_URI: token %s"
                    % (pos + 1,)
                )
            if need_bracket:
                raise Exception("expected: '(', got: '%s', token %s" % (token, pos + 1))
            need_bracket = True
            stack[level].append(token)
        elif token == "->":
            if need_simple_token:
                raise Exception(
                    "expected: file name, got: '%s', token %s" % (token, pos + 1)
                )
            if not is_src_uri:
                raise Exception(
                    "SRC_URI arrow are only allowed in SRC_URI: token %s" % (pos + 1,)
                )
            need_simple_token = True
            stack[level].append(token)
        else:
            if need_bracket:
                raise Exception("expected: '(', got: '%s', token %s" % (token, pos + 1))

            if need_simple_token and "/" in token:
                # The last token was a SRC_URI arrow,
                # make sure we have a simple file name.
                raise Exception(
                    "expected: file name, got: '%s', token %s" % (token, pos + 1)
                )

            if token[-1] == "?":
                need_bracket = True
            else:
                need_simple_token = False
                if token_class and not is_src_uri:
                    # Add a hack for SRC_URI here, to avoid conditional code at the
                    # consumer level
                    try:
                        token = token_class(token)
                    except InvalidAtom as e:
                        missing_white_space_check(token, pos)
                        raise Exception("Invalid atom (%s), token %s" % (e, pos + 1))
                    except SystemExit:
                        raise
                    except Exception:
                        missing_white_space_check(token, pos)
                        raise Exception(
                            "Invalid token '%s', token %s" % (token, pos + 1)
                        )

                    if not matchall and isinstance(token, Atom):
                        token = token.evaluate_conditionals(uselist)

            stack[level].append(token)

    if level != 0:
        raise Exception("Missing '%s' at end of string" % (")",))

    if need_bracket:
        raise Exception("Missing '%s' at end of string" % ("(",))

    if need_simple_token:
        raise Exception("Missing file name at end of string")

    return stack[0]


def check_required_use(
    required_use: str, use: AbstractSet[str], iuse_match: Callable[[str], bool]
) -> bool:
    """
    Adapted from portage's check_required_use
    Checks if the use flags listed in 'use' satisfy all
    constraints specified in 'constraints'.

    args:
        required_use: REQUIRED_USE string
        use: Enabled use flags
        iuse_match: Callable that takes a single flag argument and returns
            True if the flag is matched, false otherwise,
    returns:
        Indicates if REQUIRED_USE constraints are satisfied
    """

    valid_operators = ("||", "^^", "??")

    def is_active(token):
        if token.startswith("!"):
            flag = token[1:]
            is_negated = True
        else:
            flag = token
            is_negated = False

        if not flag or not iuse_match(flag):
            raise Exception("USE flag '%s' is not in IUSE" % (flag,))

        return (flag in use and not is_negated) or (flag not in use and is_negated)

    def is_satisfied(operator, argument):
        if operator == "||":
            return True in argument
        elif operator == "^^":
            return argument.count(True) == 1
        elif operator == "??":
            return argument.count(True) <= 1
        elif operator[-1] == "?":
            return False not in argument

    mysplit = required_use.split()
    level = 0
    stack: List[List[str]] = [[]]
    need_bracket = False

    for token in mysplit:
        if token == "(":
            need_bracket = False
            stack.append([])
            level += 1
        elif token == ")":
            if need_bracket:
                raise Exception("malformed syntax: '%s'" % required_use)
            if level > 0:
                level -= 1
                ll = stack.pop()
                op = None
                if stack[level]:
                    if stack[level][-1] in valid_operators:
                        op = stack[level].pop()
                        satisfied = is_satisfied(op, ll)
                        stack[level].append(satisfied)

                    elif (
                        not isinstance(stack[level][-1], bool)
                        and stack[level][-1][-1] == "?"
                    ):
                        op = stack[level].pop()
                        if is_active(op[:-1]):
                            satisfied = is_satisfied(op, ll)
                            stack[level].append(satisfied)
                        else:
                            continue

                if op is None:
                    satisfied = False not in ll
                    if ll:
                        stack[level].append(satisfied)
            else:
                raise Exception("malformed syntax: '%s'" % required_use)
        elif token in valid_operators:
            if need_bracket:
                raise Exception("malformed syntax: '%s'" % required_use)
            need_bracket = True
            stack[level].append(token)
        else:
            if need_bracket:
                raise Exception("malformed syntax: '%s'" % required_use)

            if token[-1] == "?":
                need_bracket = True
                stack[level].append(token)
            else:
                satisfied = is_active(token)
                stack[level].append(satisfied)

    if level != 0 or need_bracket:
        raise Exception("malformed syntax: '%s'" % required_use)

    return False not in stack[0]


def parse_usestr(usestr: str, token_class: Type = str):
    """
    Adapted from portage's check_required_use
    Converts usestring into structure containing lists of tokens,
    possibly preceeded by an operator that can be one of || ?? ^^ or a use conditional
    """

    valid_operators = ("||", "^^", "??")

    mysplit = usestr.split()
    level = 0
    stack: List[List[Any]] = [[]]
    need_bracket = False

    for token in mysplit:
        if token == "(":
            need_bracket = False
            stack.append([])
            level += 1
        elif token == ")":
            if need_bracket:
                raise Exception(f"malformed syntax: '{usestr}'")
            if level > 0:
                level -= 1
                ll = stack.pop()
                stack[level].append(ll)
            else:
                raise Exception(f"malformed syntax: '{usestr}'")
        elif token in valid_operators:
            if need_bracket:
                raise Exception(f"malformed syntax: '{usestr}'")
            need_bracket = True
            stack[level].append(token)
        else:
            if need_bracket:
                raise Exception(f"malformed syntax: '{usestr}'")

            if token[-1] == "?":
                need_bracket = True
                stack[level].append(token)
            else:
                stack[level].append(token_class(token))

    if level != 0 or need_bracket:
        raise Exception(f"malformed syntax: '{usestr}'")

    def opconvert(tokenlist):
        index = 0
        while index < len(tokenlist):
            token = tokenlist[index]
            if (
                token in valid_operators
                or isinstance(token, str)
                and token.endswith("?")
            ):
                tokenlist[index] = [token] + opconvert(tokenlist[index + 1])
                del tokenlist[index + 1]
            index += 1
        return tokenlist

    # Nested ||, ^^, and ?? groups can be combined
    # use-conditionals cannot (tokens ending in ?)
    def flatten(parent):
        index = 0
        while index < len(parent):
            token = parent[index]
            if isinstance(token, list):
                token = flatten(token)
                if token[0] in valid_operators and token[0] == parent[0]:
                    parent += token[1:]
                    del parent[index]
                    index -= 1
                elif (
                    token[0] not in valid_operators
                    and parent[0] not in valid_operators
                    and not token[0].endswith("?")
                ):
                    parent += token
                    del parent[index]
                    index -= 1
            index += 1
        return parent

    return flatten(opconvert(stack[0]))


def human_readable_required_use(required_use: List[Any]) -> str:
    """Takes a (recursive) list of tokens and turns it into a readable string"""
    if isinstance(required_use[0], list):
        string = "( " + human_readable_required_use(required_use[0]) + " )"
    else:
        string = required_use[0]

    for token in required_use[1:]:
        if isinstance(token, list):
            string += " ( " + human_readable_required_use(token) + " )"
        else:
            string += " " + token
    return re.sub(
        r"\( (\!?\w+\?)",  # Move use-conditionals in front of brackets
        r"\1 (",
        string.replace("( ^^", l10n("exactly-one-of") + " (")
        .replace("( ||", l10n("any-of") + " (")
        .replace("( ??", l10n("at-most-one-of") + " ("),
    )
