//
// LiDIA - a library for computational number theory
//   Copyright (c) 1994, 1995 by the LiDIA Group
//
// File        : 
// Author      : Victor Shoup, Thomas Pfahler (TPf)
// Last change : TPf, Feb 29, 1996, initial version
//               TPf, Sep 25, 1996, modified strategy for last step of berlekamp
//



#if defined(HAVE_MAC_DIRS) || defined(__MWERKS__)

#include <LiDIA:Fp_polynomial.h>
#include <LiDIA:Fp_polynomial_util.h>
#include <LiDIA:poly_modulus.h>
#include <LiDIA:poly_multiplier.h>

#include <LiDIA:single_factor.h>
#include <LiDIA:factorization.h>

#else

#include <LiDIA/Fp_polynomial.h>
#include <LiDIA/Fp_polynomial_util.h>
#include <LiDIA/poly_modulus.h>
#include <LiDIA/poly_multiplier.h>

#include <LiDIA/single_factor.h>
#include <LiDIA/factorization.h>

#endif



void null_space(lidia_size_t & r, base_vector < sdigit > &D, bigint**&M, lidia_size_t n, const bigint & p)
{
    debug_handler("Fp_polynomial", "null_space( lidia_size_t&, base_vector<sdigit>&, bigint**&, lidia_size_t, bigint&)");

    //input : (n x n)-matrix M, modulus p, vector D
    //output : "triangularized" "compressed" matrix M with same nullspace 
    //                      as imput-matrix M, empty rows are deleted (?)
    //              r = dimension of nullspace of M
    //              D[i] == -1 iff row i of M is zero
    //              D[i] == m iff M[m][m]!=0 and M[m][j]==0 for all 0<=j<m

    lidia_size_t k, l;
    lidia_size_t i, j;
    lidia_size_t pos;
    bigint t1, t2;
    bigint *x, *y;
    bool verbose = single_factor < Fp_polynomial >::verbose();

    if (D.capacity() < n)
	D.set_capacity(n);
    D.set_size(n);

    for (j = 0; j < n; j++)
	D[j] = -1;


    r = 0;
    l = 0;
    for (k = 0; k < n; k++)
    {
	if (verbose && k % 10 == 0) cerr << "+";

	pos = -1;
	for (i = l; i < n; i++)
	{
	    if (pos == -1 && !M[i][k].is_zero())
		pos = i;
	}

	if (pos != -1)
	{
	    //swap(M[pos], M[l]) :
	    x = M[pos];
	    M[pos] = M[l];
	    M[l] = x;

	    // make M[l, k] == -1 mod p, and make row l reduced
	    InvMod(t1, M[l][k], p);
	    NegateMod(t1, t1, p);		//t1 = -1/M[l][k]
	    M[l][k] = -1;
	    
	    for (j = k + 1; j < n; j++)
	    {
		remainder(t2, M[l][j], p);
		MulMod(M[l][j], t2, t1, p);
	    }

	    for (i = l + 1; i < n; i++)
	    {
		// M[i] = M[i] + M[l]*M[i,k]

		t1.assign( M[i][k] );	// this is already reduced

		if (!t1.is_zero())
		{
		    x = &M[i][k+1];
		    y = &M[l][k+1];

		    for (j = k + 1; j < n; j++, x++, y++)
		    {
			// *x = *x + (*y)*t1

			multiply(t2, *y, t1);
			add(*x, *x, t2);
		    }
		}

		M[i][k].assign_zero();	//this has been eliminated

	    }

	    D[k] = l;		// variable k is defined by row l

	    l++;

	}
	else
	{
	    r++;
	}

	//reduce next column of remaining rows
	if (k != n - 1)
	    for (j = l; j < n; j++)
		remainder(M[j][k + 1], M[j][k + 1], p);
    }
    //one could delete rows i with D[i] == -1, couldn't one ?
}


