"""
Base classes for canonical order.

"""

from typing import Tuple


class KeyComparable:
    """Mathics3/WL defines a "canonical ordering" between elements,
    even where they would not otherwise be comparable either
     numerically, lexicographically, etc.

    For example, there is an ordering defined between a Function
    call with some number of arguments, and a ByteArray Atom.

    Symbols that have an "Orderless" attribute use this canonic
    ordering in arranging elements.

    Also, there are builtin-functions like "Order[]", "OrderQ[]", and
    "Ordering[]", that use this canonic ordering in their computation.

    To support the WL-predefined canonic order elements types, we need
    a way to compare arbitrary elements.  That is what this class is for.

    This class adds the boilerplate Python comparison operators, like
    __lt__, __eq__, etc. that Python provides for comparing Python
    objects.

    This class is not complete in of itself; it is intended to be
    mixed into other classes, and is used as a fallback when Python
    object's comparison does not apply due to type mismatch.

    Each class should provide a `element_order` property which
    is the primitive from which all other comparisons are based on.

    The class also contains a `pattern_precedence` property that
    provides the sort key used to order a list of rules according to
    the precedence they have in the evaluation loop. Note that pattern
    precedence and element ordering are separate concepts, although
    they both have a similar feel.

    """

    @property
    def element_order(self) -> tuple:
        """Return a tuple value that is used in ordering elements
        of an expression. The tuple is ultimately compared lexicographically.

        This is used in ``Sort[]`` comparisons and in the ordering
        that occurs in an M-Expression which has the ``Orderless``
        property.

        The encoded tuple/list is selected to have the property: when
        compared against element ``expr`` in a compound expression, if

           `self.element_order <= expr.element_order`

        then self comes before expr.

        The values in the positions of the tuple are used to indicate how
        comparison should be treated for specific element classes.

        """
        raise NotImplementedError

    @property
    def pattern_precedence(self) -> tuple:
        """
        Return a precedence value, a tuple, which is used in selecting
        which pattern to select when several match.
        """
        raise NotImplementedError

    def __eq__(self, other) -> bool:
        return (
            hasattr(other, "element_order")
            and self.element_order == other.element_order
        )

    def __gt__(self, other) -> bool:
        return self.element_order > other.element_order

    def __ge__(self, other) -> bool:
        return self.element_order >= other.element_order

    def __le__(self, other) -> bool:
        return self.element_order <= other.element_order

    def __lt__(self, other) -> bool:
        return self.element_order < other.element_order

    def __ne__(self, other) -> bool:
        return (
            not hasattr(other, "element_order")
        ) or self.element_order != other.element_order


class Monomial:
    """
    An object to sort monomials, used in Expression.get_sort_key and
    Symbol.get_sort_key.
    """

    def __init__(self, exps_dict):
        self.exps = exps_dict

    def __cmp(self, other) -> int:
        self_exps = self.exps.copy()
        other_exps = other.exps.copy()
        for var in self.exps:
            if var in other.exps:
                dec = min(self_exps[var], other_exps[var])
                self_exps[var] -= dec
                if not self_exps[var]:
                    del self_exps[var]
                other_exps[var] -= dec
                if not other_exps[var]:
                    del other_exps[var]
        self_exps = sorted((var, exp) for var, exp in self_exps.items())
        other_exps = sorted((var, exp) for var, exp in other_exps.items())

        index = 0
        self_len = len(self_exps)
        other_len = len(other_exps)
        while True:
            if index >= self_len and index >= other_len:
                return 0
            if index >= self_len:
                return -1  # self < other
            if index >= other_len:
                return 1  # self > other
            self_var, self_exp = self_exps[index]
            other_var, other_exp = other_exps[index]
            if self_var < other_var:
                return -1
            if self_var > other_var:
                return 1
            if self_exp != other_exp:
                if index + 1 == self_len or index + 1 == other_len:
                    # smaller exponents first
                    if self_exp < other_exp:
                        return -1
                    elif self_exp == other_exp:
                        return 0
                    else:
                        return 1
                else:
                    # bigger exponents first
                    if self_exp < other_exp:
                        return 1
                    elif self_exp == other_exp:
                        return 0
                    else:
                        return -1
            index += 1
        return 0

    def __eq__(self, other) -> bool:
        return self.__cmp(other) == 0

    def __le__(self, other) -> bool:
        return self.__cmp(other) <= 0

    def __lt__(self, other) -> bool:
        return self.__cmp(other) < 0

    def __ge__(self, other) -> bool:
        return self.__cmp(other) >= 0

    def __gt__(self, other) -> bool:
        return self.__cmp(other) > 0

    def __ne__(self, other) -> bool:
        return self.__cmp(other) != 0


