# -*- coding: utf-8 -*-
"""This module contains the expression classes.

`Expressions <Expression>` can be used to model any kind of tree-like data structure. They consist of `operations
<Operation>` and `symbols <Symbol>`. In addition, `patterns <Pattern>` can be constructed, which may additionally,
contain `wildcards <Wildcard>` and variables.

You can define your own symbols and operations like this:

>>> f = Operation.new('f', Arity.variadic)
>>> a = Symbol('a')
>>> b = Symbol('b')

Then you can compose expressions out of these:

>>> print(f(a, b))
f(a, b)

For more information on how to create you own `operations <Operation>` and `symbols <Symbol>` you can look at their
documentation.

Normal expressions are immutable and hence :term:`hashable`:

>>> expr = f(b, x_)
>>> print(expr)
f(b, x_)
>>> hash(expr) == hash(expr)
True

Hence, some of the expression's properties are cached and nor updated when you modify them:

>>> expr.is_constant
False
>>> expr.operands = [a]
>>> expr.is_constant
False
>>> print(expr)
f(a)
>>> f(a).is_constant
True

Therefore, you should modify an expression but rather create a new one:

>>> expr2 = type(expr)(*[a])
>>> expr2.is_constant
True
>>> print(expr2)
f(a)
"""
from abc import ABCMeta
import keyword
from enum import Enum, EnumMeta
# pylint: disable=unused-import
from typing import Callable, Iterator, List, NamedTuple, Optional, Set, Tuple, Type, Union
# pylint: enable=unused-import

from multiset import Multiset

from ..utils import cached_property

__all__ = [
    'Expression', 'Arity', 'Atom', 'Symbol', 'Wildcard', 'Operation', 'SymbolWildcard', 'Pattern', 'make_dot_variable',
    'make_plus_variable', 'make_star_variable', 'make_symbol_variable', 'AssociativeOperation', 'CommutativeOperation',
    'OneIdentityOperation'
]

ExprPredicate = Optional[Callable[['Expression'], bool]]
ExpressionsWithPos = Iterator[Tuple['Expression', Tuple[int, ...]]]

MultisetOfStr = Multiset
MultisetOfVariables = Multiset


