# -*- python -*-
import sys
import resource
from polybori.gbcore import *
from polybori.memusage import *
from time import time, clock
from polybori.PyPolyBoRi import *
from polybori.blocks import declare_ring, Block
from math import sqrt
from polybori.parallel import groebner_basis_first_finished
from optparse import OptionParser
from polybori.interred import interred


# Just for debugging
def print_matrix(A):
    res = ""
    for i in xrange(len(A)):
        for j in xrange(len(A[i])):
            res += str(A[i][j]) + " "
        res += "\n"
    return res


# TODO: Implement constructor to convert Polynomials to
# GeneralBooleanPolynomial (with e_1 + ... + e_k as coefficient)
class GeneralBooleanPolynomial:
    """
    Class to represent Boolean polynomials over F_2^k
    """

    def __init__(self, k, coeff, polynomial):
        """
        Construct a GeneralBooleanPolynomial given by coeff * polynomial
        Arguments:
           k :           Number of factors of F_2
           coeff :       Array containing natural numbers in {0, ..., k-1} representing an element of F_2^k as a set
           polynomial :  Polynomial
        """
        #print "type of polynomials", type(polynomial)
        #print "polynomials is int?", isinstance(polynomial, int)
        assert isinstance(polynomial, Polynomial) or isinstance(polynomial,
            Monomial) or isinstance(polynomial, Variable) or isinstance(
            polynomial, int)
        self.polys = []
        self.k = k
        self.ring = polynomial.ring()
        for i in xrange(k):
            if i in coeff:
                self.polys.append(polynomial)
            else:
                self.polys.append(Polynomial(0, self.ring))

    def __len__(self):
        """
        Returns the number of factors k in the product of the underlying ring F_2^k
        """
        return self.k

    def __getitem__(self, k):
        """
        Return the k-th component (i.e. the projection to the k-th factor)
        """
        return self.polys[k]

    def __setitem__(self, k, value):
        """
        Sets the k-th component (i.e. the projection to the k-th factor)
        """
        self.polys[k] = value

    def __eq__(self, other):
        """
        Tests equality by testing that
         - both objects are defined over the same ring (i.e. the number of factors is the same)
         - the objects are equal in each component
        """
        if not len(self) == len(other):
            return False
        for i in xrange(len(self)):
            if self[i] != other[i]:
                return False
        return True

    def __ne__(self, other):
        return not self == other

    def __str__(self):
        """
        Returns a representation of the polynomial as string
        """
        res = ""
        # Build a list of all terms occurring
        terms = set([])
        for i in xrange(self.k):
            if not isinstance(self.polys[i], Polynomial):
                assert isinstance(self.polys[i], Monomial) or isinstance(self.
                    polys[i], Variable)
                self[i] = Polynomial(self[i])
            terms = terms.union(set(self[i].terms()))
        # Sort the terms
        terms = list(terms)
        terms.sort()
        # Determine the coefficient for each term and build up the string
        for t in terms:
            comps = [j for j in xrange(self.k) if t in set(Polynomial(self[j])
                .terms())]
            if len(comps) == self.k:
                # We use this simplified notation of the coefficient it 1
                res += str(t) + " + "
            else:
                res += str(comps) + " * " + str(t) + " + "
        res = res[0:len(res) - 3]
        if res == "":
            return "0"
        else:
            return res

    def __add__(self, other):
        """
        Addition of two GeneralBooleanPolynomial
        """
        if not len(self) == len(other):
            print "Cannot add polynomials defined over different rings"
            print "Len(self)=", len(self)
            print "Len(other)=", len(other)
            assert len(self) == len(other)
        sum = GeneralBooleanPolynomial(self.k, [], Polynomial(0, self.ring))
        for i in xrange(self.k):
            sum[i] = self[i] + other[i]
        return sum

    def __mul__(self, other):
        """
        Multiplication of two GeneralBooleanPolynomial
        """
        if not len(self) == len(other):
            print "Cannot multiply polynomials defined over different rings"
            print "Len(self)=", len(self)
            print "Len(other)=", len(other)
            assert len(self) == len(other)
        prod = GeneralBooleanPolynomial(self.k, [], Polynomial(0, self.ring))
        for i in xrange(self.k):
            prod[i] = Polynomial(self[i]) * Polynomial(other[i])
        return prod

    def __sub__(self, other):
        """
        Subtraction of two GeneralBooleanPolynomial
        """
        if not len(self) == len(other):
            print "Cannot subtract polynomials defined over different rings"
            print "Len(self)=", len(self)
            print "Len(other)=", len(other)
            assert len(self) == len(other)
        sub = GeneralBooleanPolynomial(self.k, [], Polynomial(1, self.ring))
        for i in xrange(self.k):
            sub[i] = self[i] - other[i]
        return sub

    def lc(self):
        """
        Returns leading coefficient as constant GeneralBooleanPolynomial
        """
        return GeneralBooleanPolynomial(self.k, self.lc_as_set(),
                        Polynomial(1, self.ring))

    def lc_as_set_array(self):
        """
        Returns leading coefficient as array containing the indices of the
        non-zero components of the leading coefficient.
        """
        max_term = 1
        for i in xrange(len(self)):
            if not self[i].is_zero():
                if self[i].lead() > max_term:
                    max_term = self[i].lead()
        comps = [j for j in xrange(len(self)) if max_term
                 in set(self[j].terms())]
        return comps

    def lc_as_set(self):
        """
        Returns leading coefficient as set containing the indices of the
        non-zero components of the leading coefficient.
        """
        return set(self.lc_as_set_array())

    def lc_binary(self):
        """
        Returns leading coefficient as array containing the integers 0 or 1
        representing the leading coefficient in a binary form.
        """
        lc_set = self.lc_as_set()
        lc_binary = [0] * len(self)
        for i in xrange(len(self)):
            if i in lc_set:
                lc_binary[i] = 1
        return lc_binary

    def lt(self):
        """
        Leading term in form of a GeneralBooleanPolynomial
        """
        max_term = 1
        for i in xrange(self.k):
            if not self[i].is_zero():
                if self[i].lead() > max_term:
                    max_term = self[i].lead()
        comps = [j for j in xrange(self.k) if max_term in set(self[j].terms())]
        return GeneralBooleanPolynomial(self.k, comps, max_term)

    def lm(self):
        """
        Leading monomial in form of a GeneralBooleanPolynomial
        """
        max_term = 1
        contains_non_zero_term = False
        for i in xrange(len(self)):
            if not self[i].is_zero():
                contains_non_zero_term = True
                if self[i].lead() > max_term:
                    max_term = self[i].lead()
        if not contains_non_zero_term:
            raise ValueError("lm of zero polynomial is not defined")
        return GeneralBooleanPolynomial(self.k, [i for i in xrange(self.k)],
            max_term)

    def constant_part_binary(self):
        """
        Constant part as binary tuple indicading which coefficients are non-zero
        """
        comps = []
        for i in xrange(len(self)):
            if self[i].has_constant_part():
                comps.append(1)
            else:
                comps.append(0)
        return comps

    def constant_part_as_set_array(self):
        """
        Constant part as array containing the indices of the non-zero coefficients of the constant part (sorted increasingly)
        """
        res = []
        for i in xrange(len(self)):
            if self[i].has_constant_part():
                res.append(i)
        return res

    def constant_part_as_set(self):
        """
        Constant part as set containing the indices of the non-zero coefficients of the constant part
        """
        return set(self.constant_part_as_set_array())

    def constant_part(self):
        """
        Constant part as GeneralBoolenPolynomial
        """
        res = GeneralBooleanPolynomial(len(self), [], Polynomial(0, self.ring))
        for i in xrange(len(self)):
            if self[i].has_constant_part():
                res[i] = Polynomial(1, self.ring)
            else:
                res[i] = Polynomial(0, self.ring)
        return res

    def to_expanded_polynomial_ring(self, new_variables):
        """
        Returns a representation in form of a Polynomial over a ring with
        additional variables, one for each factor in the product of fields
        F_2^k
        """
        assert self.k == len(new_variables)
        return sum(new_variables[i] * self.polys[i] for i in xrange(self.k))

    def is_monomial(self):
        """
        Test if self is a Monomial
        """
        # Build a list of all terms occurring
        terms = set([])
        for i in xrange(self.k):
            if not isinstance(self.polys[i], Polynomial):
                assert isinstance(self.polys[i], Monomial) or isinstance(self.
                    polys[i], Variable)
                self[i] = Polynomial(self[i])
            terms = terms.union(set(self[i].terms()))
        if len(terms) > 1:
            return False
        else:
            return True

    def is_zero(self):
        """
        Tests if self is zero
        """
        for i in xrange(len(self)):
            if self[i] != 0:
                return False
        return True

    def monomial(self):
        """
        Returns a PolyBoRi Monomial representing the leading monomial of self, where self should be a monomial
        """
        assert self.is_monomial()
        for i in xrange(self.k):
            if self.polys[i] != Polynomial(0, self.ring):
                return self.polys[i].lead()
        return Polynomial(0, self.ring)

    def divides(self, other):
        """
        Tests if self divides other
        """
        assert len(self) == len(other)
        assert (self.is_monomial() and other.is_monomial())
        self_divides_other = True
        for i in xrange(len(self)):
            if self[i] == 0:
                if other[i] != 0:
                    return False
                else:
                    continue
            if self[i] == 1:
                continue
            else:
                if other[i] == 1:
                    return False

            if other[i] == 0:
                continue

            assert other[i] == other[i].lead()
            other[i] = other[i].lead()
            if not other[i] in (Polynomial(self.polys[i]).lead()).divisors():
                return False
        return True


