"""Implementation of :class:`ComplexField` class. """


from sympy.external.gmpy import SYMPY_INTS
from sympy.core.numbers import Float, I
from sympy.polys.domains.characteristiczero import CharacteristicZero
from sympy.polys.domains.field import Field
from sympy.polys.domains.gaussiandomains import QQ_I
from sympy.polys.domains.simpledomain import SimpleDomain
from sympy.polys.polyerrors import DomainError, CoercionFailed
from sympy.utilities import public

from mpmath import MPContext


@public
class ComplexField(Field, CharacteristicZero, SimpleDomain):
    """Complex numbers up to the given precision. """

    rep = 'CC'

    is_ComplexField = is_CC = True

    is_Exact = False
    is_Numerical = True

    has_assoc_Ring = False
    has_assoc_Field = True

    _default_precision = 53

    @property
    def has_default_precision(self):
        return self.precision == self._default_precision

    @property
    def precision(self):
        return self._context.prec

    @property
    def dps(self):
        return self._context.dps

    @property
    def tolerance(self):
        return self._tolerance

    def __init__(self, prec=None, dps=None, tol=None):
        # XXX: The tolerance parameter is ignored but is kept for backward
        # compatibility for now.

        context = MPContext()

        if prec is None and dps is None:
            context.prec = self._default_precision
        elif dps is None:
            context.prec = prec
        elif prec is None:
            context.dps = dps
        else:
            raise TypeError("Cannot set both prec and dps")

        self._context = context

        self._dtype = context.mpc
        self.zero = self.dtype(0)
        self.one = self.dtype(1)

        # XXX: Neither of these is actually used anywhere.
        self._max_denom = max(2**context.prec // 200, 99)
        self._tolerance = self.one / self._max_denom

    @property
    def tp(self):
        # XXX: Domain treats tp as an alias of dtype. Here we need two separate
        # things: dtype is a callable to make/convert instances. We use tp with
        # isinstance to check if an object is an instance of the domain
        # already.
        return self._dtype

    def dtype(self, x, y=0):
        # XXX: This is needed because mpmath does not recognise fmpz.
        # It might be better to add conversion routines to mpmath and if that
        # happens then this can be removed.
        if isinstance(x, SYMPY_INTS):
            x = int(x)
        if isinstance(y, SYMPY_INTS):
            y = int(y)
        return self._dtype(x, y)

    def __eq__(self, other):
        return isinstance(other, ComplexField) and self.precision == other.precision

    def __hash__(self):
        return hash((self.__class__.__name__, self._dtype, self.precision))

    def to_sympy(self, element):
        """Convert ``element`` to SymPy number. """
        return Float(element.real, self.dps) + I*Float(element.imag, self.dps)

    def from_sympy(self, expr):
        """Convert SymPy's number to ``dtype``. """
        number = expr.evalf(n=self.dps)
        real, imag = number.as_real_imag()

        if real.is_Number and imag.is_Number:
            return self.dtype(real, imag)
        else:
            raise CoercionFailed("expected complex number, got %s" % expr)

    def from_ZZ(self, element, base):
        return self.dtype(element)

    def from_ZZ_gmpy(self, element, base):
        return self.dtype(int(element))

    def from_ZZ_python(self, element, base):
        return self.dtype(element)

    def from_QQ(self, element, base):
        return self.dtype(int(element.numerator)) / int(element.denominator)

    def from_QQ_python(self, element, base):
        return self.dtype(element.numerator) / element.denominator

    def from_QQ_gmpy(self, element, base):
        return self.dtype(int(element.numerator)) / int(element.denominator)

    def from_GaussianIntegerRing(self, element, base):
        return self.dtype(int(element.x), int(element.y))

    def from_GaussianRationalField(self, element, base):
        x = element.x
        y = element.y
        return (self.dtype(int(x.numerator)) / int(x.denominator) +
                self.dtype(0, int(y.numerator)) / int(y.denominator))

    def from_AlgebraicField(self, element, base):
        return self.from_sympy(base.to_sympy(element).evalf(self.dps))

    def from_RealField(self, element, base):
        return self.dtype(element)

    def from_ComplexField(self, element, base):
        return self.dtype(element)

    def get_ring(self):
        """Returns a ring associated with ``self``. """
        raise DomainError("there is no ring associated with %s" % self)

    def get_exact(self):
        """Returns an exact domain associated with ``self``. """
        return QQ_I

    def is_negative(self, element):
        """Returns ``False`` for any ``ComplexElement``. """
        return False

    def is_positive(self, element):
        """Returns ``False`` for any ``ComplexElement``. """
        return False

    def is_nonnegative(self, element):
        """Returns ``False`` for any ``ComplexElement``. """
        return False

    def is_nonpositive(self, element):
        """Returns ``False`` for any ``ComplexElement``. """
        return False

    def gcd(self, a, b):
        """Returns GCD of ``a`` and ``b``. """
        return self.one

    def lcm(self, a, b):
        """Returns LCM of ``a`` and ``b``. """
        return a*b

    def almosteq(self, a, b, tolerance=None):
        """Check if ``a`` and ``b`` are almost equal. """
        return self._context.almosteq(a, b, tolerance)

    def is_square(self, a):
        """Returns ``True``. Every complex number has a complex square root."""
        return True

    def exsqrt(self, a):
        r"""Returns the principal complex square root of ``a``.

        Explanation
        ===========
        The argument of the principal square root is always within
        $(-\frac{\pi}{2}, \frac{\pi}{2}]$. The square root may be
        slightly inaccurate due to floating point rounding error.
        """
        return a ** 0.5

CC = ComplexField()