###  SORT_KEYS prefix for pattern_precedence
#
# Pattern sort keys have 3 elements. The first one is a "magic" 4-bytes
# integer number representing different features of the element, like if
# it is an atom or an expression, if is an special pattern like `Blank`
# etc. The first order criteria is that these numbers are in ascending order.
# The second element is the sort_key for the head, and the third,
# is the list of sort_keys associated to each element of the expression,
# finished with ``END_OF_LIST_PATTERN_SORT_KEY`` to ensure that the longest
# list of patterns always come first.

# Let's start by defining the basic magic numbers:

# EXPRESSION BIT
PATTERN_SORT_KEY_IS_EXPRESSION = 0x00020000
PATTERN_SORT_KEY_VERBATIM = 0x00030000
PATTERN_SORT_KEY_LAST = 0xFFFFFFFFFFFF

# Blank and friends
PATTERN_SORT_KEY_EMPTY_ALTERNATIVES = 0x00000100
PATTERN_SORT_KEY_BLANK_WITH_HEAD = 0x00000B00
PATTERN_SORT_KEY_BLANKSEQUENCE_WITH_HEAD = 0x00000C00
PATTERN_SORT_KEY_BLANKNULLSEQUENCE_WITH_HEAD = 0x00000D00

PATTERN_SORT_KEY_BLANK_PURE = 0x00001500
PATTERN_SORT_KEY_BLANKSEQUENCE_PURE = 0x00001600
PATTERN_SORT_KEY_BLANKNULLSEQUENCE_PURE = 0x00001700

# OPTIONSPATTERN
PATTERN_SORT_KEY_OPTIONSPATTERN = 0x00002800

# Lower bits
PATTERN_SORT_KEY_NOT_PATTERNTEST = 0x00000001
PATTERN_SORT_KEY_OPTIONAL = 0x00000002
PATTERN_SORT_KEY_UNNAMED_PATTERN = 0x00000004
PATTERN_SORT_KEY_INCONDITIONAL = 0x00000008

# Used to mark a magic code as conditional or pattern test
PATTERN_SORT_KEY_CONDITIONAL = PATTERN_SORT_KEY_LAST - PATTERN_SORT_KEY_INCONDITIONAL
PATTERN_SORT_KEY_PATTERNTEST = PATTERN_SORT_KEY_LAST - PATTERN_SORT_KEY_NOT_PATTERNTEST
PATTERN_SORT_KEY_NAMEDPATTERN = PATTERN_SORT_KEY_LAST - PATTERN_SORT_KEY_UNNAMED_PATTERN


# Now, the basic combinations of these magic numbers, used on sort keys

# This is the numeric magic code for any Atom.
MAGIC_ATOM_SORT_KEY = (
    PATTERN_SORT_KEY_INCONDITIONAL
    + PATTERN_SORT_KEY_UNNAMED_PATTERN
    + PATTERN_SORT_KEY_NOT_PATTERNTEST
)

# The numeric magic code for an expression
BASIC_EXPRESSION_PATTERN_SORT_KEY = MAGIC_ATOM_SORT_KEY + PATTERN_SORT_KEY_IS_EXPRESSION