# Simple Gaus algorithm
# Should really be replaced by something faster (maybe from PolyBoRi)
def triangulate_over_F2(A, b):
    assert len(A) == len(b)
    n = len(b)
    m = len(A[0])
    print "n: ", n, "m: ", m
    print "A, b", A, b
    for i in xrange(0, min(n, m)):    # Row
        print "i", i
        if A[i][i] == 0:
            # permutate rows
            changed = False
            for l in range(i, n):
                if A[l][i] != 0:
                    for k in xrange(n):
                        A[l][k], A[i][k] = A[i][k], A[l][k]
                    b[l], b[i] = b[i], b[l]
                changed = True
            if not changed:
                return -1
        for j in xrange(0, i):
            if A[i][j] != 0:
                for k in xrange(j, n):
                    A[i][k] += A[i - 1][k]
                b[i] += b[i - 1]
    res_A = [[A[i][j] % 2 for j in xrange(m)] for i in xrange(n)]
    res_b = [b[i] % 2 for i in xrange(n)]
    return (res_A, res_b)


def projection_of_expanded_polynomial(f, e, e_vars):
    """
    Compute the projection of the expanded polynomial f to the component
    corresponding to the variable e (which is part of e_vars)
    """
    for v in e_vars:
        if v == e:
            # Substitute v by 1
            f = Polynomial(f.set().subset0(v.index())) + Polynomial(f.set().
                subset1(v.index()))
        else:
            # Substitute v by 0
            f = Polynomial(f.set().subset0(v.index()))
    return f