class Expression:
    """Base class for all expressions.

    Do not subclass this class directly but rather :class:`Symbol` or :class:`Operation`.
    Creating a direct subclass of Expression might break several (matching) algorithms.

    Attributes:
        head (Optional[Union[type, Atom]]):
            The head of the expression. For an operation, it is the type of the operation (i.e. a subclass of
            :class:`Operation`). For wildcards, it is ``None``. For symbols, it is the symbol itself.
    """

    def __init__(self, variable_name):
        super().__init__()
        self.variable_name = variable_name

    @cached_property
    def variables(self) -> MultisetOfVariables:
        """A multiset of the variables occurring in the expression."""
        variables = Multiset()
        self.collect_variables(variables)
        return variables

    def collect_variables(self, variables: MultisetOfVariables) -> None:
        """Recursively adds all variables occuring in the expression to the given multiset.

        This is used internally by `variables`. Needs to be overwritten by inheriting container expression classes.
        This method can be used when gathering the `variables` of multiple expressions, because only one multiset
        needs to be created and that is more efficient.

        Args:
            variables:
                Multiset of variables. All variables contained in the expression are recursively added to this multiset.
        """
        if self.variable_name is not None:
            variables.add(self.variable_name)

    @cached_property
    def symbols(self) -> MultisetOfStr:
        """A multiset of the symbol names occurring in the expression."""
        symbols = Multiset()
        self.collect_symbols(symbols)
        return symbols

    def collect_symbols(self, symbols: MultisetOfStr) -> None:
        """Recursively adds all symbols occuring in the expression to the given multiset.

        This is used internally by `symbols`. Needs to be overwritten by inheriting expression classes that
        can contain symbols. This method can be used when gathering the `symbols` of multiple expressions, because only
        one multiset needs to be created and that is more efficient.

        Args:
            symbols:
                Multiset of symbols. All symbols contained in the expression are recursively added to this multiset.
        """
        pass

    @cached_property
    def is_constant(self) -> bool:
        """True, iff the expression does not contain any wildcards."""
        return self._is_constant()

    @staticmethod
    def _is_constant() -> bool:
        return True

    @cached_property
    def is_syntactic(self) -> bool:
        """True, iff the expression does not contain any associative or commutative operations or sequence wildcards."""
        return self._is_syntactic()

    @staticmethod
    def _is_syntactic() -> bool:
        return True

    def with_renamed_vars(self, renaming) -> 'Expression':
        """Return a copy of the expression with renamed variables."""
        raise NotImplementedError()

    def preorder_iter(self, predicate: ExprPredicate=None) -> ExpressionsWithPos:
        """Iterates over all subexpressions that match the (optional) `predicate`.

        Args:
            predicate:
                A predicate to filter what expressions are yielded. It gets the expression and if it returns ``True``,
                the expression is yielded.

        Yields:
            Every subexpression along with a position tuple. Each item in the tuple is the position of an operation
            operand:

                - ``()`` is the position of the root element
                - ``(0, )`` that of its first operand
                - ``(0, 1)`` the position of the second operand of the root's first operand.
                - etc.

            A variable's expression always has the position ``0`` relative to the variable, i.e. if the root is a
            variable, then its expression has the position ``(0, )``.
        """
        yield from self._preorder_iter(predicate, ())

    def _preorder_iter(self, predicate: ExprPredicate, position: Tuple[int, ...]) -> ExpressionsWithPos:
        if predicate is None or predicate(self):
            yield self, position

    def __getitem__(self, position: Union[Tuple[int, ...], slice]) -> 'Expression':
        """Return the subexpression at the given position(s).

        It is also possible to use a slice notation to extract a sequence of subexpressions:

        >>> expr = f(a, b, a, c)
        >>> expr[(1, ):(2, )]
        [Symbol('b'), Symbol('a')]

        Args:
            position:
                The position as a tuple. See :meth:`preorder_iter` for its format.
                Alternatively, a range of positions can be passed using the slice notation.

        Returns:
            The subexpression at the given position(s).

        Raises:
            IndexError: If the position is invalid, i.e. it refers to a non-existing subexpression.
        """
        if isinstance(position, slice):
            if len(position.start) != len(position.stop):
                raise IndexError('Invalid slice: Start and stop must have the same length')
            if len(position.start) == 0:
                return [self]
            raise IndexError('Invalid slice: Parent expression is not an operation')
        if len(position) == 0:
            return self
        raise IndexError("Invalid position")

    def __contains__(self, expression: 'Expression') -> bool:
        return self == expression

    def __hash__(self):
        raise NotImplementedError()



_ArityBase = NamedTuple('_ArityBase', [('min_count', int), ('fixed_size', bool)])


class Arity(_ArityBase):
    """Arity of an operator as (`int`, `bool`) tuple.

    The first component is the minimum number of operands.
    If the second component is ``True``, the operator has fixed width arity. In that case, the first component
    describes the fixed number of operands required.
    If it is ``False``, the operator has variable width arity.
    """
    pass


Arity.nullary = Arity(0, True)
Arity.unary = Arity(1, True)
Arity.binary = Arity(2, True)
Arity.ternary = Arity(3, True)
Arity.polyadic = Arity(2, False)
Arity.variadic = Arity(0, False)


