"""OO layer for several polynomial representations. """

from sympy import oo
from sympy.core.sympify import CantSympify
from sympy.polys.polyerrors import CoercionFailed, NotReversible, NotInvertible
from sympy.polys.polyutils import PicklableWithSlots

class GenericPoly(PicklableWithSlots):
    """Base class for low-level polynomial representations. """

    def ground_to_ring(f):
        """Make the ground domain a ring. """
        return f.set_domain(f.dom.get_ring())

    def ground_to_field(f):
        """Make the ground domain a field. """
        return f.set_domain(f.dom.get_field())

    def ground_to_exact(f):
        """Make the ground domain exact. """
        return f.set_domain(f.dom.get_exact())

    @classmethod
    def _perify_factors(per, result, include):
        if include:
            coeff, factors = result
        else:
            coeff = result

        factors = [ (per(g), k) for g, k in factors ]

        if include:
            return coeff, factors
        else:
            return factors

from sympy.polys.densebasic import (
    dmp_validate,
    dup_normal, dmp_normal,
    dup_convert, dmp_convert,
    dmp_from_sympy,
    dup_strip,
    dup_degree, dmp_degree_in,
    dmp_degree_list,
    dmp_negative_p,
    dup_LC, dmp_ground_LC,
    dup_TC, dmp_ground_TC,
    dmp_ground_nth,
    dmp_one, dmp_ground,
    dmp_zero_p, dmp_one_p, dmp_ground_p,
    dup_from_dict, dmp_from_dict,
    dmp_to_dict,
    dmp_deflate,
    dmp_inject, dmp_eject,
    dmp_terms_gcd,
    dmp_list_terms, dmp_exclude,
    dmp_slice_in, dmp_permute,
    dmp_to_tuple,)

from sympy.polys.densearith import (
    dmp_add_ground,
    dmp_sub_ground,
    dmp_mul_ground,
    dmp_quo_ground,
    dmp_exquo_ground,
    dmp_abs,
    dup_neg, dmp_neg,
    dup_add, dmp_add,
    dup_sub, dmp_sub,
    dup_mul, dmp_mul,
    dmp_sqr,
    dup_pow, dmp_pow,
    dmp_pdiv,
    dmp_prem,
    dmp_pquo,
    dmp_pexquo,
    dmp_div,
    dup_rem, dmp_rem,
    dmp_quo,
    dmp_exquo,
    dmp_add_mul, dmp_sub_mul,
    dmp_max_norm,
    dmp_l1_norm)

from sympy.polys.densetools import (
    dmp_clear_denoms,
    dmp_integrate_in,
    dmp_diff_in,
    dmp_eval_in,
    dup_revert,
    dmp_ground_trunc,
    dmp_ground_content,
    dmp_ground_primitive,
    dmp_ground_monic,
    dmp_compose,
    dup_decompose,
    dup_shift,
    dup_transform,
    dmp_lift)

from sympy.polys.euclidtools import (
    dup_half_gcdex, dup_gcdex, dup_invert,
    dmp_subresultants,
    dmp_resultant,
    dmp_discriminant,
    dmp_inner_gcd,
    dmp_gcd,
    dmp_lcm,
    dmp_cancel)

from sympy.polys.sqfreetools import (
    dup_gff_list,
    dmp_norm,
    dmp_sqf_p,
    dmp_sqf_norm,
    dmp_sqf_part,
    dmp_sqf_list, dmp_sqf_list_include)

from sympy.polys.factortools import (
    dup_cyclotomic_p, dmp_irreducible_p,
    dmp_factor_list, dmp_factor_list_include)

from sympy.polys.rootisolation import (
    dup_isolate_real_roots_sqf,
    dup_isolate_real_roots,
    dup_isolate_all_roots_sqf,
    dup_isolate_all_roots,
    dup_refine_real_root,
    dup_count_real_roots,
    dup_count_complex_roots,
    dup_sturm)

from sympy.polys.polyerrors import (
    UnificationFailed,
    PolynomialError)

def init_normal_DMP(rep, lev, dom):
    return DMP(dmp_normal(rep, lev, dom), dom, lev)

