"""Base class for all the objects in Diofant."""

from collections import defaultdict
from collections.abc import Mapping
from itertools import zip_longest

from ..utilities import ordered
from .cache import cacheit
from .compatibility import iterable
from .decorators import _sympifyit
from .evaluate import evaluate
from .sympify import SympifyError, sympify


class Basic:
    """
    Base class for all objects in Diofant.

    Always use ``args`` property, when accessing parameters of some instance.

    """

    # To be overridden with True in the appropriate subclasses
    is_number = False
    is_Atom: bool = False
    is_Symbol = False
    is_Dummy = False
    is_Wild = False
    is_Function = False
    is_Add = False
    is_Mul = False
    is_Pow = False
    is_Number = False
    is_Float = False
    is_Rational = False
    is_Integer = False
    is_NumberSymbol = False
    is_Order = False
    is_Derivative = False
    is_Piecewise = False
    is_Poly = False
    is_Relational = False
    is_Equality = False
    is_Boolean = False
    is_Not = False
    is_Matrix: bool = False
    is_MatMul = False
    is_Vector = False

    def __new__(cls, *args):
        obj = object.__new__(cls)
        obj._hash = None  # will be set by __hash__ method.

        obj._args = args  # all items in args must be Basic objects
        return obj

    def copy(self):
        """Return swallow copy of self."""
        return self.func(*self.args)

    def __reduce_ex__(self, proto):
        """Pickling support."""
        return type(self), self.__getnewargs__(), self.__getstate__()

    def __getnewargs__(self):
        return self.args

    def __getstate__(self):
        return {'_hash': None}

    def __setstate__(self, state):
        for k, v in state.items():
            setattr(self, k, v)

    def __hash__(self):
        # hash cannot be cached using cache_it because infinite recurrence
        # occurs as hash is needed for setting cache dictionary keys
        h = self._hash
        if h is None:
            h = hash((type(self).__name__,) + self._hashable_content())
            self._hash = h
        return h

    def _hashable_content(self):
        """Return a tuple of information about self that can be used to
        compute the hash. If a class defines additional attributes,
        like ``name`` in Symbol, then this method should be updated
        accordingly to return such relevant attributes.

        Defining more than _hashable_content is necessary if __eq__ has
        been defined by a class. See note about this in Basic.__eq__.

        """
        return self._args

    @classmethod
    def class_key(cls):
        """Nice order of classes."""
        return 5, 0, cls.__name__

    @cacheit
    def sort_key(self, order=None):
        """Return a sort key.

        Examples
        ========

        >>> sorted([Rational(1, 2), I, -I], key=lambda x: x.sort_key())
        [1/2, -I, I]

        >>> [x, 1/x, 1/x**2, x**2, sqrt(x), root(x, 4), x**Rational(3, 2)]
        [x, 1/x, x**(-2), x**2, sqrt(x), x**(1/4), x**(3/2)]
        >>> sorted(_, key=lambda x: x.sort_key())
        [x**(-2), 1/x, x**(1/4), sqrt(x), x, x**(3/2), x**2]

        """
        from .numbers import Integer
        args = len(self.args), tuple(arg.sort_key(order)
                                     for arg in self._sorted_args)
        return self.class_key(), args, Integer(1).sort_key(), Integer(1)

    @_sympifyit('other', NotImplemented)
    def __eq__(self, other):
        """Return a boolean indicating whether a == b on the basis of
        their symbolic trees.

        Notes
        =====

        See [1]_.  If a class that overrides __eq__() needs to retain the
        implementation of __hash__() from a parent class, the
        interpreter must be told this explicitly by setting __hash__ =
        <ParentClass>.__hash__. Otherwise the inheritance of __hash__()
        will be blocked, just as if __hash__ had been explicitly set to
        None.

        References
        ==========

        * http://docs.python.org/dev/reference/datamodel.html#object.__hash__

        """
        if self is other:
            return True

        if type(self) != type(other):
            return False

        return self._hashable_content() == other._hashable_content()

    # Note, we always use the default ordering (lex) in __str__ and __repr__,
    # regardless of the global setting.  See issue sympy/sympy#5487.
    def __repr__(self):
        from ..printing import srepr
        return srepr(self, order=None)

    def __str__(self):
        from ..printing import sstr
        return sstr(self, order=None)

    def _repr_pretty_(self, p, cycle):
        from ..printing import pretty
        p.text(pretty(self))

    def _repr_latex_(self):
        from ..printing import latex
        return latex(self, mode='equation*')

    def atoms(self, *types):
        """Returns the atoms that form the current object.

        By default, only objects that are truly atomic and can't
        be divided into smaller pieces are returned: symbols, numbers,
        and number symbols like I and pi. It is possible to request
        atoms of any type, however, as demonstrated below.

        Examples
        ========

        >>> e = 1 + x + 2*sin(y + I*pi)
        >>> e.atoms()
        {1, 2, I, pi, x, y}

        If one or more types are given, the results will contain only
        those types of atoms.

        >>> e.atoms(Symbol)
        {x, y}

        >>> e.atoms(Number)
        {1, 2}

        >>> e.atoms(Number, NumberSymbol)
        {1, 2, pi}

        >>> e.atoms(Number, NumberSymbol, I)
        {1, 2, I, pi}

        Note that I (imaginary unit) and zoo (complex infinity) are special
        types of number symbols and are not part of the NumberSymbol class.

        The type can be given implicitly, too:

        >>> e.atoms(x)
        {x, y}

        Be careful to check your assumptions when using the implicit option
        since ``Integer(1).is_Integer = True`` but ``type(Integer(1))`` is
        ``One``, a special type of diofant atom, while ``type(Integer(2))``
        is type ``Integer`` and will find all integers in an expression:

        >>> e.atoms(Integer(1))
        {1}

        >>> e.atoms(Integer(2))
        {1, 2}

        Finally, arguments to atoms() can select more than atomic atoms: any
        diofant type can be listed as an argument and those types of "atoms"
        as found in scanning the arguments of the expression recursively:

        >>> from diofant.core.function import AppliedUndef

        >>> (1 + x + 2*sin(y + I*pi)).atoms(Mul)
        {I*pi, 2*sin(y + I*pi)}

        >>> f = Function('f')
        >>> e = 1 + f(x) + 2*sin(y + I*pi)
        >>> e.atoms(Function)
        {f(x), sin(y + I*pi)}
        >>> (1 + f(x) + 2*sin(y + I*pi)).atoms(AppliedUndef)
        {f(x)}

        """
        if types:
            types = tuple(t if isinstance(t, type) else type(t) for t in types)
        else:
            types = Atom,
        return set().union(*[self.find(t) for t in types])

    @property
    def free_symbols(self):
        """Return from the atoms of self those which are free symbols.

        For most expressions, all symbols are free symbols. For some classes
        this is not true. e.g. Integrals use Symbols for the dummy variables
        which are bound variables, so Integral has a method to return all
        symbols except those. Derivative keeps track of symbols with respect
        to which it will perform a derivative; those are
        bound variables, too, so it has its own free_symbols method.

        Any other method that uses bound variables should implement a
        free_symbols method.

        """
        return set().union(*[a.free_symbols for a in self.args])

    def rcall(self, *args):
        """Apply on the argument recursively through the expression tree.

        This method is used to simulate a common abuse of notation for
        operators. For instance in Diofant the the following will not work:

        ``(x+Lambda(y, 2*y))(z) == x+2*z``,

        however you can use

        >>> (x + Lambda(y, 2*y)).rcall(z)
        x + 2*z

        """
        if callable(self) and hasattr(self, '__call__'):
            return self(*args)
        elif self.args:
            newargs = [sub.rcall(*args) for sub in self.args]
            return type(self)(*newargs)
        else:
            return self

    @property
    def func(self):
        """The top-level function in an expression.

        The following should hold for all objects::

            x == x.func(*x.args)

        Examples
        ========

        >>> a = 2*x
        >>> a.func
        <class 'diofant.core.mul.Mul'>
        >>> a.args
        (2, x)
        >>> a.func(*a.args)
        2*x
        >>> a == a.func(*a.args)
        True

        """
        return self.__class__

    @property
    def args(self):
        """Returns a tuple of arguments of 'self'.

        Examples
        ========

        >>> cot(x).args
        (x,)
        >>> (x*y).args
        (x, y)

        """
        return self._args

    @property
    def is_evaluated(self):
        """Test if an expession is evaluated."""
        with evaluate(True):
            expr = self.func(*self.args)
        return expr == self

    @property
    def _sorted_args(self):
        """
        The same as ``args``.  Derived classes which don't fix an
        order on their arguments should override this method to
        produce the sorted representation.

        """
        return self.args

    def subs(self, *args, **kwargs):
        """
        Substitutes old for new in an expression after sympifying args.

        `args` is either:
          - one iterable argument, e.g. foo.subs(iterable). The iterable may be
             o an iterable container with (old, new) pairs. In this case the
               replacements are processed in the order given with successive
               patterns possibly affecting replacements already made.
             o a dict or set whose key/value items correspond to old/new pairs.
               In this case the old/new pairs will be sorted by op count and in
               case of a tie, by number of args and the default_sort_key. The
               resulting sorted list is then processed as an iterable container
               (see previous).

        If the keyword ``simultaneous`` is True, the subexpressions will not be
        evaluated until all the substitutions have been made.

        Examples
        ========

        >>> (1 + x*y).subs({x: pi})
        pi*y + 1
        >>> (1 + x*y).subs({x: pi, y: 2})
        1 + 2*pi
        >>> (1 + x*y).subs([(x, pi), (y, 2)])
        1 + 2*pi
        >>> reps = [(y, x**2), (x, 2)]
        >>> (x + y).subs(reps)
        6
        >>> (x + y).subs(reversed(reps))
        x**2 + 2

        >>> (x**2 + x**4).subs({x**2: y})
        y**2 + y

        To replace only the x**2 but not the x**4, use xreplace:

        >>> (x**2 + x**4).xreplace({x**2: y})
        x**4 + y

        To delay evaluation until all substitutions have been made,
        set the keyword ``simultaneous`` to True:

        >>> (x/y).subs([(x, 0), (y, 0)])
        0
        >>> (x/y).subs([(x, 0), (y, 0)], simultaneous=True)
        nan

        This has the added feature of not allowing subsequent substitutions
        to affect those already made:

        >>> ((x + y)/y).subs({x + y: y, y: x + y})
        1
        >>> ((x + y)/y).subs({x + y: y, y: x + y}, simultaneous=True)
        y/(x + y)

        In order to obtain a canonical result, unordered iterables are
        sorted by count_op length, number of arguments and by the
        default_sort_key to break any ties. All other iterables are left
        unsorted.

        >>> from diofant.abc import e

        >>> expr = sqrt(sin(2*x))*sin(exp(x)*x)*cos(2*x) + sin(2*x)

        >>> expr.subs({sqrt(sin(2*x)): a, sin(2*x): b,
        ...            cos(2*x): c, x: d, exp(x): e})
        a*c*sin(d*e) + b

        The resulting expression represents a literal replacement of the
        old arguments with the new arguments. This may not reflect the
        limiting behavior of the expression:

        >>> (x**3 - 3*x).subs({x: oo})
        nan

        >>> limit(x**3 - 3*x, x, oo)
        oo

        If the substitution will be followed by numerical
        evaluation, it is better to pass the substitution to
        evalf as

        >>> (1/x).evalf(21, subs={x: 3.0}, strict=False)
        0.333333333333333333333

        rather than

        >>> (1/x).subs({x: 3.0}).evalf(21, strict=False)
        0.333333333333333

        as the former will ensure that the desired level of precision is
        obtained.

        See Also
        ========

        replace: replacement capable of doing wildcard-like matching,
                 parsing of match, and conditional replacements
        xreplace: exact node replacement in expr tree; also capable of
                  using matching rules
        diofant.core.evalf.EvalfMixin.evalf: calculates the given formula to
                                           a desired level of precision

        """
        from ..utilities import default_sort_key
        from .numbers import Integer
        from .symbol import Dummy

        unordered = False