class _OperationMeta(ABCMeta):
    """Metaclass for `Operation`

    This metaclass is mainly used to override :meth:`__call__` to provide simplification when creating a
    new operation expression. This is done to avoid problems when overriding ``__new__`` of the operation class.
    """

    def __init__(cls, name, bases, dct):
        super(_OperationMeta, cls).__init__(name, bases, dct)

        if cls.arity[1] and cls.one_identity:
            raise TypeError('{}: An operation with fixed arity cannot have one_identity = True.'.format(name))

        if cls.arity == Arity.unary and cls.infix:
            raise TypeError('{}: Unary operations cannot use infix notation.'.format(name))

        cls.head = cls

    def __repr__(cls):
        if cls is Operation:
            return super().__repr__()
        flags = []
        if cls.associative:
            flags.append('associative')
        if cls.commutative:
            flags.append('commutative')
        if cls.one_identity:
            flags.append('one_identity')
        if cls.infix:
            flags.append('infix')
        return '{}[{!r}, {!r}, {}]'.format(cls.__name__, cls.name, cls.arity, ', '.join(flags))

    def __str__(cls):
        return cls.name

    def __call__(cls, *operands: Expression, variable_name=None):
        # __call__ is overridden, so that for one_identity operations with a single argument
        # that argument can be returned instead
        operands = list(operands)
        one_identity_applies = cls._simplify(operands)
        if one_identity_applies:
            return operands[0]

        operation = Expression.__new__(cls)
        operation.__init__(operands, variable_name=variable_name)

        return operation

    def _simplify(cls, operands: List[Expression]) -> bool:
        """Flatten/sort the operands of associative/commutative operations.

        Returns:
            True iff *one_identity* is True and the operation contains a single
            argument that is not a sequence wildcard.
        """

        if cls.associative:
            new_operands = []  # type: List[Expression]
            for operand in operands:
                if isinstance(operand, cls):
                    new_operands.extend(operand.operands)  # type: ignore
                else:
                    new_operands.append(operand)
            operands.clear()
            operands.extend(new_operands)

        if cls.one_identity and len(operands) == 1:
            expr = operands[0]
            if not isinstance(expr, Wildcard) or (expr.min_count == 1 and expr.fixed_size):
                return True

        if cls.commutative:
            operands.sort()

        return False