class DMP(PicklableWithSlots, CantSympify):
    """Dense Multivariate Polynomials over `K`. """

    __slots__ = ('rep', 'lev', 'dom', 'ring')

    def __init__(self, rep, dom, lev=None, ring=None):
        if lev is not None:
            if type(rep) is dict:
                rep = dmp_from_dict(rep, lev, dom)
            elif type(rep) is not list:
                rep = dmp_ground(dom.convert(rep), lev)
        else:
            rep, lev = dmp_validate(rep)

        self.rep = rep
        self.lev = lev
        self.dom = dom
        self.ring = ring

    def __repr__(f):
        return "%s(%s, %s, %s)" % (f.__class__.__name__, f.rep, f.dom, f.ring)

    def __hash__(f):
        return hash((f.__class__.__name__, f.to_tuple(), f.lev, f.dom, f.ring))

    def unify(f, g):
        """Unify representations of two multivariate polynomials. """
        if not isinstance(g, DMP) or f.lev != g.lev:
            raise UnificationFailed("can't unify %s with %s" % (f, g))

        if f.dom == g.dom and f.ring == g.ring:
            return f.lev, f.dom, f.per, f.rep, g.rep
        else:
            lev, dom = f.lev, f.dom.unify(g.dom)
            ring = f.ring
            if g.ring is not None:
                if ring is not None:
                    ring = ring.unify(g.ring)
                else:
                    ring = g.ring

            F = dmp_convert(f.rep, lev, f.dom, dom)
            G = dmp_convert(g.rep, lev, g.dom, dom)

            def per(rep, dom=dom, lev=lev, kill=False):
                if kill:
                    if not lev:
                        return rep
                    else:
                        lev -= 1

                return DMP(rep, dom, lev, ring)

            return lev, dom, per, F, G

    def per(f, rep, dom=None, kill=False, ring=None):
        """Create a DMP out of the given representation. """
        lev = f.lev

        if kill:
            if not lev:
                return rep
            else:
                lev -= 1

        if dom is None:
            dom = f.dom

        if ring is None:
            ring = f.ring

        return DMP(rep, dom, lev, ring)

    @classmethod
    def zero(cls, lev, dom, ring=None):
        return DMP(0, dom, lev, ring)

    @classmethod
    def one(cls, lev, dom, ring=None):
        return DMP(1, dom, lev, ring)

    @classmethod
    def from_list(cls, rep, lev, dom):
        """Create an instance of ``cls`` given a list of native coefficients. """
        return cls(dmp_convert(rep, lev, None, dom), dom, lev)

    @classmethod
    def from_sympy_list(cls, rep, lev, dom):
        """Create an instance of ``cls`` given a list of SymPy coefficients. """
        return cls(dmp_from_sympy(rep, lev, dom), dom, lev)

    def to_dict(f, zero=False):
        """Convert ``f`` to a dict representation with native coefficients. """
        return dmp_to_dict(f.rep, f.lev, f.dom, zero=zero)

    def to_sympy_dict(f, zero=False):
        """Convert ``f`` to a dict representation with SymPy coefficients. """
        rep = dmp_to_dict(f.rep, f.lev, f.dom, zero=zero)

        for k, v in rep.items():
            rep[k] = f.dom.to_sympy(v)

        return rep

    def to_list(f):
        """Convert ``f`` to a list representation with native coefficients. """
        return f.rep

    def to_sympy_list(f):
        """Convert ``f`` to a list representation with SymPy coefficients. """
        def sympify_nested_list(rep):
            out = []
            for val in rep:
                if isinstance(val, list):
                    out.append(sympify_nested_list(val))
                else:
                    out.append(f.dom.to_sympy(val))
            return out

        return sympify_nested_list(f.rep)

    def to_tuple(f):
        """
        Convert ``f`` to a tuple representation with native coefficients.

        This is needed for hashing.
        """
        return dmp_to_tuple(f.rep, f.lev)

    @classmethod
    def from_dict(cls, rep, lev, dom):
        """Construct and instance of ``cls`` from a ``dict`` representation. """
        return cls(dmp_from_dict(rep, lev, dom), dom, lev)

    @classmethod
    def from_monoms_coeffs(cls, monoms, coeffs, lev, dom, ring=None):
        return DMP(dict(list(zip(monoms, coeffs))), dom, lev, ring)

    def to_ring(f):
        """Make the ground domain a ring. """
        return f.convert(f.dom.get_ring())

    def to_field(f):
        """Make the ground domain a field. """
        return f.convert(f.dom.get_field())

    def to_exact(f):
        """Make the ground domain exact. """
        return f.convert(f.dom.get_exact())

    def convert(f, dom):
        """Convert the ground domain of ``f``. """
        if f.dom == dom:
            return f
        else:
            return DMP(dmp_convert(f.rep, f.lev, f.dom, dom), dom, f.lev)

    def slice(f, m, n, j=0):
        """Take a continuous subsequence of terms of ``f``. """
        return f.per(dmp_slice_in(f.rep, m, n, j, f.lev, f.dom))

    def coeffs(f, order=None):
        """Returns all non-zero coefficients from ``f`` in lex order. """
        return [ c for _, c in dmp_list_terms(f.rep, f.lev, f.dom, order=order) ]

    def monoms(f, order=None):
        """Returns all non-zero monomials from ``f`` in lex order. """
        return [ m for m, _ in dmp_list_terms(f.rep, f.lev, f.dom, order=order) ]

    def terms(f, order=None):
        """Returns all non-zero terms from ``f`` in lex order. """
        return dmp_list_terms(f.rep, f.lev, f.dom, order=order)

    def all_coeffs(f):
        """Returns all coefficients from ``f``. """
        if not f.lev:
            if not f:
                return [f.dom.zero]
            else:
                return [ c for c in f.rep ]
        else:
            raise PolynomialError('multivariate polynomials not supported')

    def all_monoms(f):
        """Returns all monomials from ``f``. """
        if not f.lev:
            n = dup_degree(f.rep)

            if n < 0:
                return [(0,)]
            else:
                return [ (n - i,) for i, c in enumerate(f.rep) ]
        else:
            raise PolynomialError('multivariate polynomials not supported')

    def all_terms(f):
        """Returns all terms from a ``f``. """
        if not f.lev:
            n = dup_degree(f.rep)

            if n < 0:
                return [((0,), f.dom.zero)]
            else:
                return [ ((n - i,), c) for i, c in enumerate(f.rep) ]
        else:
            raise PolynomialError('multivariate polynomials not supported')

    def lift(f):
        """Convert algebraic coefficients to rationals. """
        return f.per(dmp_lift(f.rep, f.lev, f.dom), dom=f.dom.dom)

    def deflate(f):
        """Reduce degree of `f` by mapping `x_i^m` to `y_i`. """
        J, F = dmp_deflate(f.rep, f.lev, f.dom)
        return J, f.per(F)

    def inject(f, front=False):
        """Inject ground domain generators into ``f``. """
        F, lev = dmp_inject(f.rep, f.lev, f.dom, front=front)
        return f.__class__(F, f.dom.dom, lev)

    def eject(f, dom, front=False):
        """Eject selected generators into the ground domain. """
        F = dmp_eject(f.rep, f.lev, dom, front=front)
        return f.__class__(F, dom, f.lev - len(dom.symbols))

    def exclude(f):
        r"""
        Remove useless generators from ``f``.

        Returns the removed generators and the new excluded ``f``.

        Examples
        ========

        >>> from sympy.polys.polyclasses import DMP
        >>> from sympy.polys.domains import ZZ

        >>> DMP([[[ZZ(1)]], [[ZZ(1)], [ZZ(2)]]], ZZ).exclude()
        ([2], DMP([[1], [1, 2]], ZZ, None))

        """
        J, F, u = dmp_exclude(f.rep, f.lev, f.dom)
        return J, f.__class__(F, f.dom, u)

    def permute(f, P):
        r"""
        Returns a polynomial in `K[x_{P(1)}, ..., x_{P(n)}]`.

        Examples
        ========

        >>> from sympy.polys.polyclasses import DMP
        >>> from sympy.polys.domains import ZZ

        >>> DMP([[[ZZ(2)], [ZZ(1), ZZ(0)]], [[]]], ZZ).permute([1, 0, 2])
        DMP([[[2], []], [[1, 0], []]], ZZ, None)

        >>> DMP([[[ZZ(2)], [ZZ(1), ZZ(0)]], [[]]], ZZ).permute([1, 2, 0])
        DMP([[[1], []], [[2, 0], []]], ZZ, None)

        """
        return f.per(dmp_permute(f.rep, P, f.lev, f.dom))

    def terms_gcd(f):
        """Remove GCD of terms from the polynomial ``f``. """
        J, F = dmp_terms_gcd(f.rep, f.lev, f.dom)
        return J, f.per(F)

    def add_ground(f, c):
        """Add an element of the ground domain to ``f``. """
        return f.per(dmp_add_ground(f.rep, f.dom.convert(c), f.lev, f.dom))

    def sub_ground(f, c):
        """Subtract an element of the ground domain from ``f``. """
        return f.per(dmp_sub_ground(f.rep, f.dom.convert(c), f.lev, f.dom))

    def mul_ground(f, c):
        """Multiply ``f`` by a an element of the ground domain. """
        return f.per(dmp_mul_ground(f.rep, f.dom.convert(c), f.lev, f.dom))

    def quo_ground(f, c):
        """Quotient of ``f`` by a an element of the ground domain. """
        return f.per(dmp_quo_ground(f.rep, f.dom.convert(c), f.lev, f.dom))

    def exquo_ground(f, c):
        """Exact quotient of ``f`` by a an element of the ground domain. """
        return f.per(dmp_exquo_ground(f.rep, f.dom.convert(c), f.lev, f.dom))

    def abs(f):
        """Make all coefficients in ``f`` positive. """
        return f.per(dmp_abs(f.rep, f.lev, f.dom))

    def neg(f):
        """Negate all coefficients in ``f``. """
        return f.per(dmp_neg(f.rep, f.lev, f.dom))

    def add(f, g):
        """Add two multivariate polynomials ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        return per(dmp_add(F, G, lev, dom))

    def sub(f, g):
        """Subtract two multivariate polynomials ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        return per(dmp_sub(F, G, lev, dom))

    def mul(f, g):
        """Multiply two multivariate polynomials ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        return per(dmp_mul(F, G, lev, dom))

    def sqr(f):
        """Square a multivariate polynomial ``f``. """
        return f.per(dmp_sqr(f.rep, f.lev, f.dom))

    def pow(f, n):
        """Raise ``f`` to a non-negative power ``n``. """
        if isinstance(n, int):
            return f.per(dmp_pow(f.rep, n, f.lev, f.dom))
        else:
            raise TypeError("``int`` expected, got %s" % type(n))

    def pdiv(f, g):
        """Polynomial pseudo-division of ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        q, r = dmp_pdiv(F, G, lev, dom)
        return per(q), per(r)

    def prem(f, g):
        """Polynomial pseudo-remainder of ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        return per(dmp_prem(F, G, lev, dom))

    def pquo(f, g):
        """Polynomial pseudo-quotient of ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        return per(dmp_pquo(F, G, lev, dom))

    def pexquo(f, g):
        """Polynomial exact pseudo-quotient of ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        return per(dmp_pexquo(F, G, lev, dom))

    def div(f, g):
        """Polynomial division with remainder of ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        q, r = dmp_div(F, G, lev, dom)
        return per(q), per(r)

    def rem(f, g):
        """Computes polynomial remainder of ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        return per(dmp_rem(F, G, lev, dom))

    def quo(f, g):
        """Computes polynomial quotient of ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        return per(dmp_quo(F, G, lev, dom))

    def exquo(f, g):
        """Computes polynomial exact quotient of ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        res = per(dmp_exquo(F, G, lev, dom))
        if f.ring and res not in f.ring:
            from sympy.polys.polyerrors import ExactQuotientFailed
            raise ExactQuotientFailed(f, g, f.ring)
        return res

    def degree(f, j=0):
        """Returns the leading degree of ``f`` in ``x_j``. """
        if isinstance(j, int):
            return dmp_degree_in(f.rep, j, f.lev)
        else:
            raise TypeError("``int`` expected, got %s" % type(j))

    def degree_list(f):
        """Returns a list of degrees of ``f``. """
        return dmp_degree_list(f.rep, f.lev)

    def total_degree(f):
        """Returns the total degree of ``f``. """
        return max(sum(m) for m in f.monoms())

    def homogenize(f, s):
        """Return homogeneous polynomial of ``f``"""
        td = f.total_degree()
        result = {}
        new_symbol = (s == len(f.terms()[0][0]))
        for term in f.terms():
            d = sum(term[0])
            if d < td:
                i = td - d
            else:
                i = 0
            if new_symbol:
                result[term[0] + (i,)] = term[1]
            else:
                l = list(term[0])
                l[s] += i
                result[tuple(l)] = term[1]
        return DMP(result, f.dom, f.lev + int(new_symbol), f.ring)

    def homogeneous_order(f):
        """Returns the homogeneous order of ``f``. """
        if f.is_zero:
            return -oo

        monoms = f.monoms()
        tdeg = sum(monoms[0])

        for monom in monoms:
            _tdeg = sum(monom)

            if _tdeg != tdeg:
                return None

        return tdeg

    def LC(f):
        """Returns the leading coefficient of ``f``. """
        return dmp_ground_LC(f.rep, f.lev, f.dom)

    def TC(f):
        """Returns the trailing coefficient of ``f``. """
        return dmp_ground_TC(f.rep, f.lev, f.dom)

    def nth(f, *N):
        """Returns the ``n``-th coefficient of ``f``. """
        if all(isinstance(n, int) for n in N):
            return dmp_ground_nth(f.rep, N, f.lev, f.dom)
        else:
            raise TypeError("a sequence of integers expected")

    def max_norm(f):
        """Returns maximum norm of ``f``. """
        return dmp_max_norm(f.rep, f.lev, f.dom)

    def l1_norm(f):
        """Returns l1 norm of ``f``. """
        return dmp_l1_norm(f.rep, f.lev, f.dom)

    def clear_denoms(f):
        """Clear denominators, but keep the ground domain. """
        coeff, F = dmp_clear_denoms(f.rep, f.lev, f.dom)
        return coeff, f.per(F)

    def integrate(f, m=1, j=0):
        """Computes the ``m``-th order indefinite integral of ``f`` in ``x_j``. """
        if not isinstance(m, int):
            raise TypeError("``int`` expected, got %s" % type(m))

        if not isinstance(j, int):
            raise TypeError("``int`` expected, got %s" % type(j))

        return f.per(dmp_integrate_in(f.rep, m, j, f.lev, f.dom))

    def diff(f, m=1, j=0):
        """Computes the ``m``-th order derivative of ``f`` in ``x_j``. """
        if not isinstance(m, int):
            raise TypeError("``int`` expected, got %s" % type(m))

        if not isinstance(j, int):
            raise TypeError("``int`` expected, got %s" % type(j))

        return f.per(dmp_diff_in(f.rep, m, j, f.lev, f.dom))

    def eval(f, a, j=0):
        """Evaluates ``f`` at the given point ``a`` in ``x_j``. """
        if not isinstance(j, int):
            raise TypeError("``int`` expected, got %s" % type(j))

        return f.per(dmp_eval_in(f.rep,
            f.dom.convert(a), j, f.lev, f.dom), kill=True)

    def half_gcdex(f, g):
        """Half extended Euclidean algorithm, if univariate. """
        lev, dom, per, F, G = f.unify(g)

        if not lev:
            s, h = dup_half_gcdex(F, G, dom)
            return per(s), per(h)
        else:
            raise ValueError('univariate polynomial expected')

    def gcdex(f, g):
        """Extended Euclidean algorithm, if univariate. """
        lev, dom, per, F, G = f.unify(g)

        if not lev:
            s, t, h = dup_gcdex(F, G, dom)
            return per(s), per(t), per(h)
        else:
            raise ValueError('univariate polynomial expected')

    def invert(f, g):
        """Invert ``f`` modulo ``g``, if possible. """
        lev, dom, per, F, G = f.unify(g)

        if not lev:
            return per(dup_invert(F, G, dom))
        else:
            raise ValueError('univariate polynomial expected')

    def revert(f, n):
        """Compute ``f**(-1)`` mod ``x**n``. """
        if not f.lev:
            return f.per(dup_revert(f.rep, n, f.dom))
        else:
            raise ValueError('univariate polynomial expected')

    def subresultants(f, g):
        """Computes subresultant PRS sequence of ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        R = dmp_subresultants(F, G, lev, dom)
        return list(map(per, R))

    def resultant(f, g, includePRS=False):
        """Computes resultant of ``f`` and ``g`` via PRS. """
        lev, dom, per, F, G = f.unify(g)
        if includePRS:
            res, R = dmp_resultant(F, G, lev, dom, includePRS=includePRS)
            return per(res, kill=True), list(map(per, R))
        return per(dmp_resultant(F, G, lev, dom), kill=True)

    def discriminant(f):
        """Computes discriminant of ``f``. """
        return f.per(dmp_discriminant(f.rep, f.lev, f.dom), kill=True)

    def cofactors(f, g):
        """Returns GCD of ``f`` and ``g`` and their cofactors. """
        lev, dom, per, F, G = f.unify(g)
        h, cff, cfg = dmp_inner_gcd(F, G, lev, dom)
        return per(h), per(cff), per(cfg)

    def gcd(f, g):
        """Returns polynomial GCD of ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        return per(dmp_gcd(F, G, lev, dom))

    def lcm(f, g):
        """Returns polynomial LCM of ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        return per(dmp_lcm(F, G, lev, dom))

    def cancel(f, g, include=True):
        """Cancel common factors in a rational function ``f/g``. """
        lev, dom, per, F, G = f.unify(g)

        if include:
            F, G = dmp_cancel(F, G, lev, dom, include=True)
        else:
            cF, cG, F, G = dmp_cancel(F, G, lev, dom, include=False)

        F, G = per(F), per(G)

        if include:
            return F, G
        else:
            return cF, cG, F, G

    def trunc(f, p):
        """Reduce ``f`` modulo a constant ``p``. """
        return f.per(dmp_ground_trunc(f.rep, f.dom.convert(p), f.lev, f.dom))

    def monic(f):
        """Divides all coefficients by ``LC(f)``. """
        return f.per(dmp_ground_monic(f.rep, f.lev, f.dom))

    def content(f):
        """Returns GCD of polynomial coefficients. """
        return dmp_ground_content(f.rep, f.lev, f.dom)

    def primitive(f):
        """Returns content and a primitive form of ``f``. """
        cont, F = dmp_ground_primitive(f.rep, f.lev, f.dom)
        return cont, f.per(F)

    def compose(f, g):
        """Computes functional composition of ``f`` and ``g``. """
        lev, dom, per, F, G = f.unify(g)
        return per(dmp_compose(F, G, lev, dom))

    def decompose(f):
        """Computes functional decomposition of ``f``. """
        if not f.lev:
            return list(map(f.per, dup_decompose(f.rep, f.dom)))
        else:
            raise ValueError('univariate polynomial expected')

    def shift(f, a):
        """Efficiently compute Taylor shift ``f(x + a)``. """
        if not f.lev:
            return f.per(dup_shift(f.rep, f.dom.convert(a), f.dom))
        else:
            raise ValueError('univariate polynomial expected')

    def transform(f, p, q):
        """Evaluate functional transformation ``q**n * f(p/q)``."""
        if f.lev:
            raise ValueError('univariate polynomial expected')

        lev, dom, per, P, Q = p.unify(q)
        lev, dom, per, F, P = f.unify(per(P, dom, lev))
        lev, dom, per, F, Q = per(F, dom, lev).unify(per(Q, dom, lev))

        if not lev:
            return per(dup_transform(F, P, Q, dom))
        else:
            raise ValueError('univariate polynomial expected')

    def sturm(f):
        """Computes the Sturm sequence of ``f``. """
        if not f.lev:
            return list(map(f.per, dup_sturm(f.rep, f.dom)))
        else:
            raise ValueError('univariate polynomial expected')

    def gff_list(f):
        """Computes greatest factorial factorization of ``f``. """
        if not f.lev:
            return [ (f.per(g), k) for g, k in dup_gff_list(f.rep, f.dom) ]
        else:
            raise ValueError('univariate polynomial expected')

    def norm(f):
        """Computes ``Norm(f)``."""
        r = dmp_norm(f.rep, f.lev, f.dom)
        return f.per(r, dom=f.dom.dom)

    def sqf_norm(f):
        """Computes square-free norm of ``f``. """
        s, g, r = dmp_sqf_norm(f.rep, f.lev, f.dom)
        return s, f.per(g), f.per(r, dom=f.dom.dom)

    def sqf_part(f):
        """Computes square-free part of ``f``. """
        return f.per(dmp_sqf_part(f.rep, f.lev, f.dom))

    def sqf_list(f, all=False):
        """Returns a list of square-free factors of ``f``. """
        coeff, factors = dmp_sqf_list(f.rep, f.lev, f.dom, all)
        return coeff, [ (f.per(g), k) for g, k in factors ]

    def sqf_list_include(f, all=False):
        """Returns a list of square-free factors of ``f``. """
        factors = dmp_sqf_list_include(f.rep, f.lev, f.dom, all)
        return [ (f.per(g), k) for g, k in factors ]

    def factor_list(f):
        """Returns a list of irreducible factors of ``f``. """
        coeff, factors = dmp_factor_list(f.rep, f.lev, f.dom)
        return coeff, [ (f.per(g), k) for g, k in factors ]

    def factor_list_include(f):
        """Returns a list of irreducible factors of ``f``. """
        factors = dmp_factor_list_include(f.rep, f.lev, f.dom)
        return [ (f.per(g), k) for g, k in factors ]

    def intervals(f, all=False, eps=None, inf=None, sup=None, fast=False, sqf=False):
        """Compute isolating intervals for roots of ``f``. """
        if not f.lev:
            if not all:
                if not sqf:
                    return dup_isolate_real_roots(f.rep, f.dom, eps=eps, inf=inf, sup=sup, fast=fast)
                else:
                    return dup_isolate_real_roots_sqf(f.rep, f.dom, eps=eps, inf=inf, sup=sup, fast=fast)
            else:
                if not sqf:
                    return dup_isolate_all_roots(f.rep, f.dom, eps=eps, inf=inf, sup=sup, fast=fast)
                else:
                    return dup_isolate_all_roots_sqf(f.rep, f.dom, eps=eps, inf=inf, sup=sup, fast=fast)
        else:
            raise PolynomialError(
                "can't isolate roots of a multivariate polynomial")

    def refine_root(f, s, t, eps=None, steps=None, fast=False):
        """
        Refine an isolating interval to the given precision.

        ``eps`` should be a rational number.

        """
        if not f.lev:
            return dup_refine_real_root(f.rep, s, t, f.dom, eps=eps, steps=steps, fast=fast)
        else:
            raise PolynomialError(
                "can't refine a root of a multivariate polynomial")

    def count_real_roots(f, inf=None, sup=None):
        """Return the number of real roots of ``f`` in ``[inf, sup]``. """
        return dup_count_real_roots(f.rep, f.dom, inf=inf, sup=sup)

    def count_complex_roots(f, inf=None, sup=None):
        """Return the number of complex roots of ``f`` in ``[inf, sup]``. """
        return dup_count_complex_roots(f.rep, f.dom, inf=inf, sup=sup)

    @property
    def is_zero(f):
        """Returns ``True`` if ``f`` is a zero polynomial. """
        return dmp_zero_p(f.rep, f.lev)

    @property
    def is_one(f):
        """Returns ``True`` if ``f`` is a unit polynomial. """
        return dmp_one_p(f.rep, f.lev, f.dom)

    @property
    def is_ground(f):
        """Returns ``True`` if ``f`` is an element of the ground domain. """
        return dmp_ground_p(f.rep, None, f.lev)

    @property
    def is_sqf(f):
        """Returns ``True`` if ``f`` is a square-free polynomial. """
        return dmp_sqf_p(f.rep, f.lev, f.dom)

    @property
    def is_monic(f):
        """Returns ``True`` if the leading coefficient of ``f`` is one. """
        return f.dom.is_one(dmp_ground_LC(f.rep, f.lev, f.dom))

    @property
    def is_primitive(f):
        """Returns ``True`` if the GCD of the coefficients of ``f`` is one. """
        return f.dom.is_one(dmp_ground_content(f.rep, f.lev, f.dom))

    @property
    def is_linear(f):
        """Returns ``True`` if ``f`` is linear in all its variables. """
        return all(sum(monom) <= 1 for monom in dmp_to_dict(f.rep, f.lev, f.dom).keys())

    @property
    def is_quadratic(f):
        """Returns ``True`` if ``f`` is quadratic in all its variables. """
        return all(sum(monom) <= 2 for monom in dmp_to_dict(f.rep, f.lev, f.dom).keys())

    @property
    def is_monomial(f):
        """Returns ``True`` if ``f`` is zero or has only one term. """
        return len(f.to_dict()) <= 1

    @property
    def is_homogeneous(f):
        """Returns ``True`` if ``f`` is a homogeneous polynomial. """
        return f.homogeneous_order() is not None

    @property
    def is_irreducible(f):
        """Returns ``True`` if ``f`` has no factors over its domain. """
        return dmp_irreducible_p(f.rep, f.lev, f.dom)

    @property
    def is_cyclotomic(f):
        """Returns ``True`` if ``f`` is a cyclotomic polynomial. """
        if not f.lev:
            return dup_cyclotomic_p(f.rep, f.dom)
        else:
            return False

    def __abs__(f):
        return f.abs()

    def __neg__(f):
        return f.neg()

    def __add__(f, g):
        if not isinstance(g, DMP):
            try:
                g = f.per(dmp_ground(f.dom.convert(g), f.lev))
            except TypeError:
                return NotImplemented
            except (CoercionFailed, NotImplementedError):
                if f.ring is not None:
                    try:
                        g = f.ring.convert(g)
                    except (CoercionFailed, NotImplementedError):
                        return NotImplemented

        return f.add(g)

    def __radd__(f, g):
        return f.__add__(g)

    def __sub__(f, g):
        if not isinstance(g, DMP):
            try:
                g = f.per(dmp_ground(f.dom.convert(g), f.lev))
            except TypeError:
                return NotImplemented
            except (CoercionFailed, NotImplementedError):
                if f.ring is not None:
                    try:
                        g = f.ring.convert(g)
                    except (CoercionFailed, NotImplementedError):
                        return NotImplemented

        return f.sub(g)

    def __rsub__(f, g):
        return (-f).__add__(g)

    def __mul__(f, g):
        if isinstance(g, DMP):
            return f.mul(g)
        else:
            try:
                return f.mul_ground(g)
            except TypeError:
                return NotImplemented
            except (CoercionFailed, NotImplementedError):
                if f.ring is not None:
                    try:
                        return f.mul(f.ring.convert(g))
                    except (CoercionFailed, NotImplementedError):
                        pass
                return NotImplemented

    def __truediv__(f, g):
        if isinstance(g, DMP):
            return f.exquo(g)
        else:
            try:
                return f.mul_ground(g)
            except TypeError:
                return NotImplemented
            except (CoercionFailed, NotImplementedError):
                if f.ring is not None:
                    try:
                        return f.exquo(f.ring.convert(g))
                    except (CoercionFailed, NotImplementedError):
                        pass
                return NotImplemented

    def __rtruediv__(f, g):
        if isinstance(g, DMP):
            return g.exquo(f)
        elif f.ring is not None:
            try:
                return f.ring.convert(g).exquo(f)
            except (CoercionFailed, NotImplementedError):
                pass
        return NotImplemented

    def __rmul__(f, g):
        return f.__mul__(g)

    def __pow__(f, n):
        return f.pow(n)

    def __divmod__(f, g):
        return f.div(g)

    def __mod__(f, g):
        return f.rem(g)

    def __floordiv__(f, g):
        if isinstance(g, DMP):
            return f.quo(g)
        else:
            try:
                return f.quo_ground(g)
            except TypeError:
                return NotImplemented

    def __eq__(f, g):
        try:
            _, _, _, F, G = f.unify(g)

            if f.lev == g.lev:
                return F == G
        except UnificationFailed:
            pass

        return False

    def __ne__(f, g):
        return not f == g

    def eq(f, g, strict=False):
        if not strict:
            return f == g
        else:
            return f._strict_eq(g)

    def ne(f, g, strict=False):
        return not f.eq(g, strict=strict)

    def _strict_eq(f, g):
        return isinstance(g, f.__class__) and f.lev == g.lev \
            and f.dom == g.dom \
            and f.rep == g.rep

    def __lt__(f, g):
        _, _, _, F, G = f.unify(g)
        return F < G

    def __le__(f, g):
        _, _, _, F, G = f.unify(g)
        return F <= G

    def __gt__(f, g):
        _, _, _, F, G = f.unify(g)
        return F > G

    def __ge__(f, g):
        _, _, _, F, G = f.unify(g)
        return F >= G

    def __bool__(f):
        return not dmp_zero_p(f.rep, f.lev)