def expanded_polynomial2general_polynomial(polynomial, new_variables, ring):
    """
    Returns a GeneralBooleanPolynomial associated to a Polynomial (polynomial) in
    additional variables (new_variables), where the
    GeneralBooleanPolynomial in obtained from the projections of polynomial
    to the factors of F_2^k (by specialization of the additional variables)
    """
    comps = [projection_of_expanded_polynomial(polynomial, e, new_variables)
        for e in new_variables]
    sum2 = GeneralBooleanPolynomial(len(new_variables), [0] * len(
        new_variables), Polynomial(0, ring))
    for i in xrange(len(new_variables)):
        sum2[i] = comps[i]
    return sum2


def reduce_general_boolean_polynomial(F, polynomial):
    """
    Computes the reduction of polynomial via the ideal given by F
    """
    r = polynomial
    s = len(F)
    k = len(polynomial)
    h = [GeneralBooleanPolynomial(len(polynomial), [0] * len(polynomial),
        Polynomial(0, self.ring))] * s
    # Indices i where leading monomial of F[i] divided leading monomial of r

    def calc_Is():
        ret = []
        for i in xrange(s):
            if (F[i].is_zero() and not r.is_zero()):
                continue
            if (F[i].lm()).divides(r.lm()):
                ret.append(i)
        return ret

    def calc_X():
        ret = []
        for i in xrange(len(Is)):
            ret.append((r.lm()).monomial() / (F[Is[i]].lm()).monomial())
        return ret

    def included(i, set):
        if i in set:
            return 1
        else:
            return 0

    Is = calc_Is()
    X = calc_X()

    for f in F:
        assert len(f) == len(polynomial)

    lm_polynomial = (polynomial.lm()).monomial()
    lc_polynomial_binary = polynomial.lc_binary()

    while len(Is) is not 0:
        exp = [F[i].lm() for i in xrange(s)]
        matrix = [[included(i, F[j].lc_as_set()) for j in Is] for i in xrange(
            k)]

        # Compute solution
        coeff = [[0] * len(Is) for j in xrange(k)]
        for j in xrange(k):
            if lc_polynomial_binary[j]:
                coeff[j][0] = 1

        sum = GeneralBooleanPolynomial(k, [0] * k, Polynomial(0, self.ring))
        for i in xrange(len(Is)):
            c = [coeff[l][i] for l in xrange(k)]
            c_set = [l for l in xrange(k) if coeff[l][i] == 1]
            poly1 = GeneralBooleanPolynomial(k, c_set, X[i])

            sum += GeneralBooleanPolynomial(k, c_set, X[i]) * F[Is[i]].lt()

        if polynomial.lt() == sum:
            r -= sum
        else:
            break

        if r.is_zero():
            return r

        Is = calc_Is()
        X = calc_X()
    return r