class Operation(Expression, metaclass=_OperationMeta):
    """Base class for all operations.

    Do not instantiate this class directly, but create a subclass for every operation in your domain.
    You can use :meth:`new` as a shortcut for doing so.
    """

    name = None  # type: str
    """str: Name or symbol for the operator.

    This needs to be overridden in the subclass.
    """

    arity = Arity.variadic  # type: Arity
    """Arity: The arity of the operator.

    Trying to construct an operation expression with a number of operands that does not fit its
    operation's arity will result in an error.
    """

    associative = False
    """bool: True if the operation is associative, i.e. `f(a, f(b, c)) = f(f(a, b), c)`.

    This attribute is used to flatten nested associative operations of the same type.
    Therefore, the `arity` of an associative operation has to have an unconstrained maximum
    number of operand.
    """

    commutative = False
    """bool: True if the operation is commutative, i.e. `f(a, b) = f(b, a)`.

    Note that commutative operations will always be converted into canonical
    form with sorted operands.
    """

    one_identity = False
    """bool: True if the operation with a single argument is equivalent to the identity function.

    This property is used to simplify expressions, e.g. for ``f`` with ``f.one_identity = True``
    the expression ``f(a)`` if simplified to ``a``.
    """

    infix = False
    """bool: True if the name of the operation should be used as an infix operator by str()."""

    def __init__(self, operands: List[Expression], variable_name=None) -> None:
        """Create an operation expression.

        Args:
            *operands
                The operands for the operation expression.

        Raises:
            ValueError:
                if the operand count does not match the operation's arity.
            ValueError:
                if the operation contains conflicting variables, i.e. variables with the same name that match
                different things. A common example would be mixing sequence and fixed variables with the same name in
                one expression.
        """
        super().__init__(variable_name)

        operand_count, variable_count = self._count_operands(operands)

        if not variable_count and operand_count < self.arity.min_count:
            raise ValueError(
                "Operation {!s} got arity {!s}, but got {:d} operands.".
                format(type(self).__name__, self.arity, operand_count)
            )

        if self.arity.fixed_size and operand_count > self.arity.min_count:
            msg = "Operation {!s} got arity {!s}, but got {:d} operands.".format(
                type(self).__name__, self.arity, operand_count
            )
            if self.associative:
                msg += " Associative operations should have a variadic/polyadic arity."
            raise ValueError(msg)

        self.operands = operands

    @staticmethod
    def _count_operands(operands):
        operand_count = 0
        variable = False
        for operand in operands:
            if isinstance(operand, Wildcard):
                operand_count += operand.min_count
                if not operand.fixed_size:
                    variable = True
            else:
                operand_count += 1
        return operand_count, variable

    def __str__(self):
        if self.infix:
            separator = ' {!s} '.format(self.name) if self.name else ''
            value = '({!s})'.format(separator.join(str(o) for o in self.operands))
        else:
            value = '{!s}({!s})'.format(self.name, ', '.join(str(o) for o in self.operands))
        if self.variable_name:
            value = '{}: {}'.format(self.variable_name, value)
        return value

    def __repr__(self):
        operand_str = ', '.join(map(repr, self.operands))
        if self.variable_name:
            return '{!s}({!s}, variable_name={})'.format(type(self).__name__, operand_str, self.variable_name)
        return '{!s}({!s})'.format(type(self).__name__, operand_str)

    @staticmethod
    def new(
            name: str,
            arity: Arity,
            class_name: str=None,
            *,
            associative: bool=False,
            commutative: bool=False,
            one_identity: bool=False,
            infix: bool=False
    ) -> Type['Operation']:
        """Utility method to create a new operation type.

        Example:

        >>> Times = Operation.new('*', Arity.polyadic, 'Times', associative=True, commutative=True, one_identity=True)
        >>> Times
        Times['*', Arity(min_count=2, fixed_size=False), associative, commutative, one_identity]
        >>> str(Times(Symbol('a'), Symbol('b')))
        '*(a, b)'

        Args:
            name:
                Name or symbol for the operator. Will be used as name for the new class if
                `class_name` is not specified.
            arity:
                The arity of the operator as explained in the documentation of `Operation`.
            class_name:
                Name for the new operation class to be used instead of name. This argument
                is required if `name` is not a valid python identifier.

        Keyword Args:
            associative:
                See :attr:`~Operation.associative`.
            commutative:
                See :attr:`~Operation.commutative`.
            one_identity:
                See :attr:`~Operation.one_identity`.
            infix:
                See :attr:`~Operation.infix`.

        Raises:
            ValueError: if the class name of the operation is not a valid class identifier.
        """
        class_name = class_name or name
        if not class_name.isidentifier() or keyword.iskeyword(class_name):
            raise ValueError("Invalid identifier for new operator class.")

        return type(
            class_name, (Operation, ), {
                'name': name,
                'arity': arity,
                'associative': associative,
                'commutative': commutative,
                'one_identity': one_identity,
                'infix': infix
            }
        )

    def __lt__(self, other):
        if not isinstance(other, Expression):
            return NotImplemented
        if not isinstance(other, type(self)) and not isinstance(self, type(other)):
            return type(self).__name__ < type(other).__name__
        if self.name != other.name:
            return self.name < other.name
        if len(self.operands) != len(other.operands):
            return len(self.operands) < len(other.operands)
        for left, right in zip(self.operands, other.operands):
            if left < right:
                return True
            elif right < left:
                return False
        return (self.variable_name or '') < (other.variable_name or '')

    def __eq__(self, other):
        if not isinstance(other, type(self)):
            return NotImplemented
        return (
            len(self.operands) == len(other.operands) and all(x == y for x, y in zip(self.operands, other.operands)) and
            self.variable_name == other.variable_name
        )

    def __iter__(self):
        return iter(self.operands)

    def __len__(self):
        return len(self.operands)

    def __getitem__(self, key: Union[Tuple[int, ...], slice]) -> Expression:
        if isinstance(key, int):
            return self.operands[key]
        if isinstance(key, slice):
            if len(key.start) != len(key.stop):
                raise IndexError('Invalid slice: Start and stop must have the same length')
            if len(key.start) == 0:
                return [self]
            if key.start > key.stop:
                raise IndexError('Invalid slice: Start must come before stop')
            if len(key.start) == 1:
                return self.operands[key.start[0]:key.stop[0] + 1]
            start, *new_start = key.start
            stop, *new_stop = key.stop
            if start != stop:
                raise IndexError('Invalid slice: Start and stop must have the same parent')
            return self.operands[start][new_start:new_stop]
        if isinstance(key, (list, tuple)):
            if len(key) == 0:
                return self
            head, *remainder = key
            return self.operands[head][remainder]
        raise TypeError('Invalid key: {}'.format(key))

    __getitem__.__doc__ = Expression.__getitem__.__doc__

    def __contains__(self, expression: 'Expression') -> bool:
        if self == expression:
            return True
        for operand in self.operands:
            if operand == expression:
                return True
            try:
                if expression in operand:
                    return True
            except TypeError:
                pass
        return False

    def _is_constant(self) -> bool:
        return all(x.is_constant for x in self.operands)

    def _is_syntactic(self) -> bool:
        if self.associative or self.commutative:
            return False
        return all(o.is_syntactic for o in self.operands)

    def collect_variables(self, variables) -> None:
        if self.variable_name:
            variables.add(self.variable_name)
        for operand in self.operands:
            operand.collect_variables(variables)

    def collect_symbols(self, symbols) -> None:
        symbols.add(self.name)
        for operand in self.operands:
            operand.collect_symbols(symbols)

    def _preorder_iter(self, predicate: ExprPredicate=None, position: Tuple[int, ...]=()) -> ExpressionsWithPos:
        if predicate is None or predicate(self):
            yield self, position
        for i, operand in enumerate(self.operands):
            yield from operand._preorder_iter(predicate, position + (i, ))  # pylint: disable=protected-access

    def __hash__(self):
        return hash((self.name, ) + tuple(self.operands))

    def with_renamed_vars(self, renaming) -> 'Operation':
        return type(self)(
            *(o.with_renamed_vars(renaming) for o in self.operands),
            variable_name=renaming.get(self.variable_name, self.variable_name)
        )

    def __copy__(self) -> 'Operation':
        return type(self)(*self.operands, variable_name=self.variable_name)