def init_normal_DMF(num, den, lev, dom):
    return DMF(dmp_normal(num, lev, dom),
               dmp_normal(den, lev, dom), dom, lev)

class DMF(PicklableWithSlots, CantSympify):
    """Dense Multivariate Fractions over `K`. """

    __slots__ = ('num', 'den', 'lev', 'dom', 'ring')

    def __init__(self, rep, dom, lev=None, ring=None):
        num, den, lev = self._parse(rep, dom, lev)
        num, den = dmp_cancel(num, den, lev, dom)

        self.num = num
        self.den = den
        self.lev = lev
        self.dom = dom
        self.ring = ring

    @classmethod
    def new(cls, rep, dom, lev=None, ring=None):
        num, den, lev = cls._parse(rep, dom, lev)

        obj = object.__new__(cls)

        obj.num = num
        obj.den = den
        obj.lev = lev
        obj.dom = dom
        obj.ring = ring

        return obj

    @classmethod
    def _parse(cls, rep, dom, lev=None):
        if type(rep) is tuple:
            num, den = rep

            if lev is not None:
                if type(num) is dict:
                    num = dmp_from_dict(num, lev, dom)

                if type(den) is dict:
                    den = dmp_from_dict(den, lev, dom)
            else:
                num, num_lev = dmp_validate(num)
                den, den_lev = dmp_validate(den)

                if num_lev == den_lev:
                    lev = num_lev
                else:
                    raise ValueError('inconsistent number of levels')

            if dmp_zero_p(den, lev):
                raise ZeroDivisionError('fraction denominator')

            if dmp_zero_p(num, lev):
                den = dmp_one(lev, dom)
            else:
                if dmp_negative_p(den, lev, dom):
                    num = dmp_neg(num, lev, dom)
                    den = dmp_neg(den, lev, dom)
        else:
            num = rep

            if lev is not None:
                if type(num) is dict:
                    num = dmp_from_dict(num, lev, dom)
                elif type(num) is not list:
                    num = dmp_ground(dom.convert(num), lev)
            else:
                num, lev = dmp_validate(num)

            den = dmp_one(lev, dom)

        return num, den, lev

    def __repr__(f):
        return "%s((%s, %s), %s, %s)" % (f.__class__.__name__, f.num, f.den,
                                         f.dom, f.ring)

    def __hash__(f):
        return hash((f.__class__.__name__, dmp_to_tuple(f.num, f.lev),
            dmp_to_tuple(f.den, f.lev), f.lev, f.dom, f.ring))

    def poly_unify(f, g):
        """Unify a multivariate fraction and a polynomial. """
        if not isinstance(g, DMP) or f.lev != g.lev:
            raise UnificationFailed("can't unify %s with %s" % (f, g))

        if f.dom == g.dom and f.ring == g.ring:
            return (f.lev, f.dom, f.per, (f.num, f.den), g.rep)
        else:
            lev, dom = f.lev, f.dom.unify(g.dom)
            ring = f.ring
            if g.ring is not None:
                if ring is not None:
                    ring = ring.unify(g.ring)
                else:
                    ring = g.ring

            F = (dmp_convert(f.num, lev, f.dom, dom),
                 dmp_convert(f.den, lev, f.dom, dom))

            G = dmp_convert(g.rep, lev, g.dom, dom)

            def per(num, den, cancel=True, kill=False, lev=lev):
                if kill:
                    if not lev:
                        return num/den
                    else:
                        lev = lev - 1

                if cancel:
                    num, den = dmp_cancel(num, den, lev, dom)

                return f.__class__.new((num, den), dom, lev, ring=ring)

            return lev, dom, per, F, G

    def frac_unify(f, g):
        """Unify representations of two multivariate fractions. """
        if not isinstance(g, DMF) or f.lev != g.lev:
            raise UnificationFailed("can't unify %s with %s" % (f, g))

        if f.dom == g.dom and f.ring == g.ring:
            return (f.lev, f.dom, f.per, (f.num, f.den),
                                         (g.num, g.den))
        else:
            lev, dom = f.lev, f.dom.unify(g.dom)
            ring = f.ring
            if g.ring is not None:
                if ring is not None:
                    ring = ring.unify(g.ring)
                else:
                    ring = g.ring

            F = (dmp_convert(f.num, lev, f.dom, dom),
                 dmp_convert(f.den, lev, f.dom, dom))

            G = (dmp_convert(g.num, lev, g.dom, dom),
                 dmp_convert(g.den, lev, g.dom, dom))

            def per(num, den, cancel=True, kill=False, lev=lev):
                if kill:
                    if not lev:
                        return num/den
                    else:
                        lev = lev - 1

                if cancel:
                    num, den = dmp_cancel(num, den, lev, dom)

                return f.__class__.new((num, den), dom, lev, ring=ring)

            return lev, dom, per, F, G

    def per(f, num, den, cancel=True, kill=False, ring=None):
        """Create a DMF out of the given representation. """
        lev, dom = f.lev, f.dom

        if kill:
            if not lev:
                return num/den
            else:
                lev -= 1

        if cancel:
            num, den = dmp_cancel(num, den, lev, dom)

        if ring is None:
            ring = f.ring

        return f.__class__.new((num, den), dom, lev, ring=ring)

    def half_per(f, rep, kill=False):
        """Create a DMP out of the given representation. """
        lev = f.lev

        if kill:
            if not lev:
                return rep
            else:
                lev -= 1

        return DMP(rep, f.dom, lev)

    @classmethod
    def zero(cls, lev, dom, ring=None):
        return cls.new(0, dom, lev, ring=ring)

    @classmethod
    def one(cls, lev, dom, ring=None):
        return cls.new(1, dom, lev, ring=ring)

    def numer(f):
        """Returns the numerator of ``f``. """
        return f.half_per(f.num)

    def denom(f):
        """Returns the denominator of ``f``. """
        return f.half_per(f.den)

    def cancel(f):
        """Remove common factors from ``f.num`` and ``f.den``. """
        return f.per(f.num, f.den)

    def neg(f):
        """Negate all coefficients in ``f``. """
        return f.per(dmp_neg(f.num, f.lev, f.dom), f.den, cancel=False)

    def add(f, g):
        """Add two multivariate fractions ``f`` and ``g``. """
        if isinstance(g, DMP):
            lev, dom, per, (F_num, F_den), G = f.poly_unify(g)
            num, den = dmp_add_mul(F_num, F_den, G, lev, dom), F_den
        else:
            lev, dom, per, F, G = f.frac_unify(g)
            (F_num, F_den), (G_num, G_den) = F, G

            num = dmp_add(dmp_mul(F_num, G_den, lev, dom),
                          dmp_mul(F_den, G_num, lev, dom), lev, dom)
            den = dmp_mul(F_den, G_den, lev, dom)

        return per(num, den)

    def sub(f, g):
        """Subtract two multivariate fractions ``f`` and ``g``. """
        if isinstance(g, DMP):
            lev, dom, per, (F_num, F_den), G = f.poly_unify(g)
            num, den = dmp_sub_mul(F_num, F_den, G, lev, dom), F_den
        else:
            lev, dom, per, F, G = f.frac_unify(g)
            (F_num, F_den), (G_num, G_den) = F, G

            num = dmp_sub(dmp_mul(F_num, G_den, lev, dom),
                          dmp_mul(F_den, G_num, lev, dom), lev, dom)
            den = dmp_mul(F_den, G_den, lev, dom)

        return per(num, den)

    def mul(f, g):
        """Multiply two multivariate fractions ``f`` and ``g``. """
        if isinstance(g, DMP):
            lev, dom, per, (F_num, F_den), G = f.poly_unify(g)
            num, den = dmp_mul(F_num, G, lev, dom), F_den
        else:
            lev, dom, per, F, G = f.frac_unify(g)
            (F_num, F_den), (G_num, G_den) = F, G

            num = dmp_mul(F_num, G_num, lev, dom)
            den = dmp_mul(F_den, G_den, lev, dom)

        return per(num, den)

    def pow(f, n):
        """Raise ``f`` to a non-negative power ``n``. """
        if isinstance(n, int):
            return f.per(dmp_pow(f.num, n, f.lev, f.dom),
                         dmp_pow(f.den, n, f.lev, f.dom), cancel=False)
        else:
            raise TypeError("``int`` expected, got %s" % type(n))

    def quo(f, g):
        """Computes quotient of fractions ``f`` and ``g``. """
        if isinstance(g, DMP):
            lev, dom, per, (F_num, F_den), G = f.poly_unify(g)
            num, den = F_num, dmp_mul(F_den, G, lev, dom)
        else:
            lev, dom, per, F, G = f.frac_unify(g)
            (F_num, F_den), (G_num, G_den) = F, G

            num = dmp_mul(F_num, G_den, lev, dom)
            den = dmp_mul(F_den, G_num, lev, dom)

        res = per(num, den)
        if f.ring is not None and res not in f.ring:
            from sympy.polys.polyerrors import ExactQuotientFailed
            raise ExactQuotientFailed(f, g, f.ring)
        return res

    exquo = quo

    def invert(f, check=True):
        """Computes inverse of a fraction ``f``. """
        if check and f.ring is not None and not f.ring.is_unit(f):
            raise NotReversible(f, f.ring)
        res = f.per(f.den, f.num, cancel=False)
        return res

    @property
    def is_zero(f):
        """Returns ``True`` if ``f`` is a zero fraction. """
        return dmp_zero_p(f.num, f.lev)

    @property
    def is_one(f):
        """Returns ``True`` if ``f`` is a unit fraction. """
        return dmp_one_p(f.num, f.lev, f.dom) and \
            dmp_one_p(f.den, f.lev, f.dom)

    def __neg__(f):
        return f.neg()

    def __add__(f, g):
        if isinstance(g, (DMP, DMF)):
            return f.add(g)

        try:
            return f.add(f.half_per(g))
        except TypeError:
            return NotImplemented
        except (CoercionFailed, NotImplementedError):
            if f.ring is not None:
                try:
                    return f.add(f.ring.convert(g))
                except (CoercionFailed, NotImplementedError):
                    pass
            return NotImplemented

    def __radd__(f, g):
        return f.__add__(g)

    def __sub__(f, g):
        if isinstance(g, (DMP, DMF)):
            return f.sub(g)

        try:
            return f.sub(f.half_per(g))
        except TypeError:
            return NotImplemented
        except (CoercionFailed, NotImplementedError):
            if f.ring is not None:
                try:
                    return f.sub(f.ring.convert(g))
                except (CoercionFailed, NotImplementedError):
                    pass
            return NotImplemented

    def __rsub__(f, g):
        return (-f).__add__(g)

    def __mul__(f, g):
        if isinstance(g, (DMP, DMF)):
            return f.mul(g)

        try:
            return f.mul(f.half_per(g))
        except TypeError:
            return NotImplemented
        except (CoercionFailed, NotImplementedError):
            if f.ring is not None:
                try:
                    return f.mul(f.ring.convert(g))
                except (CoercionFailed, NotImplementedError):
                    pass
            return NotImplemented

    def __rmul__(f, g):
        return f.__mul__(g)

    def __pow__(f, n):
        return f.pow(n)

    def __truediv__(f, g):
        if isinstance(g, (DMP, DMF)):
            return f.quo(g)

        try:
            return f.quo(f.half_per(g))
        except TypeError:
            return NotImplemented
        except (CoercionFailed, NotImplementedError):
            if f.ring is not None:
                try:
                    return f.quo(f.ring.convert(g))
                except (CoercionFailed, NotImplementedError):
                    pass
            return NotImplemented

    def __rtruediv__(self, g):
        r = self.invert(check=False)*g
        if self.ring and r not in self.ring:
            from sympy.polys.polyerrors import ExactQuotientFailed
            raise ExactQuotientFailed(g, self, self.ring)
        return r

    def __eq__(f, g):
        try:
            if isinstance(g, DMP):
                _, _, _, (F_num, F_den), G = f.poly_unify(g)

                if f.lev == g.lev:
                    return dmp_one_p(F_den, f.lev, f.dom) and F_num == G
            else:
                _, _, _, F, G = f.frac_unify(g)

                if f.lev == g.lev:
                    return F == G
        except UnificationFailed:
            pass

        return False

    def __ne__(f, g):
        try:
            if isinstance(g, DMP):
                _, _, _, (F_num, F_den), G = f.poly_unify(g)

                if f.lev == g.lev:
                    return not (dmp_one_p(F_den, f.lev, f.dom) and F_num == G)
            else:
                _, _, _, F, G = f.frac_unify(g)

                if f.lev == g.lev:
                    return F != G
        except UnificationFailed:
            pass

        return True

    def __lt__(f, g):
        _, _, _, F, G = f.frac_unify(g)
        return F < G

    def __le__(f, g):
        _, _, _, F, G = f.frac_unify(g)
        return F <= G

    def __gt__(f, g):
        _, _, _, F, G = f.frac_unify(g)
        return F > G

    def __ge__(f, g):
        _, _, _, F, G = f.frac_unify(g)
        return F >= G

    def __bool__(f):
        return not dmp_zero_p(f.num, f.lev)