void build_matrix(bigint **&M, lidia_size_t n, const Fp_polynomial & g, const poly_modulus & F)
{
    debug_handler("Fp_polynomial", "build_matrix( bigint**&, lidia_size_t, Fp_polynomial&, poly_modulus&, lidia_size_t)");

//input : polynomial g, deg(g) < n; Polymodulus F, deg(F) < n
//output: (n x n)-matrix M, where column i is equivalent to g^i mod F   (i = 0..n-1)

    if (g.degree() >= n || F.deg() > n)
	lidia_error_handler("Fp_polynomial", "build_matrix( ... )::degree of polynomil too big");

    g.comp_modulus(F.modulus(), "build_matrix");

    lidia_size_t i, j, m;
    bool verbose = single_factor< Fp_polynomial >::verbose();
    Fp_polynomial h;
    const bigint & p = g.modulus();

    M = new bigint *[n];
    if (!M) lidia_error_handler("Fp_polynomial", 
		"build_matrix( ... )::out of memory");
    for (i = 0; i < n; i++)
    {
	M[i] = new bigint[n];
	if (!M[i]) lidia_error_handler("Fp_polynomial", 
		    "build_matrix( ... )::out of memory");
    }

    poly_multiplier G(g, F);

    F.forward_modulus(h);
    h.assign_one();
    for (j = 0; j < n; j++)
    {
	if (verbose && j % 10 == 0)
	    cerr << "+";

	m = h.degree();
	for (i = 0; i < n; i++)
	{
	    if (i <= m)
		M[i][j].assign(h[i]);
	    else
		(M[i][j]).assign_zero();
	}

	if (j < n - 1)
	    multiply(h, h, G, F);	//poly_multiplier

    }

//we might move this into the function null_space
    bigint one(1);
    for (i = 0; i < n; i++)
	SubMod(M[i][i], M[i][i], one, p);

}



void random_basis_elt(Fp_polynomial & g, const base_vector < sdigit > &D, bigint ** M, const bigint & p)  // MM, "**& M -> ** M for SunPro"
{
    debug_handler("Fp_polynomial", "random_basis_elt( Fp_polynomial&, base_vector<sdigit>&, bigint**&, lidia_size_t, bigint& )");

    //input : vector D, Matrix M, modulus p as output of function null_space
    //output: g = random basis element of ker M
    //REMEMBER: all coefficients MUST lie in the interval [0 .. p-1] !!!

    bigint t1, t2;
    lidia_size_t n = D.size();
    lidia_size_t i, j, s;

    g.set_modulus(p);

    for (j = n - 1; j >= 0; j--)
    {
	if (D[j] == -1)
	    g.set_coefficient(randomize(p), j);    //g[j].assign(randomize(p));
	else
	{
	    i = (lidia_size_t) D[j];

	    // g[j] = sum_{s=j+1}^{n-1} g[s]*M[i,s]

	    t1.assign_zero();
	    for (s = j + 1; s < n; s++)
	    {
		multiply(t2, g[s], M[i][s]);
		add(t1, t1, t2);
	    }
	    g.set_coefficient(t1, j);	//Remainder(g[j], t1, p);
	}
    }
//    g.remove_leading_zeros();
}


void
sf_berlekamp_work(factorization< Fp_polynomial > &factors,
	const Fp_polynomial &x_to_the_power_of_p, const poly_modulus &F)