Operation.register(list)
Operation.register(tuple)
Operation.register(set)
Operation.register(frozenset)
Operation.register(dict)


class AssociativeOperation(metaclass=ABCMeta):
    @classmethod
    def __subclasshook__(cls, C):
        if cls is AssociativeOperation:
            if issubclass(C, Operation) and hasattr(C, 'associative'):
                return C.associative
        return NotImplemented


class CommutativeOperation(metaclass=ABCMeta):
    @classmethod
    def __subclasshook__(cls, C):
        if cls is CommutativeOperation:
            if issubclass(C, Operation) and hasattr(C, 'commutative'):
                return C.commutative
        return NotImplemented


CommutativeOperation.register(set)
CommutativeOperation.register(frozenset)
CommutativeOperation.register(dict)


class OneIdentityOperation(metaclass=ABCMeta):
    @classmethod
    def __subclasshook__(cls, C):
        if cls is OneIdentityOperation:
            if issubclass(C, Operation) and hasattr(C, 'one_identity'):
                return C.one_identity
        return NotImplemented


class Atom(Expression):  # pylint: disable=abstract-method
    """Base for all atomic expressions."""

    __iter__ = None


class Symbol(Atom):
    """An atomic constant expression term.

    It is uniquely identified by its name.

    Attributes:
        name (str):
            The symbol's name.
    """

    def __init__(self, name: str, variable_name=None) -> None:
        """
        Args:
            name:
                The name of the symbol that uniquely identifies it.
        """
        super().__init__(variable_name)
        self.name = name
        self.head = self

    def __str__(self):
        if self.variable_name:
            return '{}: {}'.format(self.name, self.variable_name)
        return self.name

    def __repr__(self):
        if self.variable_name:
            return '{!s}({!r}, variable_name={})'.format(type(self).__name__, self.name, self.variable_name)
        return '{!s}({!r})'.format(type(self).__name__, self.name)

    def collect_symbols(self, symbols):
        symbols.add(self.name)

    def with_renamed_vars(self, renaming) -> 'Symbol':
        return type(self)(self.name, variable_name=renaming.get(self.variable_name, self.variable_name))

    def __copy__(self) -> 'Symbol':
        return type(self)(self.name, variable_name=self.variable_name)

    def __lt__(self, other):
        if not isinstance(other, Expression):
            return NotImplemented
        if isinstance(other, Symbol):
            if self.name == other.name:
                return (self.variable_name or '') < (other.variable_name or '')
            return self.name < other.name
        return type(self).__name__ < type(other).__name__

    def __eq__(self, other):
        if not isinstance(other, type(self)):
            return NotImplemented
        return self.name == other.name and self.variable_name == other.variable_name

    def __hash__(self):
        return hash((Symbol, self.name, self.variable_name))