def init_normal_ANP(rep, mod, dom):
    return ANP(dup_normal(rep, dom),
               dup_normal(mod, dom), dom)

class ANP(PicklableWithSlots, CantSympify):
    """Dense Algebraic Number Polynomials over a field. """

    __slots__ = ('rep', 'mod', 'dom')

    def __init__(self, rep, mod, dom):
        if type(rep) is dict:
            self.rep = dup_from_dict(rep, dom)
        else:
            if type(rep) is not list:
                rep = [dom.convert(rep)]

            self.rep = dup_strip(rep)

        if isinstance(mod, DMP):
            self.mod = mod.rep
        else:
            if type(mod) is dict:
                self.mod = dup_from_dict(mod, dom)
            else:
                self.mod = dup_strip(mod)

        self.dom = dom

    def __repr__(f):
        return "%s(%s, %s, %s)" % (f.__class__.__name__, f.rep, f.mod, f.dom)

    def __hash__(f):
        return hash((f.__class__.__name__, f.to_tuple(), dmp_to_tuple(f.mod, 0), f.dom))

    def unify(f, g):
        """Unify representations of two algebraic numbers. """
        if not isinstance(g, ANP) or f.mod != g.mod:
            raise UnificationFailed("can't unify %s with %s" % (f, g))

        if f.dom == g.dom:
            return f.dom, f.per, f.rep, g.rep, f.mod
        else:
            dom = f.dom.unify(g.dom)

            F = dup_convert(f.rep, f.dom, dom)
            G = dup_convert(g.rep, g.dom, dom)

            if dom != f.dom and dom != g.dom:
                mod = dup_convert(f.mod, f.dom, dom)
            else:
                if dom == f.dom:
                    mod = f.mod
                else:
                    mod = g.mod

            per = lambda rep: ANP(rep, mod, dom)

        return dom, per, F, G, mod

    def per(f, rep, mod=None, dom=None):
        return ANP(rep, mod or f.mod, dom or f.dom)

    @classmethod
    def zero(cls, mod, dom):
        return ANP(0, mod, dom)

    @classmethod
    def one(cls, mod, dom):
        return ANP(1, mod, dom)

    def to_dict(f):
        """Convert ``f`` to a dict representation with native coefficients. """
        return dmp_to_dict(f.rep, 0, f.dom)

    def to_sympy_dict(f):
        """Convert ``f`` to a dict representation with SymPy coefficients. """
        rep = dmp_to_dict(f.rep, 0, f.dom)

        for k, v in rep.items():
            rep[k] = f.dom.to_sympy(v)

        return rep

    def to_list(f):
        """Convert ``f`` to a list representation with native coefficients. """
        return f.rep

    def to_sympy_list(f):
        """Convert ``f`` to a list representation with SymPy coefficients. """
        return [ f.dom.to_sympy(c) for c in f.rep ]

    def to_tuple(f):
        """
        Convert ``f`` to a tuple representation with native coefficients.

        This is needed for hashing.
        """
        return dmp_to_tuple(f.rep, 0)

    @classmethod
    def from_list(cls, rep, mod, dom):
        return ANP(dup_strip(list(map(dom.convert, rep))), mod, dom)

    def neg(f):
        return f.per(dup_neg(f.rep, f.dom))

    def add(f, g):
        dom, per, F, G, mod = f.unify(g)
        return per(dup_add(F, G, dom))

    def sub(f, g):
        dom, per, F, G, mod = f.unify(g)
        return per(dup_sub(F, G, dom))

    def mul(f, g):
        dom, per, F, G, mod = f.unify(g)
        return per(dup_rem(dup_mul(F, G, dom), mod, dom))

    def pow(f, n):
        """Raise ``f`` to a non-negative power ``n``. """
        if isinstance(n, int):
            if n < 0:
                F, n = dup_invert(f.rep, f.mod, f.dom), -n
            else:
                F = f.rep

            return f.per(dup_rem(dup_pow(F, n, f.dom), f.mod, f.dom))
        else:
            raise TypeError("``int`` expected, got %s" % type(n))

    def div(f, g):
        dom, per, F, G, mod = f.unify(g)
        return (per(dup_rem(dup_mul(F, dup_invert(G, mod, dom), dom), mod, dom)), f.zero(mod, dom))

    def rem(f, g):
        dom, _, _, G, mod = f.unify(g)

        s, h = dup_half_gcdex(G, mod, dom)

        if h == [dom.one]:
            return f.zero(mod, dom)
        else:
            raise NotInvertible("zero divisor")

    def quo(f, g):
        dom, per, F, G, mod = f.unify(g)
        return per(dup_rem(dup_mul(F, dup_invert(G, mod, dom), dom), mod, dom))

    exquo = quo

    def LC(f):
        """Returns the leading coefficient of ``f``. """
        return dup_LC(f.rep, f.dom)

    def TC(f):
        """Returns the trailing coefficient of ``f``. """
        return dup_TC(f.rep, f.dom)

    @property
    def is_zero(f):
        """Returns ``True`` if ``f`` is a zero algebraic number. """
        return not f

    @property
    def is_one(f):
        """Returns ``True`` if ``f`` is a unit algebraic number. """
        return f.rep == [f.dom.one]

    @property
    def is_ground(f):
        """Returns ``True`` if ``f`` is an element of the ground domain. """
        return not f.rep or len(f.rep) == 1

    def __pos__(f):
        return f

    def __neg__(f):
        return f.neg()

    def __add__(f, g):
        if isinstance(g, ANP):
            return f.add(g)
        else:
            try:
                return f.add(f.per(g))
            except (CoercionFailed, TypeError):
                return NotImplemented

    def __radd__(f, g):
        return f.__add__(g)

    def __sub__(f, g):
        if isinstance(g, ANP):
            return f.sub(g)
        else:
            try:
                return f.sub(f.per(g))
            except (CoercionFailed, TypeError):
                return NotImplemented

    def __rsub__(f, g):
        return (-f).__add__(g)

    def __mul__(f, g):
        if isinstance(g, ANP):
            return f.mul(g)
        else:
            try:
                return f.mul(f.per(g))
            except (CoercionFailed, TypeError):
                return NotImplemented

    def __rmul__(f, g):
        return f.__mul__(g)

    def __pow__(f, n):
        return f.pow(n)

    def __divmod__(f, g):
        return f.div(g)

    def __mod__(f, g):
        return f.rem(g)

    def __truediv__(f, g):
        if isinstance(g, ANP):
            return f.quo(g)
        else:
            try:
                return f.quo(f.per(g))
            except (CoercionFailed, TypeError):
                return NotImplemented

    def __eq__(f, g):
        try:
            _, _, F, G, _ = f.unify(g)

            return F == G
        except UnificationFailed:
            return False

    def __ne__(f, g):
        try:
            _, _, F, G, _ = f.unify(g)

            return F != G
        except UnificationFailed:
            return True

    def __lt__(f, g):
        _, _, F, G, _ = f.unify(g)
        return F < G

    def __le__(f, g):
        _, _, F, G, _ = f.unify(g)
        return F <= G

    def __gt__(f, g):
        _, _, F, G, _ = f.unify(g)
        return F > G

    def __ge__(f, g):
        _, _, F, G, _ = f.unify(g)
        return F >= G

    def __bool__(f):
        return bool(f.rep)


