"""Generic set theory interfaces."""

import itertools

from mpmath import mpf, mpi

from ..core import Basic, Eq, Expr, Mul, S, nan, oo, sympify, zoo
from ..core.compatibility import iterable, ordered
from ..core.decorators import _sympifyit
from ..core.evalf import EvalfMixin
from ..core.evaluate import global_evaluate
from ..core.singleton import Singleton
from ..logic import And, Not, Or, false, true
from ..utilities import subsets
from .contains import Contains


class Set(Basic):
    """
    The base class for any kind of set.

    This is not meant to be used directly as a container of items. It does not
    behave like the builtin ``set``; see :class:`FiniteSet` for that.

    Real intervals are represented by the :class:`Interval` class and unions of
    sets by the :class:`Union` class.
 Th
e empty set is represented by the
    :class:`EmptySet` class and available as a singleton as ``S.EmptySet``.

    """

    is_number = False
    is_iterable = False
    is_interval = False

    is_FiniteSet = False
    is_Interval = False
    is_ProductSet = False
    is_Union = False
    is_Intersection = None
    is_EmptySet = None
    is_UniversalSet = None
    is_Complement = None
    is_SymmetricDifference = None

    @staticmethod
    def _infimum_key(expr):
        """Return infimum (if possible) else oo."""
        try:
            infimum = expr.inf
            assert infimum.is_comparable
        except (NotImplementedError,
                AttributeError, AssertionError, ValueError):
            infimum = oo
        return infimum

    def union(self, other):
        """
        Returns the union of 'self' and 'other'.

        Examples
        ========

        As a shortcut it is possible to use the '+' operator:

        >>> Interval(0, 1).union(Interval(2, 3))
        [0, 1] U [2, 3]
        >>> Interval(0, 1) + Interval(2, 3)
        [0, 1] U [2, 3]
        >>> Interval(1, 2, True, True) + FiniteSet(2, 3)
        (1, 2] U {3}

        Similarly it is possible to use the '-' operator for set differences:

        >>> Interval(0, 2) - Interval(0, 1)
        (1, 2]
        >>> Interval(1, 3) - FiniteSet(2)
        [1, 2) U (2, 3]

        """
        return Union(self, other)

    def intersection(self, other):
        """
        Returns the intersection of 'self' and 'other'.


        >>> Interval(1, 3).intersection(Interval(1, 2))
        [1, 2]

        """
        return Intersection(self, other)

    def _intersection(self, other):
        """
        This function should only be used internally

        self._intersection(other) returns a new, intersected set if self knows how
        to intersect itself with other, otherwise it returns ``None``

        When making a new set class you can be assured that other will not
        be a :class:`Union`, :class:`FiniteSet`, or :class:`EmptySet`

        Used within the :class:`Intersection` class

        """
        return

    def is_disjoint(self, other):
        """
        Returns True if 'self' and 'other' are disjoint

        Examples
        ========

        >>> Interval(0, 2).is_disjoint(Interval(1, 2))
        False
        >>> Interval(0, 2).is_disjoint(Interval(3, 4))
        True

        References
        ==========

        * https://en.wikipedia.org/wiki/Disjoint_sets

        """
        return self.intersection(other) == S.EmptySet

    def isdisjoint(self, other):
        """Alias for :meth:`is_disjoint()`."""
        return self.is_disjoint(other)

    def _union(self, other):
        """
        This function should only be used internally

        self._union(other) returns a new, joined set if self knows how
        to join itself with other, otherwise it returns ``None``.
        It may also return a python set of Diofant Sets if they are somehow
        simpler. If it does this it must be idempotent i.e. the sets returned
        must return ``None`` with _union'ed with each other

        Used within the :class:`Union` class

        """
        return

    def complement(self, universe):
        r"""
        The complement of 'self' w.r.t the given the universe.

        Examples
        ========

        >>> Interval(0, 1).complement(S.Reals)
        (-oo, 0) U (1, oo)

        >>> Interval(0, 1).complement(S.UniversalSet)
        UniversalSet() \ [0, 1]

        """
        return Complement(universe, self)

    def _complement(self, other):
        # this behaves as other - self
        if other.is_subset(self):
            return S.EmptySet
        elif isinstance(other, ProductSet):
            # For each set consider it or it's complement
            # We need at least one of the sets to be complemented
            # Consider all 2^n combinations.
            # We can conveniently represent these options easily using a
            # ProductSet

            # XXX: this doesn't work if the dimentions of the sets isn't same.
            # A - B is essentially same as A if B has a different
            # dimensionality than A
            switch_sets = ProductSet(FiniteSet(o, o - s) for s, o in
                                     zip(self.sets, other.sets))
            product_sets = (ProductSet(*set) for set in switch_sets)
            # Union of all combinations but this one
            return Union(p for p in product_sets if p != other)

        elif isinstance(other, Interval):
            if isinstance(self, Interval) or isinstance(self, FiniteSet):
                return Intersection(other, self.complement(S.Reals))

        elif isinstance(other, Union):
            return Union(o - self for o in other.args)

        elif isinstance(other, Complement):
            return Complement(other.args[0], Union(other.args[1], self), evaluate=False)

        elif isinstance(other, FiniteSet):
            unks = FiniteSet(*[el for el in other
                               if self.contains(el) not in [true, false]])
            other = FiniteSet(*[el for el in other
                                if self.contains(el) != true])
            ret = FiniteSet(*[el for el in other if self.contains(el) == false])
            if unks:
                ret |= Complement(FiniteSet(*unks), self, evaluate=False)
            return ret

    def symmetric_difference(self, other):
        """
        Returns symmetric difference of ``self`` and ``other``.

        Examples
        ========

        >>> Interval(1, 3).symmetric_difference(Reals)
        (-oo, 1) U (3, oo)

        References
        ==========

        * https://en.wikipedia.org/wiki/Symmetric_difference

        """
        return SymmetricDifference(self, other)

    def _symmetric_difference(self, other):
        return Union(Complement(self, other), Complement(other, self))

    @property
    def inf(self):
        """
        The infimum of 'self'

        Examples
        ========

        >>> Interval(0, 1).inf
        0
        >>> Union(Interval(0, 1), Interval(2, 3)).inf
        0

        """
        return self._inf

    @property
    def _inf(self):
        raise NotImplementedError(f'({self})._inf')

    @property
    def sup(self):
        """
        The supremum of 'self'

        Examples
        ========

        >>> Interval(0, 1).sup
        1
        >>> Union(Interval(0, 1), Interval(2, 3)).sup
        3

        """
        return self._sup

    @property
    def _sup(self):
        raise NotImplementedError(f'({self})._sup')

    @_sympifyit('other', false)
    def contains(self, other):
        """
        Returns True if 'other' is contained in 'self' as an element.

        As a shortcut it is possible to use the 'in' operator:

        Examples
        ========

        >>> Interval(0, 1).contains(0.5)
        true
        >>> 0.5 in Interval(0, 1)
        True

        """
        ret = self._contains(other)
        if ret is None:
            ret = Contains(other, self, evaluate=False)
        return ret

    def _contains(self, other):
        raise NotImplementedError(f'({self})._contains({other})')

    def is_subset(self, other):
        """
        Returns True if 'self' is a subset of 'other'.

        Examples
        ========

        >>> Interval(