class Wildcard(Atom):
    """A wildcard that matches any expression.

    The wildcard will match any number of expressions between *min_count* and *fixed_size*.
    Optionally, the wildcard can also be constrained to only match expressions satisfying a predicate.

    Attributes:
        min_count (int):
            The minimum number of expressions this wildcard will match.
        fixed_size (bool):
            If ``True``, the wildcard matches exactly *min_count* expressions.
            If ``False``, the wildcard is a sequence wildcard and can match *min_count* or more expressions.
    """

    head = None

    def __init__(self, min_count: int, fixed_size: bool, variable_name=None, optional=None) -> None:
        """
        Args:
            min_count:
                The minimum number of expressions this wildcard will match. Must be a non-negative number.
            fixed_size:
                If ``True``, the wildcard matches exactly *min_count* expressions.
                If ``False``, the wildcard is a sequence wildcard and can match *min_count* or more expressions.

        Raises:
            ValueError: if *min_count* is negative or when trying to create a fixed zero-length wildcard.
        """
        if min_count < 0:
            raise ValueError("min_count cannot be negative")
        if min_count == 0 and fixed_size:
            raise ValueError("Cannot create a fixed zero length wildcard")

        super().__init__(variable_name)
        self.min_count = min_count
        self.fixed_size = fixed_size
        self.optional = optional

    def _is_constant(self) -> bool:
        return False

    def _is_syntactic(self) -> bool:
        return self.fixed_size

    def with_renamed_vars(self, renaming) -> 'Wildcard':
        return type(self)(
            self.min_count, self.fixed_size, variable_name=renaming.get(self.variable_name, self.variable_name)
        )

    @staticmethod
    def dot(name=None) -> 'Wildcard':
        """Create a `Wildcard` that matches a single argument.

        Args:
            name: An optional name for the wildcard.

        Returns:
            A dot wildcard.
        """
        return Wildcard(min_count=1, fixed_size=True, variable_name=name)

    @staticmethod
    def optional(name, default) -> 'Wildcard':
        """Create a `Wildcard` that matches a single argument with a default value.

        If the wildcard does not match, the substitution will contain the
        default value instead.

        Args:
            name:
                The name for the wildcard.
            default:
                The default value of the wildcard.

        Returns:
            A n optional wildcard.
        """
        return Wildcard(min_count=1, fixed_size=True, variable_name=name, optional=default)

    @staticmethod
    def symbol(name: str=None, symbol_type: Type[Symbol]=Symbol) -> 'SymbolWildcard':
        """Create a `SymbolWildcard` that matches a single `Symbol` argument.

        Args:
            name:
                Optional variable name for the wildcard.
            symbol_type:
                An optional subclass of `Symbol` to further limit which kind of symbols are
                matched by the wildcard.

        Returns:
            A `SymbolWildcard` that matches the *symbol_type*.
        """
        if isinstance(name, type) and issubclass(name, Symbol) and symbol_type is Symbol:
            return SymbolWildcard(name)
        return SymbolWildcard(symbol_type, variable_name=name)

    @staticmethod
    def star(name=None) -> 'Wildcard':
        """Creates a `Wildcard` that matches any number of arguments.

        Args:
            name:
                Optional variable name for the wildcard.

        Returns:
            A star wildcard.
        """
        return Wildcard(min_count=0, fixed_size=False, variable_name=name)

    @staticmethod
    def plus(name=None) -> 'Wildcard':
        """Creates a `Wildcard` that matches at least one and up to any number of arguments

        Args:
            name:
                Optional variable name for the wildcard.

        Returns:
            A plus wildcard.
        """
        return Wildcard(min_count=1, fixed_size=False, variable_name=name)

    def __str__(self):
        value = None
        if not self.fixed_size:
            if self.min_count == 0:
                value = '___'
            elif self.min_count == 1:
                value = '__'
        elif self.min_count == 1:
            value = '_'
        if value is None:
            value = '_[{:d}{!s}]'.format(self.min_count, '' if self.fixed_size else '+')
        if self.variable_name:
            value = '{}{}'.format(self.variable_name, value)
        if self.optional is not None:
            value += ': {}'.format(self.optional)
        return value

    def __repr__(self):
        if self.variable_name:
            if self.optional is not None:
                return '{!s}({!r}, {!r}, variable_name={}, optional={})'.format(
                    type(self).__name__, self.min_count, self.fixed_size, self.variable_name, self.optional
                )
            return '{!s}({!r}, {!r}, variable_name={})'.format(
                type(self).__name__, self.min_count, self.fixed_size, self.variable_name
            )
        return '{!s}({!r}, {!r})'.format(type(self).__name__, self.min_count, self.fixed_size)

    def __lt__(self, other):
        if not isinstance(other, Expression):
            return NotImplemented
        if not isinstance(other, Wildcard):
            return type(self).__name__ < type(other).__name__
        if self.min_count != other.min_count or self.fixed_size != other.fixed_size:
            return self.min_count < other.min_count or (self.fixed_size and not other.fixed_size)
        if self.variable_name != other.variable_name:
            return (self.variable_name or '') < (other.variable_name or '')
        if not isinstance(self, SymbolWildcard):
            return isinstance(other, SymbolWildcard)
        if isinstance(other, SymbolWildcard):
            return self.symbol_type.__name__ < other.symbol_type.__name__
        return False

    def __eq__(self, other):
        if not isinstance(other, type(self)):
            return NotImplemented
        return (
            other.min_count == self.min_count and other.fixed_size == self.fixed_size and
            self.variable_name == other.variable_name and
            self.optional == other.optional
        )

    def __hash__(self):
        return hash((Wildcard, self.min_count, self.fixed_size, self.variable_name))

    def __copy__(self) -> 'Wildcard':
        return type(self)(self.min_count, self.fixed_size, variable_name=self.variable_name, optional=self.optional)