import pandas as pd
import geopandas as gpd

pd.options.mode.chained_assignment = None

# Load different data
renacom = pd.read_csv('~/data/niger_election_data/processed/renacom_full.csv',
                      encoding="ISO-8859-1")
renaloc = pd.read_csv('~/data/niger_election_data/processed/renaloc_full.csv',
                      encoding="ISO-8859-1")
renaloc = renaloc[renaloc.level == 'Localite']
renaloc.region = renaloc.region.str.lower()
renaloc.departement = renaloc.departement.str.lower()
renaloc.commune = renaloc.commune.str.lower()
osm = pd.read_csv('~/data/osm/niger.csv', encoding="ISO-8859-1")
bureaux = pd.read_csv('~/data/niger_election_data/raw/Niger_Bureaux.csv',
                      encoding='ISO-8859-1')
health_facilities = pd.read_csv('~/data/dhis/niger/org_units_description.csv')

# Standardize names in RENALOC and RENACOM
def change_number(name, suffix, changer):
    """Change suffixes in names."""
    len_suffix = len(suffix)
    to_change = (name.str[-len_suffix:] == suffix)
    changer = name[to_change].str[0:-len_suffix] + [changer]
    name.loc[to_change] = changer
    return name

change_number(renacom.commune, ' i', ' 1')
change_number(renacom.commune, ' ii', ' 2')
change_number(renacom.commune, ' iii', ' 3')
change_number(renacom.commune, ' iv', ' 4')
change_number(renacom.commune, ' v', ' 5')
change_number(renaloc.commune, ' i', ' 1')
change_number(renaloc.commune, ' ii', ' 2')

# Add region, departement, commune in bureaux
commune = pd.read_csv('~/data/niger_election_data/raw/Niger_Communes.csv',
                      encoding='ISO-8859-1')
bureaux = bureaux.drop('Unnamed: 0', axis=1)
commune = commune.drop('Unnamed: 0', axis=1)
bureaux = bureaux.merge(commune)
departement = pd.read_csv('~/data/niger_election_data/raw/Niger_Departements.csv',
                          encoding='ISO-8859-1')
departement = departement.drop('Unnamed: 0', axis=1)
bureaux = bureaux.merge(departement)
region = pd.read_csv('~/data/niger_election_data/raw/Niger_Regions.csv',
                     encoding='ISO-8859-1')
region = region.drop('Unnamed: 0', axis=1)
bureaux = bureaux.merge(region)

bureaux = bureaux[['ID_BUREAU', 'NOM_BUREAU', 'NOM_COMMUNE', 'NOM_DEPART',
                   'NOM_REGION']]
bureaux.columns = ['locality_id', 'locality', 'commune', 'departement',
                   'region']

bureaux.commune = bureaux.commune.str.lower()
bureaux.departement = bureaux.departement.str.lower()
bureaux.region = bureaux.region.str.lower()

change_number(bureaux.commune, ' i', ' 1')
change_number(bureaux.commune, ' ii', ' 2')
change_number(bureaux.commune, ' iii', ' 3')
change_number(bureaux.commune, ' iv', ' 4')
change_number(bureaux.commune, ' v', ' 5')
renacom.commune[~renacom.commune.isin(bureaux.commune)].unique()
bureaux.commune[bureaux.commune == 'tibiri (doutchi)'] = 'tibiri'
bureaux.commune[bureaux.commune == 'gangara (aguie)'] = 'gangara'
bureaux.commune[bureaux.commune == 'gangara (tanout)'] = 'gangara'
bureaux.commune[bureaux.commune == 'tibiri (maradi)'] = 'tibiri'
bureaux.commune[bureaux.commune == 'maradi arrondissement 1'] = 'arrondissement 1'
bureaux.commune[bureaux.commune == 'maradi arrondissement 2'] = 'arrondissement 2'
bureaux.commune[bureaux.commune == 'maradi arrondissement 3'] = 'arrondissement 3'
bureaux.departement[bureaux.departement == 'tibiri (doutchi)'] = 'tibiri'

# Standardize GPS communes names

fp = '/Users/grlurton/data/niger_election_data/external/commune_shp/nigcom.shp'
commune_gps = gpd.read_file(fp)

commune_gps.GPS_NAME = commune_gps.GPS_NAME.str.lower()

renaloc_no_match = renaloc[~renaloc.commune.isin(commune_gps.GPS_NAME)]
gps_no_match = commune_gps[~commune_gps.GPS_NAME.isin(renaloc.commune)]

def standardize_name(var, pattern, replace):
    out = var.loc[:].str.replace(pattern, replace)
    return out.values

gps_no_match.loc[:, 'standard_name'] = standardize_name(gps_no_match['GPS_NAME'], "'", "")
renaloc_no_match.loc[:, 'standard_name'] = standardize_name(renaloc_no_match['commune'], "'", "")

dico = {}

for idx in gps_no_match.index:
    standard_name = gps_no_match.loc[idx, 'standard_name']
    if standard_name in renaloc_no_match.standard_name.tolist():
        print(standard_name)
        ren_name = renaloc_no_match.loc[renaloc_no_match['standard_name'] == standard_name,'commune'].unique()[0]
        dico[ren_name] = gps_no_match.loc[idx, 'GPS_NAME']
        commune_gps.loc[idx, 'GPS_NAME'] = ren_name

dico

renaloc_no_match = renaloc[~renaloc.commune.isin(commune_gps.GPS_NAME)]
gps_no_match = commune_gps[~commune_gps.GPS_NAME.isin(renaloc.commune)]

gps_no_match.loc[:, 'standard_name'] = standardize_name(gps_no_match['GPS_NAME'], "-", " ")
renaloc_no_match.loc[:, 'standard_name'] = standardize_name(renaloc_no_match['commune'], "-", " ")