def stratified(I):
    """
    Tests if I does no contain two polynomials with the same leading monomials
    """
    leading_monomials = []
    for p in I:
        if p.is_zero():
            continue
        lm = p.lm()
        if lm in leading_monomials:
            return False
        leading_monomials.append(lm)
    return True


def build_dict_from_array_of_extended_polynomials(A, e_vars):
    new_dict = {}
    for p in A:
        new_dict[p] = expanded_polynomial2general_polynomial(p, e_vars)
    return new_dict


def stratify_dict_I_gb_I(dict, e_vars, debug=0):
    """
    Wrapper (calls either stratify_dict_I_gb_I_our_alg or stratify_dict_I_gb_I_Inoue
    """
    #return stratify_dict_I_gb_I_Inoue(dict, e_vars, debug)
    return stratify_dict_I_gb_I_our_alg(dict, e_vars, debug)


def stratify_dict_I_gb_I_our_alg(dict, e_vars, debug=0):
    """
    Build a stratified Groebner bases for dict
    """
    # Ideal for the polynomials of the new basis
    A = []
    # and their leading monomials
    LMs = []
    new_dict = {}


    while len(dict) > 0:
        # We copy the keys of dict into I in order to sort them.
        # This way the elements are traversed in a unique order.
        I = dict.keys()
        I.sort()
        p = I[0]

        p_gb = dict[p]

        del dict[p]

        if debug > 1:
            print "Processing p", p
            print "A before proceeding", A

        if p_gb.is_zero():
            LMs.append(Polynomial(0, self.ring))
            A.append(p)
            if debug > 1:
                print "Adding p that becomes zero"
            continue

        p_gb_lm = p_gb.lm()

        # If the leading monomial of p does not coincide with any of
        # polynomials already in A, then add p to A
        if not p_gb_lm in LMs:
            A.append(p)
            LMs.append(p_gb_lm)
            if debug > 1:
                print "Appending", p, "since its lm is not contained in A yet"
            continue

        # Index of p_gb_lm in LMs
        i = LMs.index(p_gb_lm)

        # Polynomial in A with the same lm as p
        b = A[i]

        # The Polynomial we want to add to A while keeping A stratified
        r = p

        b_gb = expanded_polynomial2general_polynomial(b, e_vars)
        r_gb = expanded_polynomial2general_polynomial(r, e_vars)

        # Leading coefficients as GeneralBooleanPolynomial
        lc_b_gb = b_gb.lc()
        lc_r_gb = r_gb.lc()
        unit = GeneralBooleanPolynomial(len(e_vars), [o for o in xrange(len(
            e_vars))], Polynomial(1, p.ring()))

        b1_gb = b_gb * (unit + lc_r_gb) + r_gb
        r2_gb = r_gb * (unit + lc_r_gb + lc_r_gb * lc_b_gb) + lc_r_gb * b_gb

        b1 = b1_gb.to_expanded_polynomial_ring(e_vars)
        r2 = r2_gb.to_expanded_polynomial_ring(e_vars)

        A[i] = b1
        if debug > 1:
            print "New polynomial in A (replaced)", A[i]

        if r2 != 0 and r2 not in A:
            dict[r2] = r2_gb

    return build_dict_from_array_of_extended_polynomials(A, e_vars)


def stratify_dict_I_gb_I_Inoue(dict, e_vars, debug=0):
    """
    Reimplementation of a simple algorithm of Inoue from BGSet
    """
    # Ideal for the polynomials of the new basis
    A = []
    new_dict = {}
    while len(dict) > 0:
        p = dict.keys()[0]
        p_gb = dict[p]
        del dict[p]

        if p_gb.is_zero():
            A.append(p)
            continue

        p_gb_lm = p_gb.lm()

        for f in dict.keys():
            f_gb = dict[f]

            if f_gb.is_zero():
                continue

            if p_gb_lm == f_gb.lm():
                p = p + f
                del dict[f]
        A.append(p)
    return build_dict_from_array_of_extended_polynomials(A, e_vars)