class SymbolWildcard(Wildcard):
    """A special `Wildcard` that matches a `Symbol`.

    Attributes:
        symbol_type:
            A subclass of `Symbol` to constrain what the wildcard matches.
            If not specified, the wildcard will match any `Symbol`.
    """

    def __init__(self, symbol_type: Type[Symbol]=Symbol, variable_name=None) -> None:
        """
        Args:
            symbol_type:
                A subclass of `Symbol` to constrain what the wildcard matches.
                If not specified, the wildcard will match any `Symbol`.

        Raises:
            TypeError: if *symbol_type* is not a subclass of `Symbol`.
        """
        super().__init__(1, True, variable_name)

        if not issubclass(symbol_type, Symbol):
            raise TypeError("The type constraint must be a subclass of Symbol")

        self.symbol_type = symbol_type

    def with_renamed_vars(self, renaming) -> 'SymbolWildcard':
        return type(self)(self.symbol_type, variable_name=renaming.get(self.variable_name, self.variable_name))

    def __eq__(self, other):
        return (
            isinstance(other, type(self)) and self.symbol_type == other.symbol_type and
            self.variable_name == other.variable_name
        )

    def __hash__(self):
        return hash((SymbolWildcard, self.symbol_type, self.variable_name))

    def __repr__(self):
        if self.variable_name:
            return '{!s}({!r}, variable_name={})'.format(type(self).__name__, self.symbol_type, self.variable_name)
        return '{!s}({!r})'.format(type(self).__name__, self.symbol_type)

    def __str__(self):
        if self.variable_name:
            return '{}_[{!s}]'.format(self.variable_name, self.symbol_type.__name__)
        return '_[{!s}]'.format(self.symbol_type.__name__)

    def __copy__(self) -> 'SymbolWildcard':
        return type(self)(self.symbol_type, self.variable_name)