for idx in gps_no_match.index:
    standard_name = gps_no_match.loc[idx, 'standard_name']
    if standard_name in renaloc_no_match.standard_name.tolist():
        print(standard_name)
        ren_name = renaloc_no_match.loc[renaloc_no_match['standard_name'] == standard_name,'commune'].unique()[0]
        dico[ren_name] = gps_no_match.loc[idx, 'GPS_NAME']
        commune_gps.loc[idx, 'GPS_NAME'] = ren_name

renaloc_no_match = renaloc[~renaloc.commune.isin(commune_gps.GPS_NAME)]
gps_no_match = commune_gps[~commune_gps.GPS_NAME.isin(renaloc.commune)]

gps_no_match.loc[:, 'standard_name'] = standardize_name(gps_no_match['GPS_NAME'], " ", "")
renaloc_no_match.loc[:, 'standard_name'] = standardize_name(renaloc_no_match['commune'], " ", "")

for idx in gps_no_match.index:
    standard_name = gps_no_match.loc[idx, 'standard_name']
    if standard_name in renaloc_no_match.standard_name.tolist():
        print(standard_name)
        ren_name = renaloc_no_match.loc[renaloc_no_match['standard_name'] == standard_name,'commune'].unique()[0]
        dico[ren_name] = gps_no_match.loc[idx, 'GPS_NAME']
        commune_gps.loc[idx, 'GPS_NAME'] = ren_name

renaloc_no_match = renaloc[~renaloc.commune.isin(commune_gps.GPS_NAME)]
gps_no_match = commune_gps[~commune_gps.GPS_NAME.isin(renaloc.commune)]

dico["attantane"] = "attatane"
dico["bankilare"] = "bankillare"
dico['birni lalle'] = 'birni nlalle'
dico['damagaram takaya'] = "damagaram-t"
dico['dan-goulbi'] = 'dan goulgi'
dico['diagourou'] = 'diagorou'
dico["dingazi"] = 'dingazi banda'
dico["djiratawa"] = 'djirataoua'
dico["dogonkiria"] = "dogon kirya"
dico['el allassane maireyrey'] = 'alhassane  mairerey'
dico['falmey/falmey haoussa'] = 'falmey'
dico['farey'] = 'farrey'
dico['galma koudawatche'] = "galma"
dico['goroubankassam'] = 'goroun bakassam'
dico['guidan amoumoune'] = 'guidan amoumane'
dico['guilladje'] = 'guiladje'
dico['kablewa'] = 'kabalewa'
dico['kourfeye centre'] = 'kourfey centre'
dico['kourteye'] = 'kourtey'
dico['makalondi'] = "parc w"
dico['matamey'] = "matameye"
dico["n'guelbely"] = 'nguel bely'
dico['ouro gueladjo'] = 'ouro gueladio'
dico['roumbou 1'] = 'roumbou i'
dico['sakoira'] = 'sakouara'
dico['sarkin haoussa'] = "serkin haoussa"
dico['sarkin yamma'] = 'sarkin yama'
dico['tagriss'] = 'tagris'
dico['tombokoirey 1'] = 'tombokoarey 1'
dico['tombokoirey 2'] = 'tombokoarey 2'
dico['tondikiwindi'] = 'tondikwindi'
dico['tsernaoua'] = 'tsernawa'

for comm in dico.keys():
    commune_gps.GPS_NAME[commune_gps.GPS_NAME == dico[comm]] = comm

renaloc_no_match = renaloc[~renaloc.commune.isin(commune_gps.GPS_NAME)]
gps_no_match = commune_gps[~commune_gps.GPS_NAME.isin(renaloc.commune)]

# [k for k, v in dico.items() if 'dogo dogo' in v]

# Geocache OSM

# OSM data from https://download.geofabrik.de/africa/niger.html

fp = '/Users/grlurton/data/osm/niger-latest-free.shp/gis_osm_places_free_1.shp'
osm_data = gpd.read_file(fp)

osm_data = osm_data.to_crs(commune_gps.crs)

osm_data = gpd.sjoin(osm_data, commune_gps, how="inner", op="within")

# TODO Add region, departement, commune in dhis

# TODO if needed geocache DHIS

# Standardize output

renacom_keep = ['MILIEU', 'region', 'departement', 'commune', 'LOCALITE',
                'TYPELOCALITE', 'MASCULIN', 'FEMININ',
                'TOTAL', 'LONGITUDE', 'LATITUDE']
renacom_out = renacom[renacom_keep]
renacom_out.columns = ['milieu', 'region', 'departement', 'commune',
                       'locality', 'locality_type', 'hommes', 'femmes',
                       'population', 'longitude', 'latitude']
renacom_out['source'] = 'renacom'
renacom_out['locality_id'] = ['RENACOM'] + pd.Series(list(range(len(renacom_out)))).astype(str)

renaloc_keep = ['locality', 'population', 'hommes', 'femmes',
                'settlement_type', 'region', 'departement', 'commune',
                'longitude', 'latitude']
renaloc_out = renaloc[pd.isnull(renaloc.milieu)]
renaloc_out = renaloc_out[renaloc_keep]
renaloc_out.columns = ['locality', 'population', 'hommes', 'femmes',
                       'locality_type', 'region', 'departement', 'commune',
                       'longitude', 'latitude']
renaloc_out['source'] = 'renaloc'
renaloc_out['locality_id'] = ['RENALOC'] + pd.Series(list(range(len(renaloc_out)))).astype(str)

osm_keep = ['osm_id', 'fclass', 'population', 'name', 'geometry', 'GPS_NAME',
            'REGION']
osm_data['longitude'] = osm_data.geometry.x
osm_data['latitude'] = osm_data.geometry.y
osm_out = osm_data[osm_keep]
osm_out.columns = ['locality_id', 'locality_type', 'population', 'locality',
                   'geometry', 'commune', 'region']
osm_out['source'] = 'osm'

bureaux_out = bureaux[~(bureaux.region == 'diaspora')]
bureaux_out['source'] = 'bureaux'

data_out = renacom_out.append(renaloc_out).append(osm_out).append(bureaux_out)

data_out.to_csv("~/data/niger_election_data/processed/pooled_data.csv")


import os
import librosa
import torch
import numpy as np

# Load the SERNet model
model = torch.hub.load('pyannote/pyannote-audio', 'SER_0')

# Define a list of emotion labels
emotions = ['neutral', 'calm', 'happy', 'sad', 'angry', 'fearful', 'disgust', 'surprised']

# Define the path to the directory containing the audio files
data_dir = "/Users/amitosi/PycharmProjects/chester/udi/emotion"

# Define empty lists to store the emotion labels and probabilities
emotion_labels = []
emotion_probs = []

# Set the model to evaluation mode
model.eval()

# Disable gradient calculation to speed up inference
torch.set_grad_enabled(False)

# Iterate through the audio files in the directory
for file in os.listdir(data_dir):
    try:
        # Load the audio data and resample it to 16 kHz (the same sample rate used during training)
        filepath = os.path.join(data_dir, file)
        audio_dat, sample_rate = librosa.load(filepath, sr=16000)
        audio_dat = librosa.resample(audio_dat, sample_rate, 16000)

        # Extract the features from the audio data using the SERNet model
        features = model({'audio': audio_dat})

        # Compute the probability distribution over the emotion classes
        probs = torch.nn.functional.softmax(features['log_likelihood']).numpy()[0]

        # Choose the emotion with the highest probability as the predicted emotion
        pred_emotion = emotions[np.argmax(probs)]

        # Append the predicted emotion label and probability to the respective lists
        emotion_labels.append(pred_emotion)
        emotion_probs.append(probs)
    except Exception as e:
        print(f"Error encountered while parsing {file}: {e}")


# coding: utf-8
import sys
sys.path.append('../')
import json
import gzip
import time
import datetime
import logging
from Models import reports
from AdApi.reports import Reports
from Config.api_config import report_type, account
# from Timers.timers_handler import TimersHandler

formatter = logging.Formatter("%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s")
file_name = '/home/develop/logs/adrp_logs/{}.log'.format(datetime.date.today())
log = logging.getLogger()
log.setLevel(logging.INFO)

fh = logging.FileHandler(file_name, mode='a+')
fh.setLevel(logging.DEBUG)
fh.setFormatter(formatter)
log.addHandler(fh)

ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(formatter)
log.addHandler(ch)

class DownloadReports:
    """
    定时批量下载报告
    """
    # time_fmt = '%Y-%m-%d %H:%M:%S'

    def get_report_country_for_date(self, table_name, report_date):
        session = reports.DBSession()
        report_excute = eval('reports.{}'.format(table_name))
        mkps = session.query(report_excute, report_excute.Country)\
                      .filter_by(SnapDate=report_date).all()
        mkps = set([(m[1]) for m in mkps])
        return list(mkps)

    def get_report_date(self, table_name):
        session = reports.DBSession()
        report_excute = eval('reports.{}'.format(table_name))
        snap_report = session.query(report_excute, report_excute.SnapDate).all()
        snap_date = set([''.join(str(d[1]).split('-')) for d in snap_report])
        return list(snap_date)

    def del_reports_for_date(self, table_name, snap_date, country):
        session = reports.DBSession()
        report_excute = eval('reports.{}'.format(table_name))
        records = session.query(report_excute).filter_by(SnapDate=snap_date,
                                                         Country=country).all()
        for record in records:
            session.delete(record)
        session.commit()

    def add_report_to_sql(self, json_b, table_name, country, params):
        report_json = json.loads(json_b.decode())
        snap_date = params.get('reportDate')
        report_excute = 'reports.{}'.format(table_name)

        session = reports.DBSession()
        for report in report_json:
            report_to_sql = eval(report_excute)(snap_date, country, report)
            session.add(report_to_sql)
        session.commit()

    def gen_reports(self, client, params):
        reports = client.create_report(params)
        # token失效时，刷新token
        if 'UNAUTHORIZED' in reports.text:
            client.do_refresh_token()
            reports = client.create_report(params)
        log.info('POST_RESP: %s', reports.text)
        return reports

    def download_report(self, client, params):
        reports = self.gen_reports(client, params)
        reports_dict = dict(json.loads(reports.text))
        report_id = reports_dict.get('reportId', None)
        if report_id:
            time.sleep(15)                          # 下载报告前，需要报告生成成功
            report = client.get_report(report_id)

            try:
                report = gzip.decompress(report.content)  # 解压缩
                log.info('GET Success!')
            except Exception as e:
                log.error('Decompress Error: %s', e)
            return report

    def report_to_sql(self, client, params):
        record_t = params.get('record_type')
        table_name = 'Apr' + params.get('spon').capitalize() + record_t[0].upper() + record_t[1:]
        country = params.get('mkp')
        report_date = params.get('reportDate')
        # 删除旧数据
        snap_dates = self.get_report_date(table_name)
        mkps = self.get_report_country_for_date(table_name, report_date)
        if (report_date in snap_dates) and (country in mkps):
            try:
                log.info('Delete {} old data...'.format(country))
                self.del_reports_for_date(table_name, report_date, country)
            except Exception as e:
                log.error('DeleteSqlError: %s', e)
        # 添加新数据
        report_byte = self.download_report(client, params)
        try:
            self.add_report_to_sql(report_byte, table_name, country, params)
        except Exception as e:
            log.error('AddSqlError: %s', e)

    def batch_download_reports(self, client, params):
        for rp_type in report_type.get('type'):      # sp or hsa
            params['spon'] = rp_type
            rc_type_list = report_type.get(rp_type)
            for rc_type in rc_type_list:              # reportType
                params['record_type'] = rc_type
                self.report_to_sql(client, params)

    def run(self, client, params):
        log.info('Report download start...')
        log.info('Report Marketplace: %s', params.get('mkp'))
        client.do_refresh_token()
        interval_day = 2
        while interval_day <= 32:    # 更新前30天内报告数据(由于订单的退换货原因每天数据不一致)
            if interval_day < 9 or interval_day == 32:     # 只更新前7天和第前30天数据
                report_date = datetime.datetime.now()
                report_date -= datetime.timedelta(days=interval_day)
                report_date = report_date.strftime('%Y%m%d')
                log.info('Report Date: %s', report_date)
                params['reportDate'] = str(report_date)
                self.batch_download_reports(client, params)
            interval_day += 1
        log.info('Marketplace: %s', params.get('mkp'))
        log.info('Report download end!')