// x_to_the_power_of_p = x^p mod f
// Assumes f is square-free, monic, deg(f) > 0 and f(0) != 0 .
// returns list of factors of f.
{
    debug_handler("Fp_polynomial", "sf_berlekamp_work( base_vector<Fp_polynomial>&, Fp_polynomial&, poly_modulus& )");
    
    const Fp_polynomial &f = F.modulus();

    if (f.const_term().is_zero())
	lidia_error_handler( "Fp_polynomial", "sf_berlekamp::const_term == 0" );

    factors.kill();
    
    bool verbose = single_factor< Fp_polynomial >::verbose();
    my_timer t;

    const bigint & p = f.modulus();
    lidia_size_t n = f.degree();

    Fp_polynomial g,h;

    base_vector < sdigit > D;
    lidia_size_t r;
    bigint **M;

    if (verbose) t.start("building matrix...");
    build_matrix(M, n, x_to_the_power_of_p, F);
    if (verbose) t.stop();

    if (verbose) t.start("diagonalizing...");
    null_space(r, D, M, n, p);
    if (verbose) t.stop();

    if (verbose) cerr << "number of factors = " << r << "\n";

    if (r == 1)
    {
	append_irred_factor(factors, f);
	return;
    }

    int method;
    // 3 = minimal polynomial method
    // 1 = direct powering method
    // 2 = gcd method

    if (p > 2*r)
	method = 3;
    else
	method = 1;
	

//    if (f.degree() < f.modulus())  method = 0;
//    else                           method = 1;

    lidia_size_t i, j;
    base_vector < bigint > roots;
    lidia_size_t len;
    Fp_polynomial *container;
    int counter;
    Fp_polynomial tmp, hh;
    bigint p2;

    switch(method)	//DO NOT MODIFY THE ORDER OF THE case-BLOCKS !!!
    {
    case(2) :
	if (!is_int(f.modulus())) lidia_error_handler("Fp_polynomial",
				"sf_berlekamp(...)::modulus too large");
	    
	if (verbose) t.start("computing gcd's ...");

	container = new Fp_polynomial[r];
	if (!container) lidia_error_handler("Fp_polynomial",
			"sf_berlekamp(...)::out of memory");
	container[0].assign(f);
	len = 1;

	while (len < r)
	{
	    if (verbose) cerr << "+";

	    random_basis_elt(g, D, M, p);
	    for (i = 0; (i < p) && (len < r); i++)
	    {
		for (j = 0; (j < len) && (len < r); j++)
		{
		    gcd(h, container[j], g);
		    if (!h.is_one() && h!=container[j])
		    {
			container[len].assign( h );
			len++;
			divide(container[j], container[j], h);
		    }
		}
		add(g, g, (bigint)1);
	    }
	}
	for (i = 0; i < n; i++) delete[] M[i];
	delete[] M;

	for (i = 0; i < r; i++)
	    append_irred_factor(factors, container[i]);
	delete[] container;

	if (verbose) t.stop();
	break;
	
    case(3) :
	if (verbose) t.start("computing splitting polynomial...");
	random_basis_elt(g, D, M, p);
	checked_min_poly(h, g, r, F);
	counter = 0;
	while (h.degree() < r && counter < 100)
	{
	    if (verbose) cerr << "+";
	    random_basis_elt(g, D, M, p);
	    checked_min_poly(h, g, r, F);
	    counter++;
	}
	if (verbose) t.stop();
	if (counter != 100)
	{
	    for (i = 0; i < n; i++) delete[] M[i];
	    delete[] M;
	
	    if (verbose) t.start("finding roots of splitting polynomial...");
	    find_roots(roots, h);
	    if (verbose) t.stop();

	    if (verbose) t.start("finding factors...");
	    find_irred_factors(factors, f, g, roots);
	    break;
	}
	else
	    if (verbose) cout<<"FAILED."<<endl;
	//if failed: trying direct powering method
		
    
    case(1) :
	if (verbose) t.start("direct powering method...");

	container = new Fp_polynomial[r];
	if (!container) lidia_error_handler("Fp_polynomial",
			"sf_berlekamp(...)::out of memory");
	container[0].assign(f);
	len = 1;

	shift_right(p2, p, 1);	//p2 = (p-1)/2

	do
	{
	    if (verbose) cerr << "+";
	    random_basis_elt(g, D, M, p);
//		bigint c = randomize(p);
//		add(g, g, c);
	    power(hh, g, p2, F);
	    add(h, hh, (bigint)1);
	    for (i = 0; (i < len) && (len < r); i++)
	    {
		gcd(tmp, h, container[i]);
		if (!tmp.is_one()  &&  tmp != container[i])
		{
		    divide(container[len], container[i], tmp);
		    container[i].assign( tmp );
		    len++;
		}
		if (len < r)
		{
		    gcd(tmp, hh, container[i]);
		    if (!tmp.is_one()  &&  tmp != container[i])	
		    {
			divide(container[len], container[i], tmp);
			container[i].assign( tmp );
			len++;
		    }
		}
	    }
	} while (len < r);
	
	for (i = 0; i < n; i++) delete[] M[i];
	delete[] M;

	for (i = 0; i < r; i++)
	    append_irred_factor(factors, container[i]);
	delete[] container;
			    
	if (verbose) t.stop();
	break;
	
    default : lidia_error_handler("Fp_polynomial",
			"sf_berlekamp::wrong index for method");
    };

    if (verbose)
    {
	cerr << "degrees:";
	for (i = 0; i < factors.no_of_prime_components(); i++)
	    cerr << " " << factors.prime_base(i).base().degree();
	cerr << "\n";
    }
}