# Blanks
BLANK_WITH_PATTERN_PATTERN_SORT_KEY = (
    BASIC_EXPRESSION_PATTERN_SORT_KEY + PATTERN_SORT_KEY_BLANK_WITH_HEAD
)  # Blank[A] (`pat_`)
BLANK_GENERAL_PATTERN_SORT_KEY = (
    BASIC_EXPRESSION_PATTERN_SORT_KEY + PATTERN_SORT_KEY_BLANK_PURE
)  # Blank[] (`pat_`)
BLANKSEQUENCE_WITH_PATTERN_PATTERN_SORT_KEY = (
    BASIC_EXPRESSION_PATTERN_SORT_KEY + PATTERN_SORT_KEY_BLANKSEQUENCE_WITH_HEAD
)  # BlankSequence[A] (`pat__A`)
BLANKSEQUENCE_GENERAL_PATTERN_SORT_KEY = (
    BASIC_EXPRESSION_PATTERN_SORT_KEY + PATTERN_SORT_KEY_BLANKSEQUENCE_PURE
)  # BlankSequence[] (`pat__`)

BLANKNULLSEQUENCE_WITH_PATTERN_PATTERN_SORT_KEY = (
    BASIC_EXPRESSION_PATTERN_SORT_KEY + PATTERN_SORT_KEY_BLANKNULLSEQUENCE_WITH_HEAD
)  # BlankNullSequence[A] (`pat___A`)
BLANKNULLSEQUENCE_GENERAL_PATTERN_SORT_KEY = (
    BASIC_EXPRESSION_PATTERN_SORT_KEY + PATTERN_SORT_KEY_BLANKNULLSEQUENCE_PURE
)  # BlankNullSequence[] (`pat___`)


# Used in the case Alternative[]
EMPTY_ALTERNATIVE_PATTERN_SORT_KEY = (
    PATTERN_SORT_KEY_IS_EXPRESSION + PATTERN_SORT_KEY_EMPTY_ALTERNATIVES
)  # Alternatives[]
# OptionsPatterns
OPTIONSPATTERN_SORT_KEY = (
    BASIC_EXPRESSION_PATTERN_SORT_KEY + PATTERN_SORT_KEY_OPTIONSPATTERN
)  # OptionsPattern[]
# Verbatim
VERBATIM_PATTERN_SORT_KEY = (
    PATTERN_SORT_KEY_VERBATIM | BASIC_EXPRESSION_PATTERN_SORT_KEY
)  # Verbatim[expr]

# Now, two pattern sort keys that are used many times:
# Atoms
BASIC_ATOM_PATTERN_SORT_KEY = (MAGIC_ATOM_SORT_KEY, 0, 0)
# and "end of list" to ensure that patterns with more elements come first.
END_OF_LIST_PATTERN_SORT_KEY = (
    PATTERN_SORT_KEY_LAST,
)  # Used as the last element in the third field.


###  _ELT_ORDER suffixes are used in element ordering and
###  Expression.element_order().

BASIC_ATOM_NUMBER_ELT_ORDER = 0x00
BASIC_ATOM_STRING_ELT_ORDER = 0x01
BASIC_ATOM_BYTEARRAY_ELT_ORDER = 0x02
LITERAL_EXPRESSION_ELT_ORDER = 0x03
BASIC_ATOM_NUMERICARRAY_ELT_ORDER = 0x04

BASIC_NUMERIC_EXPRESSION_ELT_ORDER = 0x12
GENERAL_NUMERIC_EXPRESSION_ELT_ORDER = 0x13
IMAGE_EXPRESSION_ELT_ORDER = 0x13

BASIC_EXPRESSION_ELT_ORDER = 0x22
GENERAL_EXPRESSION_ELT_ORDER = 0x23


def wma_str_sort_key(s: str) -> Tuple[str, str]:
    """
    Return a Tuple providing the sort key
    reproduce the order of strings and symbols
    in WMA.
    For example, the following is a list of sorted
    strings in the WMA order:
    `{Abeja, ABEJA, ave de paso, Ave de paso, Ave de Paso, AVe}`
    The order criteria is: first sort case insensitive, then
    for the first different character in the original string,
    lower case comes before upper case.
    """
    # An alternative to this implementation would be to map the
    # characters in a way that
    # a -> A
    # A -> B
    # b -> C
    # B -> D
    #  ...
    # m -> Z
    # M -> a
    # n -> b
    # N -> c
    #  ...
    # z -> y
    # Z -> z
    # so the result is again a string. Another possibility would be
    # to return a wrapper class that implement this special comparison
    # on the fly through the method `__lt__`.
    return (
        s.lower(),
        s.swapcase(),
    )