if __name__ == '__main__':

    client_id = account.get('client_id')
    client_secret = account.get('client_secret')
    access_token = account.get('access_token')
    refresh_token = account.get('refresh_token')
    rp_scope = report_type.get('rp_scope')

    dw_report = DownloadReports()
    for scope in rp_scope:  # marketplace
        params = {}
        params['mkp'] = scope[:2]
        params['scope'] = scope[2:]
        report_client = Reports(client_id, client_secret, access_token, refresh_token, params['scope'])
        dw_report.run(report_client, params)

    # timer = datetime.datetime(2019, 5, 8, 16, 15)
    # dw_report_timer = TimersHandler(datetime.datetime.now() + datetime.timedelta(minutes=1), dw_report.run, args)
    # dw_report_timer.excute_job()


import asyncio
import atexit
import logging
from datetime import datetime, timedelta
from time import monotonic
from typing import Dict, List, Optional, Tuple, Union

import backoff
import typedload
from aiohttp.client_exceptions import ClientError
from aiohttp.web_exceptions import HTTPClientError, HTTPServerError
from gql import Client
from gql.client import AsyncClientSession, ReconnectingAsyncClientSession
from gql.transport.aiohttp import AIOHTTPTransport
from gql.transport.aiohttp import log as aiohttp_logger
from gql.transport.exceptions import TransportError, TransportQueryError
from gql.transport.websockets import WebsocketsTransport
from gql.transport.websockets import log as websockets_logger
from graphql import DocumentNode
from typedload.exceptions import TypedloadException
from websockets.exceptions import ConnectionClosedError

from betwatch.__about__ import __version__
from betwatch.queries import (
    MUTATION_UPDATE_USER_EVENT_DATA,
    QUERY_GET_LAST_SUCCESSFUL_PRICE_UPDATE,
    SUBSCRIPTION_BETFAIR_UPDATES,
    SUBSCRIPTION_RACES_UPDATES,
    query_get_race,
    query_get_races,
    subscription_race_price_updates,
)
from betwatch.types import (
    BetfairMarket,
    Bookmaker,
    BookmakerMarket,
    Race,
    RaceProjection,
    RaceUpdate,
    SubscriptionUpdate,
)
from betwatch.types.exceptions import NotEntitledError
from betwatch.types.filters import RacesFilter
from betwatch.types.updates import SelectionData