void
sf_berlekamp(factorization< Fp_polynomial > &factors, const Fp_polynomial & f)
{
    bool verbose = single_factor< Fp_polynomial >::verbose();
    my_timer t;
    Fp_polynomial g;
    poly_modulus F(f);
    if (verbose) t.start("computing x^p...");
    power_x(g, f.modulus(), F);
    if (verbose) t.stop();
    sf_berlekamp_work(factors, g, F);
}
		    


void
berlekamp(factorization < Fp_polynomial > &factors, const Fp_polynomial & f)
// returns a list of factors, with multiplicities.
{
    debug_handler("Fp_polynomial", "berlekamp( factorization< Fp_polynomial >&, Fp_polynomial& )");

    if (f.is_zero())
	lidia_error_handler("Fp_polynomial", "berlekamp( factorization< Fp_polynomial >&, Fp_polynomial& )::input is zero polynomial");

    bool verbose = single_factor< Fp_polynomial >::verbose();

    my_timer t;
    factorization < Fp_polynomial > sfd, x;
    Fp_polynomial tmp;
    bigint lc = f.lead_coeff();

    if (verbose) t.start("square-free decomposition...");
    if (lc.is_one())
    {
	square_free_decomp(sfd, f);
	factors.kill();
    }
    else
    {
	tmp.assign(f);
	tmp.make_monic();
	square_free_decomp(sfd, tmp);
	factors.kill();
	tmp.assign_one();
	tmp.set_coefficient(lc, 0);
	factors.append(tmp);
    }
    if (verbose) t.stop();

    lidia_size_t i;
    for (i = 0; i < sfd.no_of_composite_components(); i++)
    {
	if (verbose)
	{
	    cerr << "factoring multiplicity " << sfd.composite_exponent(i);
	    cerr << ", deg = " << sfd.composite_base(i).base().degree() << "\n";
	}

	/*   check for trivial factor 'x'   */
	if (!sfd.composite_base(i).base().const_term().is_zero())
	    sf_berlekamp(x, sfd.composite_base(i).base());
	else
	{
	    tmp.assign(sfd.composite_base(i).base());
	    shift_right(tmp,tmp,1);
	    
	    if (tmp.degree() > 0)
		sf_berlekamp(x, tmp);
	    else
		x.kill();
	
	    /*   append trivial factor   */
	    tmp.assign_x();
	    append_irred_factor(factors, tmp, sfd.composite_exponent(i));
	}
	x.power(sfd.composite_exponent(i));

	//append factors
	multiply(factors, factors, x);
    }
}


factorization< Fp_polynomial > berlekamp(const Fp_polynomial& f)
{
    factorization< Fp_polynomial > F;
    berlekamp(F, f);
    return F;
}

factorization< Fp_polynomial > 
single_factor< Fp_polynomial >::berlekamp() const
{
    factorization< Fp_polynomial > F;
    ::berlekamp(F, rep);
    return F;
}


factorization< Fp_polynomial > 
single_factor< Fp_polynomial >::sf_berlekamp() const	
{
    factorization< Fp_polynomial > F;
    ::sf_berlekamp(F, rep);
    return F;
}

factorization< Fp_polynomial > sf_berlekamp(const Fp_polynomial& f)
{
    factorization< Fp_polynomial > F;
    sf_berlekamp(F, f);
    return F;
}
