class FiniteField(Field):

    def __init__(self, p, k, f):
        self.p, self.k, self.f = p, k, f
        self.field = PolynomialsOverField(Zp(p))

    def cardinality(self):
        return self.p ** self.k

    def div_mod(self, elem1, elem2):

        e1 = copy.deepcopy(elem1)
        e2 = copy.deepcopy(elem2)

        # make both polynomials of degree lower than f's
        e1 = self.rep(e1)
        e2 = self.rep(e2)

        # make sure that elem1 has a higher degree than elem2's.
        # note that adding f is essentially no different from adding 0 in this field.
        # e1 = self.field.add(self.f, e1)

        # result = self.field.div_mod(e1, e2)
        result = self.mul(e1, self.mul_inv(e2))
        # make sure result is returned in proper representation.
        return self.rep(result), self.add_id()

    def add_id(self):
        return Polynomial([0], self.field)

    def mul_id(self):
        return Polynomial([1], self.field)

    def add_inv(self, elem):
        return self.rep(self.field.add_inv(elem))

    def mul_inv(self, elem):
        res = self.field.extended_gcd(elem, self.f)
        return res[1]

    def add(self, elem1, elem2):
        return self.rep(self.field.add(elem1, elem2))

    def mul(self, elem1, elem2):
        return self.rep(self.field.mul(elem1, elem2))

    # elem's equivalence class
    def rep(self, elem):
        return self.field.div_mod(elem, self.f)[1]

    def random_elem(self):
        elem = []
        for i in range(self.k):
            elem.append(random.randrange(self.p))
        return Polynomial(elem, Zp(self.p))

    def __eq__(self, other):
        if isinstance(other, FiniteField):
            return self.p == other.p and self.k == other.k
        return NotImplemented
class CantorZassenhaus:

    def __init__(self, f):
        # field = Fq, f = f, fx = F[X]
        self.field = f.base_ring
        self.f = f
        self.fx = PolynomialsOverField(f.base_ring)
        self._l = f.degree()

    def distinct_degree_factorization(self):
        _L = []
        _X = Polynomial([self.field.add_id(), self.field.mul_id()], self.field)
        h = self.fx.rem(_X, self.f)
        k = 0
        fc = copy.deepcopy(self.f)

        while fc != self.fx.mul_id():
            h = self.fx.rem(self.fx.exp(h, self.field.cardinality()), fc)
            k += 1
            aux = self.fx.add(h, self.fx.add_inv(_X))
            g = self.fx.gcd(aux, fc)
            if g.degree() > 1:
                _L.append((g, k))
                fc = self.fx.div(fc, g)
                h = self.fx.rem(h, fc)
        return _L

    # generate a random polynomial in F[X]/(h)
    def random_polynomial_h(self, h):
        res = []
        for i in range(h.degree()):
            res.append(self.field.random_elem())
        pol = Polynomial(res, self.field)
        return self.fx.rem(pol, h)

    def mk(self, k):
        w = self.field.k
        res = [self.field.add_id()] * (2 ** (w * k - 1) + 1)
        acc = 1
        for i in range(w * k):
            res[acc] = self.field.mul_id()
            acc *= 2
        return Polynomial(res, self.field)

    # f is a monic polynomial of degree l and k the number of times
    def equal_degree_factorization(self, pol, k):
        mk = self.mk(k)
        fc = copy.deepcopy(pol)
        r = pol.degree() // k
        # print("r: ", r)
        _H = [fc]
        # print("_H: ", _H)
        while len(_H) < r:
            _Hp = []
            for elem in _H:
                alpha = self.random_polynomial_h(elem)
                eval_mk = self.fx.evaluate_polynomial(mk, alpha)
                eval_mk = self.fx.rem(eval_mk, fc)
                d = self.fx.gcd(eval_mk, elem)
                if d.degree() == 0 or d == elem:
                    _Hp.append(elem)
                else:
                    # print("d: ", d)
                    # print("elem: ", elem)
                    _Hp.append(d)
                    # print("Div :", self.fx.div(elem, d))
                    _Hp.append(self.fx.div(elem, d))
            _H = _Hp
        return _H

    def compute(self):
        step1 = self.distinct_degree_factorization()
        # print("Step 1 result: ", step1)
        # for pol in step1:
        #    print(self.fx.obtain_monic_representation(pol[0]))
        res = []
        for elem in step1:
            # print("elem for: ", elem)
            sol = self.equal_degree_factorization(elem[0], elem[1])
            # print(sol)
            res.extend(sol)
        return res