class BetwatchAsyncClient:
    def __init__(
        self,
        api_key: str,
        transport_logging_level: int = logging.WARNING,
        host="api.betwatch.com",
    ):
        self.api_key = api_key

        self._gql_sub_transport: WebsocketsTransport
        self._gql_transport: AIOHTTPTransport
        self._gql_sub_client: Client
        self._gql_client: Client

        self._host = host

        self.connect()

        self._http_session: Union[
            None, ReconnectingAsyncClientSession, AsyncClientSession
        ] = None

        # flag to indicate if we have entered the context manager
        self._websocket_session: Union[
            None, ReconnectingAsyncClientSession, AsyncClientSession
        ] = None

        websockets_logger.setLevel(transport_logging_level)
        aiohttp_logger.setLevel(transport_logging_level)

        # register the cleanup function to be called on exit
        atexit.register(self.__exit)

        # lock to prevent multiple sessions being created
        self._session_lock = asyncio.Lock()

        self._subscription_queue: asyncio.Queue[SubscriptionUpdate] = asyncio.Queue()
        self._subscriptions_betfair: Dict[str, asyncio.Task] = {}
        self._subscriptions_prices: Dict[str, asyncio.Task] = {}
        self._subscriptions_updates: Dict[Tuple[str, str], asyncio.Task] = {}

        self._monitor_task: Union[asyncio.Task, None] = None
        self._last_reconnect: float = monotonic()

    def connect(self):
        logging.debug("connecting to client sessions")
        self._gql_sub_transport = WebsocketsTransport(
            url=f"wss://{self._host}/sub",
            headers={
                "X-API-KEY": self.api_key,
                "User-Agent": f"betwatch-sdk-python-{__version__}",
            },
            init_payload={"apiKey": self.api_key},
            pong_timeout=60,
            ping_interval=5,
        )
        self._gql_transport = AIOHTTPTransport(
            url=f"https://{self._host}/query",
            headers={
                "X-API-KEY": self.api_key,
                "User-Agent": f"betwatch-sdk-python-{__version__}",
            },
        )
        # Create a GraphQL client using the defined transport
        self._gql_sub_client = Client(
            transport=self._gql_sub_transport,
            execute_timeout=60,
        )
        self._gql_client = Client(
            transport=self._gql_transport,
            execute_timeout=60,
        )
        logging.debug("connected to client sessions")

    async def disconnect(self):
        """Disconnect from the websocket connection."""
        logging.debug("disconnecting from client sessions")
        await self.__cleanup()
        logging.debug("disconnected from client sessions")

    async def __cleanup(self):
        """Gracefully close clients."""
        async with self._session_lock:
            try:
                self._http_session = None
                await self._gql_client.close_async()
            except Exception:
                pass
            try:
                self._websocket_session = None
                await self._gql_sub_client.close_async()
            except Exception:
                pass

    def __exit(self):
        """Close the client."""
        logging.info("closing connection to Betwatch API (may take a few seconds)")
        asyncio.run(self.__cleanup())

    async def __aenter__(self):
        """Pass through to the underlying client's __aenter__ method."""
        logging.debug("entering context manager")
        async with self._session_lock:
            if not self._websocket_session:
                self._websocket_session = await self._gql_sub_client.connect_async(
                    reconnecting=True
                )
        logging.debug("entered context manager")
        return self._websocket_session

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Pass through to the underlying client's __aexit__ method."""
        logging.debug("exiting context manager")
        async with self._session_lock:
            if self._websocket_session:
                self._websocket_session = (
                    await self._websocket_session.client.close_async()
                )
        logging.debug("exited context manager")
        return self._websocket_session

    async def _setup_websocket_session(self):
        """Connect to websocket connection"""
        logging.debug("setting up websocket session")
        async with self._session_lock:
            if not self._websocket_session:
                self._websocket_session = await self._gql_sub_client.connect_async(
                    reconnecting=True
                )
        logging.debug("websocket session setup")
        return self._websocket_session

    async def _setup_http_session(self):
        """Setup the HTTP session."""
        async with self._session_lock:
            if not self._http_session:
                self._http_session = await self._gql_client.connect_async()
        return self._http_session

    async def get_races_today(
        self,
        projection: Optional[RaceProjection] = None,
        filter: Optional[RacesFilter] = None,
    ) -> List[Race]:
        """Get all races for today."""
        # set defaults
        if not projection:
            projection = RaceProjection()
        if not filter:
            filter = RacesFilter()

        today = datetime.today().strftime("%Y-%m-%d")
        tomorrow = (datetime.today() + timedelta(days=0)).strftime("%Y-%m-%d")
        return await self.get_races_between_dates(
            today, tomorrow, projection=projection, filter=filter
        )

    async def get_races_between_dates(
        self,
        date_from: Union[str, datetime],
        date_to: Union[str, datetime],
        projection: Optional[RaceProjection] = None,
        filter: Optional[RacesFilter] = None,
    ) -> List[Race]:
        """Get a list of races in between two dates.

        Args:
            date_from (Union[str, datetime]): Date to start from (inclusive)
            date_to (Union[str, datetime]): Date to end at (inclusive   )
            projection (_type_, optional): The fields to return. Defaults to RaceProjection().
            filter (_type_, optional): The filter to apply. Defaults to RacesFilter().

        Returns:
            List[Race]: List of races that match the criteria
        """
        # set defaults
        if not projection:
            projection = RaceProjection()
        if not filter:
            filter = RacesFilter()

        if isinstance(date_from, datetime):
            date_from = date_from.strftime("%Y-%m-%d")
        if isinstance(date_to, datetime):
            date_to = date_to.strftime("%Y-%m-%d")

        # prefer the date_from and date_to passed into the function
        if filter.date_from and filter.date_from != date_from:
            logging.debug(
                f"Overriding date_from in filter ({filter.date_from} with {date_from})"
            )
            filter.date_from = date_from
        if filter.date_to and filter.date_to != date_to:
            logging.debug(
                f"Overriding date_to in filter ({filter.date_to} with {date_to})"
            )
            filter.date_to = date_to
        return await self.get_races(projection, filter)

    @backoff.on_exception(backoff.expo, (ClientError, HTTPClientError, HTTPServerError))
    async def get_races(
        self,
        projection: Optional[RaceProjection] = None,
        filter: Optional[RacesFilter] = None,
        json: bool = False,
    ) -> List[Race] or List[Dict]:
        # set defaults
        if not projection:
            projection = RaceProjection()
        if not filter:
            filter = RacesFilter()
        try:
            logging.info(
                f"Getting races with projection {projection} and filter {filter}"
            )

            done = False
            races: List[Race] = []

            # iterate until no more races are found
            while not done:
                session = await self._setup_http_session()

                query = query_get_races(projection)

                variables = filter.to_dict()

                result = await session.execute(query, variable_values=variables)

                if result.get("races"):
                    logging.info(
                        f"Received {len(result['races'])} races - attempting to get more..."
                    )

                    if not json:
                        races.extend(typedload.load(result["races"], List[Race]))
                    else:
                        races.extend(result["races"])

                    # change the offset to the next page
                    filter.offset += filter.limit

                else:
                    filter.offset = 0
                    logging.debug("No more races found")
                    done = True

            return races
        except TypedloadException as e:
            logging.error(f"Error parsing Betwatch API response: {e}")
            raise e
        except (ClientError, HTTPClientError, HTTPServerError, TimeoutError) as e:
            logging.warning(f"Error reaching Betwatch API: {e}")
            raise e
        except TransportQueryError as e:
            if e.errors:
                for error in e.errors:
                    msg = error.get("message")
                    if msg:
                        # sometimes we can provide better feedback
                        if "limit argument" in msg:
                            # adjust the limit and try again

                            filter.limit = int(
                                msg.split("limit argument less than")[1].strip()
                            )
                            logging.info(
                                f"Cannot query more than {filter.limit} - adjusting limit to {filter.limit} and trying again"
                            )
                            return await self.get_races(projection, filter)
                        else:
                            logging.error(f"{error}")
                    else:
                        logging.error(f"{error}")
            else:
                logging.error(f"Error querying Betwatch API: {e}")
            return []

    async def get_race(
        self,
        race_id: str,
        projection: Optional[RaceProjection] = None,
        json: bool = False,
    ) -> Union[Race, None] or Union[Dict, None]:
        """Get all details of a specific race by id.

        Args:
            race_id (str): The id of a race. This can be obtained from the `get_races` method.
            projection (RaceProjection, optional): The fields to return. Defaults to RaceProjection(markets=True).

        Returns:
            Union[Race, None]: The race object or None if the race is not found.
        """
        # set defaults
        if not projection:
            projection = RaceProjection(markets=True)
        query = query_get_race(projection)
        return await self._get_race_by_id(race_id, query)

    async def _monitor(self):
        """Monitor the subscription tasks and restart them if they fail"""
        logging.debug("Starting subscription monitor")
        while True:
            try:
                await asyncio.sleep(1)
                for d in [
                    self._subscriptions_prices,
                    self._subscriptions_updates,
                    self._subscriptions_betfair,
                ]:
                    for key, task in d.items():
                        if task.done():
                            # check for errors
                            try:
                                err = task.exception()
                                if err:
                                    logging.warning(f"Subscription task closed: {err}")
                                    # reset the connection
                                    if self._websocket_session:
                                        await self.disconnect()

                                    # if the user is not entitled to the data, don't retry
                                    # check if err is of type NotEntitledError
                                    if isinstance(err, NotEntitledError):
                                        logging.warning(
                                            "You are not entitled to subscriptions. Please contact api@betwatch.com to upgrade your API key."
                                        )
                                        del d[key]
                                        continue

                            except asyncio.InvalidStateError:
                                pass

                            logging.warning(
                                f"Retrying subscription task for {key if key else 'all races'}"
                            )

                            # replace the task in the dict with a new one
                            if d == self._subscriptions_prices:
                                d[key] = asyncio.create_task(
                                    self._subscribe_bookmaker_updates(key)
                                )
                            elif d == self._subscriptions_updates:
                                d[key] = asyncio.create_task(
                                    self._subscribe_race_updates(*key)
                                )
                            elif d == self._subscriptions_betfair:
                                d[key] = asyncio.create_task(
                                    self._subscribe_betfair_updates(key)
                                )

                            # update last reconnect
                            self._last_reconnect = monotonic()
            except asyncio.CancelledError:
                logging.debug("Subscription monitor cancelled")
                return
            except Exception as e:
                logging.debug(f"Error in subscription monitor: {e}")
                raise e

    async def listen(self):
        """Subscribe to any updates from your subscriptions"""
        if (
            len(self._subscriptions_prices) < 1
            and len(self._subscriptions_betfair) < 1
            and len(self._subscriptions_updates) < 1
        ):
            raise Exception("You must subscribe to a race before listening for updates")

        # dont spam the user with warnings of queue size
        last_warning = datetime.now()

        # start monitor
        self._monitor_task = asyncio.create_task(self._monitor())

        while True:
            try:
                logging.debug("Waiting for subscription update")
                update = await self._subscription_queue.get()
                self._subscription_queue.task_done()
                yield update
                logging.debug("Subscription update received")

                # check if we are falling behind
                if (
                    self._subscription_queue.qsize() > 25
                    and (datetime.now() - last_warning).seconds > 10
                ):
                    logging.warning(
                        f"Subscription queue is {self._subscription_queue.qsize()} items behind"
                    )
                    last_warning = datetime.now()
            except asyncio.CancelledError:
                logging.info("Subscription queue cancelled")
                return

    def get_subscribed_race_ids(self) -> List[str]:
        """Get a list of all subscribed races"""
        unique = list(self._subscriptions_prices.keys()) + list(
            self._subscriptions_betfair.keys()
        )
        return list(set(unique)) if unique else []

    async def unsubscribe_race(self, race_id: str):
        await self.unsubscribe_betfair_updates(race_id)
        await self.unsubscribe_bookmaker_updates(race_id)

    async def unsubscribe_bookmaker_updates(self, race_id: str):
        if race_id not in self._subscriptions_prices:
            logging.info(
                f"Not subscribed to {race_id if race_id else 'all races'} bookmaker updates"
            )
            return

        self._subscriptions_prices[race_id].cancel()
        del self._subscriptions_prices[race_id]
        logging.info(
            f"Unsubscribed from {race_id if race_id else 'all races'} bookmaker updates"
        )

    async def subscribe_bookmaker_updates(
        self,
        race_id: str,
        projection: Optional[RaceProjection] = None,
    ):
        # set defaults
        if not projection:
            projection = RaceProjection(markets=True)

        if race_id in self._subscriptions_prices:
            logging.info(
                f"Already subscribed to {race_id if race_id else 'all races'} bookmaker updates"
            )
            return

        # make sure the total subscriptions is less than 10
        if len(self._subscriptions_prices) >= 10:
            raise Exception(
                "Cannot subscribe to more than 10 races at one time. Use an empty race_id to subscribe to all races in one subscription"
            )

        self._subscriptions_prices[race_id] = asyncio.create_task(
            self._subscribe_bookmaker_updates(race_id, projection)
        )

    async def _subscribe_bookmaker_updates(
        self,
        race_id: str,
        projection: Optional[RaceProjection] = None,
    ):
        """Subscribe to price updates for a specific race.

        Args:
            race_id (str): The id of a specific race. This can be obtained from the `get_races` method.

        Yields:
            List[BookmakerMarket]: A list of bookmaker markets with updated prices.
        """
        # set defaults
        if not projection:
            projection = RaceProjection(markets=True)

        try:
            session = await self._setup_websocket_session()

            query = subscription_race_price_updates(projection)
            variables = {"id": race_id}

            logging.info(
                f"Subscribing to bookmaker updates for {race_id if race_id else 'all races'}"
            )

            async for result in session.subscribe(query, variable_values=variables):
                if result.get("priceUpdates"):
                    update = SubscriptionUpdate(
                        race_id=race_id,
                        bookmaker_markets=typedload.load(
                            result["priceUpdates"], List[BookmakerMarket]
                        ),
                    )

                    self._subscription_queue.put_nowait(update)
        except TransportError as e:
            logging.debug(f"Error subscribing to bookmaker updates: {e}")

            # check if the user is entitled to this data
            if "does not have access" in e.args[0]:
                raise NotEntitledError(
                    "API key is not entitled to websocket subscriptions"
                ) from e
        except asyncio.CancelledError:
            await self.unsubscribe_bookmaker_updates(race_id)
            return
        except ConnectionClosedError as e:
            logging.debug(f"Error on bookmaker prices subscription: {e}")
        except Exception as e:
            logging.debug(f"Error subscribing to bookmaker updates: {e}")

    async def unsubscribe_betfair_updates(self, race_id: str):
        if race_id not in self._subscriptions_betfair:
            logging.info(
                f"Not subscribed to {race_id if race_id else 'all races'} betfair updates"
            )
            return

        self._subscriptions_betfair[race_id].cancel()
        del self._subscriptions_betfair[race_id]
        logging.info(
            f"Unsubscribed from {race_id if race_id else 'all races'} betfair updates"
        )

    async def subscribe_betfair_updates(self, race_id: str):
        if race_id in self._subscriptions_betfair:
            logging.info(
                f"Already subscribed to {race_id if race_id else 'all races'} betfair updates"
            )
            return

        # make sure the total subscriptions is less than 10
        if len(self._subscriptions_betfair) >= 10:
            raise Exception(
                "Cannot subscribe to more than 10 races at one time. Use an empty race_id to subscribe to all races in one subscription"
            )

        self._subscriptions_betfair[race_id] = asyncio.create_task(
            self._subscribe_betfair_updates(race_id)
        )

    async def _subscribe_betfair_updates(
        self,
        race_id: str,
    ):
        """Subscribe to betfair price updates for a specific race.

        Args:
            race_id (str): The id of a specific race. This can be obtained from the `get_races` method.

        Yields:
            List[BetfairMarket]: A list of betfair markets with updated prices.
        """
        try:
            session = await self._setup_websocket_session()

            query = SUBSCRIPTION_BETFAIR_UPDATES
            variables = {"id": race_id}

            logging.info(
                f"Subscribing to betfair updates for {race_id if race_id else 'all races'}"
            )

            async for result in session.subscribe(query, variable_values=variables):
                if result.get("betfairUpdates"):
                    update = SubscriptionUpdate(
                        race_id=race_id,
                        betfair_markets=typedload.load(
                            result["betfairUpdates"], List[BetfairMarket]
                        ),
                    )
                    self._subscription_queue.put_nowait(update)

        except TransportError as e:
            logging.debug(f"Error subscribing to betfair updates: {e}")

            # check if the user is entitled to this data
            if "does not have access" in e.args[0]:
                raise NotEntitledError(
                    "API key is not entitled to websocket subscriptions"
                ) from e
        except asyncio.CancelledError:
            await self.unsubscribe_betfair_updates(race_id)
            return
        except ConnectionClosedError as e:
            logging.debug(f"Error on betfair subscription: {e}")

    async def unsubscribe_race_updates(self, date_from: str, date_to: str):
        if (date_from, date_to) not in self._subscriptions_updates:
            logging.info(f"Not subscribed to races updates for {date_from} - {date_to}")
            return

        self._subscriptions_updates[(date_from, date_to)].cancel()
        del self._subscriptions_updates[(date_from, date_to)]
        logging.info(f"Unsubscribed from races updates for {date_from} - {date_to}")

    async def subscribe_race_updates(self, date_from: str, date_to: str):
        if (date_from, date_to) in self._subscriptions_updates:
            logging.info(
                f"Already subscribed to races updates for {date_from} - {date_to}"
            )
            return

        self._subscriptions_updates[(date_from, date_to)] = asyncio.create_task(
            self._subscribe_race_updates(date_from, date_to)
        )

    async def _subscribe_race_updates(self, date_from: str, date_to: str):
        try:
            session = await self._setup_websocket_session()

            query = SUBSCRIPTION_RACES_UPDATES
            variables = {"dateFrom": date_from, "dateTo": date_to}

            logging.debug(f"Subscribing to race updates for {date_from} - {date_to}")

            async for result in session.subscribe(query, variable_values=variables):
                if result.get("racesUpdates"):
                    ru = typedload.load(result["racesUpdates"], RaceUpdate)
                    update = SubscriptionUpdate(
                        race_id=ru.id,
                        race_update=ru,
                    )
                    self._subscription_queue.put_nowait(update)

        except TransportError as e:
            logging.debug(f"Error subscribing to race updates: {e}")
        except asyncio.CancelledError:
            await self.unsubscribe_race_updates(date_from, date_to)
            return
        except ConnectionClosedError as e:
            logging.debug(f"Error on race updates subscription: {e}")

    @backoff.on_exception(backoff.expo, Exception, max_time=60, max_tries=5)
    async def _get_race_by_id(
        self,
        race_id: str,
        query: DocumentNode,
        json: bool = False,
    ) -> Union[Race, None] or Union[Dict, None]:
        logging.info(f"Getting race (id={race_id})")
        session = await self._setup_http_session()
        variables = {
            "id": race_id,
        }

        result = await session.execute(query, variable_values=variables)

        if result.get("race") and not json:
            return typedload.load(result["race"], Race)
        elif json:
            return result["race"]
        return None

    async def update_event_data(
        self, race_id: str, column_name: str, data: List[SelectionData]
    ):
        """
        Updates event data for a given race ID.

        Args:
            race_id (str): race id to be checked
            column_name (str): name of the column to be updated
            data (List[SelectionData]): list of selection data to be updated
        """

        logging.info(f"Updating event data (id={race_id})")
        session = await self._setup_http_session()
        selection_data = [
            {"selectionId": d["selection_id"], "value": str(d["value"])} for d in data
        ]

        if not selection_data:
            raise ValueError("Cannot update event data with empty selection data")

        res = await session.execute(
            MUTATION_UPDATE_USER_EVENT_DATA,
            variable_values={
                "input": {
                    "eventId": race_id,
                    "customData": [
                        {"columnName": column_name, "selectionData": selection_data}
                    ],
                }
            },
        )
        logging.debug(res)

    async def get_race_last_updated_times(
        self, race_id: str
    ) -> Dict[Bookmaker, datetime]:
        """Get the last time each bookmaker was checked for a price update.
           This does not mean that the price was updated, just that the bookmaker was checked.

        Args:
            race_id (str): race id to be checked

        Returns:
            Dict[str, datetime]: dictionary with bookmaker name as key and datetime as value
        """
        race = await self._get_race_by_id(
            race_id, QUERY_GET_LAST_SUCCESSFUL_PRICE_UPDATE
        )

        if isinstance(race, Race) and race.links:
            return {
                link.bookmaker: link.last_successful_price_update
                for link in race.links
                if link.bookmaker and link.last_successful_price_update
            }
        elif isinstance(race, Dict) and "links" in race:
            return {
                link["bookmaker"]: link["last_successful_price_update"]
                for link in race["links"]
                if link["bookmaker"] and link["last_successful_price_update"]
            }

        return {}