class Pattern:
    """A pattern is a term that can be matched against another subject term.

    A pattern can contain variables and can optionally have constraints attached to it.
    Those constraints a predicates which limit what the pattern can match.
    """

    def __init__(self, expression, *constraints) -> None:
        """
        Args:
            expression:
                The term that forms the pattern.
            *constraints:
                Optional constraints for the pattern.
        """
        self.expression = expression
        self.constraints = constraints

    def __str__(self):
        if not self.constraints:
            return str(self.expression)
        return '{} /; {}'.format(self.expression, ' and '.join(map(str, self.constraints)))

    def __repr__(self):
        if not self.constraints:
            return '{}({})'.format(type(self).__name__, self.expression)
        return '{}({}, constraints={})'.format(type(self).__name__, self.expression, self.constraints)

    def __eq__(self, other):
        if not isinstance(other, Pattern):
            return NotImplemented
        return self.expression == other.expression and self.constraints == other.constraints

    @property
    def is_syntactic(self):
        """True, iff the pattern is :term:`syntactic`."""
        return self.expression.is_syntactic

    @property
    def local_constraints(self):
        """The subset of the pattern constraints which are local.

        A local constraint has a defined non-empty set of dependency variables.
        These constraints can be evaluated once their dependency variables have a substitution.
        """
        return [c for c in self.constraints if c.variables]

    @property
    def global_constraints(self):
        """The subset of the pattern constraints which are global.

        A global constraint does not define dependency variables and can only be evaluated, once the
        match has been completed.
        """
        return [c for c in self.constraints if not c.variables]


def make_dot_variable(name):
    """Create a new variable with the given name that matches a single term.

    Args:
        name:
            The name of the variable

    Returns:
        The new dot variable.
    """
    return Wildcard.dot(name)


def make_symbol_variable(name, symbol_type=Symbol):
    """Create a new variable with the given name that matches a single symbol.

    Optionally, a symbol type can be specified to further limit what the variable can match.

    Args:
        name:
            The name of the variable
        symbol_type:
            The symbol type must be a subclass of `Symbol`. Defaults to `Symbol` itself.

    Returns:
        The new symbol variable.
    """
    return Wildcard.symbol(name, symbol_type)


def make_star_variable(name):
    """Create a new variable with the given name that matches any number of terms.

    Can also match an empty argument sequence.

    Args:
        name:
            The name of the variable

    Returns:
        The new star variable.
    """
    return Wildcard.star(name)


def make_plus_variable(name):
    """Create a new variable with the given name that matches any number of terms.

    Only matches sequences with at least one argument.

    Args:
        name:
            The name of the variable

    Returns:
        The new plus variable.
    """
    return Wildcard.plus(name)