//
// LiDIA - a library for computational number theory
//   Copyright (c) 1994, 1995 by the LiDIA Group
//
// File        : factoring.c
// Author      : Victor Shoup, Thomas Pfahler (TPf)
// Last change : TPf, Feb 29, 1996, initial version
//             


#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:udigit.h>
//#include <LiDIA:random.h>

//#include <LiDIA:factorization.h>

#else

#include <LiDIA/Fp_polynomial.h>
#include <LiDIA/Fp_polynomial_util.h>
#include <LiDIA/poly_modulus.h>
#include <LiDIA/udigit.h>
//#include <LiDIA/random.h>

//#include <LiDIA/factorization.h>

#endif



void find_roots(base_vector<bigint>& x, const Fp_polynomial& f)
{
// f is monic, and has deg(f) distinct roots.
// appends the list of roots to x (as values in the range 0..p-1 where p = modulus of f)
	debug_handler( "factoring.c", "find_roots( base_vector<bigint>&, Fp_polynomial& )" );

	if (f.degree() == 0)
	    return;

	const bigint &p = f.modulus();

	if (f.degree() == 1)
	{
		lidia_size_t k = x.size();

		if (x.capacity() < k+1)
			x.set_capacity(k+1);
		x.set_size(k+1);

		NegateMod(x[k], f.const_term(), p);
		return;
	}

	Fp_polynomial h;

	bigint r;	//static
	bigint p1;	//static


	//shift_right(p1, p, 1);
	p1.assign(p);
	p1.divide_by_2();


	{
		poly_modulus F(f);
		bigint minus_one;
		minus_one.assign_one();
		minus_one.negate();
		do
		{
		 	r.assign(randomize(p));
			power_x_plus_a(h, r, p1, F);
			add(h, h, minus_one);
			gcd(h, h, f);
		} while (h.degree() <= 0 || h.degree() == f.degree());
	}

	find_roots(x, h);
	divide(h, f, h);
	find_roots(x, h);
}


base_vector< bigint > find_roots(const Fp_polynomial& f)
{
    base_vector< bigint > v;
    find_roots(v, f);
    return v;
}


void trace_map(Fp_polynomial& w, const Fp_polynomial& a, lidia_size_t d,
    const poly_modulus& F, const Fp_polynomial& b)
// w = a+a^q+...+^{q^{d-1}} mod f;
// it is assumed that d >= 0, and b = x^q mod f, q a power of p
// Space allocation can be controlled via ComposeBound
{
	debug_handler( "factoring.c", "trace_map( Fp_polynomial&, Fp_polynomial&, lidia_size_t, poly_modulus&, Fp_polynomial& )" );

	a.comp_modulus(b, "trace_map");
	a.comp_modulus(F.modulus(), "trace_map");

	Fp_polynomial y, z, t;

	z.assign( b );
	y.assign( a );
	F.forward_modulus(w);
	w.assign_zero();

	while (d)
	{
		if (d == 1)
		{
			if (w.is_zero())
				w.assign( y );
			else
			{
				compose(w, w, z, F);
				add(w, w, y);
			}
		}
		else
		{
			if ((d & 1) == 0)
			{
				compose2(z, t, z, y, z, F);
				add(y, t, y);
			}
			else
			{
				if (w.is_zero())
				{
					w.assign( y );
					compose2(z, t, z, y, z, F);
					add(y, t, y);
				}
				else
				{
					compose3(z, t, w, z, y, w, z, F);
					add(w, w, y);
					add(y, t, y);
				}
			}
		}
		d = d >> 1;
	}
}


void power_compose(Fp_polynomial& y, const Fp_polynomial& h, lidia_size_t q, const poly_modulus& F)
// w = x^{q^d} mod f;
// it is assumed that d >= 0, and b = x^q mod f, q a power of p
// Space allocation can be controlled via ComposeBound
{
	debug_handler( "factoring.c", "power_compose( Fp_polynomial&, Fp_polynomial&, lidia_size_t, poly_modulus& )" );

	h.comp_modulus(F.modulus(), "power_compose");
		
	Fp_polynomial z;
	z.set_max_degree(F.deg() - 1);
	lidia_size_t sw;

	z.assign( h );
	
	F.forward_modulus(y);	
	y.assign_x();

	while (q)
	{
		sw = 0;

		if (q > 1) sw = 2;
		if (q & 1)
		{
			if (y.is_x())
				y.assign( z );
			else
				sw = sw | 1;
		}

		switch (sw)
		{
		case 0:
			break;

		case 1:
			compose(y, y, z, F);
			break;

		case 2:
			compose(z, z, z, F);
			break;

		case 3:
			compose2(y, z, y, z, z, F);
			break;
		}

		q = q >> 1;
	}
}


bool prob_irred_test(const Fp_polynomial& f, lidia_size_t iter)
// performs a fast, probabilistic irreduciblity test
// the test can err only if f is reducible, and the
// error probability is bounded by p^{-iter}.
// Works for any p.
{
	debug_handler( "factoring.c", "prob_irred_test( Fp_polynomial&, lidia_size_t )" );

	lidia_size_t n = f.degree();

	if (n <= 0)
		return false;
	if (n == 1)
		return true;

	const bigint& p = f.modulus();

	poly_modulus F(f);

	Fp_polynomial b, r, s;

	power_x(b, p, F);

	lidia_size_t i;

	for (i = 0; i < iter; i++)
	{
		randomize(r, p, n-1);
		trace_map(s, r, n, F, b);

		if (s.degree() > 0)
			return false;
	}

	if (p >= n)
		return true;

	lidia_size_t pp;

	//p is now < n;      n is of type lidia_size_t

	p.sizetify(pp);

	if (n % pp != 0)
		return true;

	power_compose(s, b, n/pp, F);
	return !s.is_x();
}




static
lidia_size_t base_case(const Fp_polynomial& h, lidia_size_t q, int a, const poly_modulus& F)
{
	debug_handler( "factoring.c", "base_case( Fp_polynomial&, lidia_size_t, int, poly_modulus& )" );

	lidia_size_t b;
	Fp_polynomial lh;
	lh.set_max_degree(F.deg() - 1);

	lh.assign( h );
	b = 0;
	while (b < a-1 && !lh.is_x())
	{
		b++;
		power_compose(lh, lh, q, F);
	}

	if (!lh.is_x())
		b++;

	return b;
}

int compute_split(int lo, int hi, const fac_vec& fvec)
{
	debug_handler( "factoring.c", "compute_split( int, int, fac_vec& )" );

	int mid, i;
	double total, sum;

	total = 0;
	for (i = lo; i <= hi; i++)
		total = total + fvec[i].len;

	mid = lo-1;
	sum = 0;
	while (sum < total/2)
	{
		mid++;
		sum = sum + fvec[mid].len;
	}

	if (mid == hi   ||   (mid != lo && 2*sum > total + fvec[mid].len))
		mid--;

	return mid;
}

static
void tandem_power_compose(Fp_polynomial& y1, Fp_polynomial& y2,
    const Fp_polynomial& h, lidia_size_t q1, lidia_size_t q2,
    const poly_modulus& F)
{
	debug_handler( "factoring.c", "tandem_power_compose( Fp_polynomial&, Fp_polynomial&,	Fp_polynomial& h, lidia_size_t, lidia_size_t, poly_modulus& )" );
		
	Fp_polynomial z;
	z.set_max_degree(F.deg() - 1);
	lidia_size_t sw;

	z.assign( h );

	F.forward_modulus(y1);
	F.forward_modulus(y2);
	y1.assign_x();
	y2.assign_x();

	while (q1 || q2)
	{
		sw = 0;

		if (q1 > 1 || q2 > 1)
			sw = 4;

		if (q1 & 1)
		{
			if (y1.is_x())
				y1.assign( z );
			else
				sw = sw | 2;
		}

		if (q2 & 1)
		{
			if (y2.is_x())
				y2.assign( z );
			else
				sw = sw | 1;
		}

		switch (sw)
		{
		case 0:
			break;

		case 1:
			compose(y2, y2, z, F);
			break;

		case 2:
			compose(y1, y1, z, F);
			break;

		case 3:
			compose2(y1, y2, y1, y2, z, F);
			break;

		case 4:
			compose(z, z, z, F);
			break;

		case 5:
			compose2(z, y2, z, y2, z, F);
			break;

		case 6:
			compose2(z, y1, z, y1, z, F);
			break;

		case 7:
			compose3(z, y1, y2, z, y1, y2, z, F);
			break;
		}

		q1 = q1 >> 1;
		q2 = q2 >> 1;
	}
}


void rec_compute_degree(int lo, int hi, const Fp_polynomial& h, 
	const poly_modulus& F, fac_vec& fvec)
{
	debug_handler( "factoring.c", "rec_compute_degree( int, int, Fp_polynomial&, poly_modulus&, fac_vec& )" );

	int mid;
	lidia_size_t q1, q2;
	Fp_polynomial h1, h2;

	if (h.is_x())
	{
		fvec.clear(lo, hi);
		return;
	}

	if (lo == hi)
	{
		fvec[lo].b = base_case(h, fvec[lo].q, fvec[lo].a, F);
		return;
	}

	mid = compute_split(lo, hi, fvec);

	q1 = fvec.prod(lo, mid);
	q2 = fvec.prod(mid+1, hi);

	tandem_power_compose(h1, h2, h, q1, q2, F);
	rec_compute_degree(lo, mid, h2, F, fvec);
	rec_compute_degree(mid+1, hi, h1, F, fvec);
}


lidia_size_t compute_degree(const Fp_polynomial& h, const poly_modulus& F)
// f = F.f is assumed to be an "equal degree" polynomial
// h = x^p mod f
// the common degree of the irreducible factors of f is computed
// This routine is useful in counting points on elliptic curves
{
	debug_handler( "factoring.c", "compute_degree( Fp_polynomial&, poly_modulus& )" );

	h.comp_modulus(F.modulus(), "compute_degree");

	if (h.is_x())
		return 1;
		
	lidia_size_t res;

	fac_vec fvec(F.deg());

	int i, NumFactors = fvec.number_of_factors();

	rec_compute_degree(0, NumFactors-1, h, F, fvec);

	res = 1;

	for (i = 0; i < NumFactors; i++)
		res = res * (lidia_size_t)udigit_power(fvec[i].q, fvec[i].b);

	return res;
}

lidia_size_t prob_compute_degree(const Fp_polynomial& h, const poly_modulus& F)
// same as above, but uses a slightly faster probabilistic algorithm
// the return value may be 0 or may be too big, but for large p
// (relative to n), this happens with very low probability.
{
	debug_handler( "factoring.c", "prob_compute_degree( Fp_polynomial&, poly_modulus& )" );

	h.comp_modulus(F.modulus(), "prob_compute_degree");
		
	if (h.is_x())
		return 1;

	lidia_size_t n = F.deg();
	const bigint &p = h.modulus();

	Fp_polynomial P1, P2, P3;

	randomize(P1, p, n-1);
	trace_map(P2, P1, n, F, h);
	min_poly(P3, P2, n/2, F);

	lidia_size_t r = P3.degree();

	if (r <= 0 || n % r != 0)
		return 0;
	else
		return n/r;
}



bigint find_root(const Fp_polynomial& ff)
// finds a root of ff.
// assumes that ff is monic and splits into distinct linear factors
{
	debug_handler( "factoring.c", "find_root( Fp_polynomial& )" );

	poly_modulus F;
	Fp_polynomial h, f;

	bigint r;	//static

	bigint minus_one, root;
	minus_one.assign_one();
	minus_one.negate();

	const bigint &p = ff.modulus();

	bigint p_div2;  //static
	//shift_right(p_div2, p, 1);
	p_div2.assign(p);
	p_div2.divide_by_2();

	f.assign( ff );

	while (f.degree() > 1)
	{
		F.build(f);
		r.assign(randomize(p-1));
		power_x_plus_a(h, r, p_div2, F);
		add(h, h, minus_one);
		gcd(h, h, f);
		if (h.degree() > 0 && h.degree() < f.degree())
		{
			if (h.degree() > f.degree()/2)
				divide(f, f, h);
			else
				f.assign( h );
		}
	}

	NegateMod(root, f.const_term(), p);
	return root;
}

static
bool irred_base_case(const Fp_polynomial& h, lidia_size_t q, int a, const poly_modulus& F)
{
	debug_handler( "factoring.c", "irred_base_case( Fp_polynomial&, lidia_size_t, int, poly_modulus& )" );

	lidia_size_t e;
	Fp_polynomial x, s, d;

	e = (lidia_size_t)udigit_power(q, a-1);
	power_compose(s, h, e, F);
	
	F.forward_modulus(x);
	x.assign_x();
	subtract(s, s, x);
	gcd(d, F.modulus(), s);
	return d.is_one();
}


bool rec_irred_test(int lo, int hi, const Fp_polynomial& h,
	const poly_modulus& F, const fac_vec& fvec)
{
	debug_handler( "factoring.c", "rec_irred_test( int, int, Fp_polynomial&, poly_modulus&, fac_vec& )" );

	lidia_size_t q1, q2;
	int mid;
	Fp_polynomial h1, h2;

	if (h.is_x()) return false;

	if (lo == hi)
	{
		return irred_base_case(h, fvec[lo].q, fvec[lo].a, F);
	}

	mid = compute_split(lo, hi, fvec);

	q1 = fvec.prod(lo, mid);
	q2 = fvec.prod(mid+1, hi);

	tandem_power_compose(h1, h2, h, q1, q2, F);
	return 		rec_irred_test(lo, mid, h2, F, fvec)
			&&  rec_irred_test(mid+1, hi, h1, F, fvec);
}

bool det_irred_test(const Fp_polynomial& f)
// performs a recursive deterministic irreducibility test
{
	debug_handler( "factoring.c", "det_irred_test( Fp_polynomial& )" );

	if (f.degree() <= 0)
		return false;
	if (f.degree() == 1)
		return true;

	poly_modulus F(f);

	Fp_polynomial h;
	const bigint &p = f.modulus();

	power_x(h, p, F);

	Fp_polynomial s;
	power_compose(s, h, F.deg(), F);
	if (!s.is_x())
		return false;

	fac_vec fvec(F.deg());

	int NumFactors = fvec.number_of_factors();

	return rec_irred_test(0, NumFactors-1, h, F, fvec);
}


bool iter_irred_test(const Fp_polynomial& f)
// performs an iterative deterministic irreducibility test,
// based on DDF
{
	debug_handler( "factoring.c", "iter_irred_test( Fp_polynomial& )" );

	const MaxLimit = 8;

	if (f.degree() <= 0)
		return false;
	if (f.degree() == 1)
		return true;

	poly_modulus F(f);

	Fp_polynomial h;
	const bigint &p = f.modulus();
	
	power_x(h, p, F);

	lidia_size_t CompTableSize = 2*square_root(f.degree());

	poly_argument H;

	H.build(h, F, CompTableSize);

	lidia_size_t i, d, limit, old_n;
	Fp_polynomial g, x, t, prod;

	F.forward_modulus(x);
	x.assign_x();

	i = 0;
	g = h;
	d = 1;
	limit = 1;

	F.forward_modulus(prod);
	prod.assign_one();


	while (2*d <= f.degree())
	{
		old_n = f.degree();
		subtract(t, g, x);
		multiply(prod, prod, t, F);	//poly_modulus
		i++;
		if (i == limit)
		{
			gcd(t, f, prod);
			if (!t.is_one())
				return false;

			prod.assign_one();
			limit = comparator<lidia_size_t>::min(MaxLimit, 2 * limit);
			i = 0;
		}

		d = d + 1;
		if (2*d <= f.degree())
		{
			H.compose(g, g, F);
		}
	}

	if (i > 0)
	{
		gcd(t, f, prod);
		if (!t.is_one())
			return false;
	}

	return true;
}

static
void multiply_by_x_plus_y(base_vector<Fp_polynomial>& h, const Fp_polynomial& f, const Fp_polynomial& g)
// h represents the bivariate polynomial h[0] + h[1]*y + ... + h[n-1]*y^k,
// where the h[i]'s are polynomials in x, each of degree < deg(f),
// and k < deg(g).
// h is replaced by the bivariate polynomial h*(x+y) (mod f(x), g(y)).

{
	debug_handler( "factoring.c", "multiply_by_x_plus_y( base_vector<Fp_polynomial>&, Fp_polynomial&, Fp_polynomial& )" );

	lidia_size_t n = g.degree();
	lidia_size_t k = h.size()-1;

	if (k < 0)
		return;

	if (k < n-1)
	{
		if (h.capacity() < k+2)
			h.set_capacity(k+2);
		h.set_size(k+2);

		h[k+1].assign( h[k] );
		for (lidia_size_t i = k; i >= 1; i--)
		{
			multiply_by_x_mod(h[i], h[i], f);
			add(h[i], h[i], h[i-1]);
		}
		multiply_by_x_mod(h[0], h[0], f);
	}
	else
	{
		Fp_polynomial b, t;

		b.assign( h[n-1] );
		for (lidia_size_t i = n-1; i >= 1; i--)
		{
			multiply_by_scalar(t, b, g[i]);
			multiply_by_x_mod(h[i], h[i], f);
			add(h[i], h[i], h[i-1]);
			subtract(h[i], h[i], t);
		}
		multiply_by_scalar(t, b, g[0]);
		multiply_by_x_mod(h[0], h[0], f);
		subtract(h[0], h[0], t);
	}

	// normalize

	k = h.size()-1;
	while (k >= 0 && h[k].is_zero())
		k--;
	if (h.capacity() < k+1)
		h.set_capacity(k+1);
	h.set_size(k+1);

}


void irred_combine(Fp_polynomial& x, const Fp_polynomial& f, const Fp_polynomial& g)
{
	debug_handler( "factoring.c", "irred_combine( Fp_polynomial&, Fp_polynomial&, Fp_polynomial& )" );

	if (f.degree() < g.degree())
	{
		irred_combine(x, g, f);
		return;
	}

	// f.degree() >= deg(g)...not necessary, but maybe a little more
	//  					                  time & space efficient

	lidia_size_t df = f.degree();
	lidia_size_t dg = g.degree();
	lidia_size_t m = df*dg;
	
	Fp_polynomial a, b;
	bigint t;
	const bigint &p = g.modulus();

	base_vector<Fp_polynomial> h(dg,dg);

	lidia_size_t i;
	for (i = 0; i < dg; i++)
		h[i].set_max_degree(df-1);

	if (h.capacity() == 0)
		h.set_capacity(1);
	h.set_size(1);
	h[0].MOD = g.MOD;	//= set_modulus(p);
	h[0].assign_one();

	a.MOD = g.MOD;	//= set_modulus(p)
	b.MOD = g.MOD;	//= set_modulus(p)

	poly_matrix M(g.MOD);

	a.set_degree(2*m-1);

	for (i = 0; i < 2*m; i++)
	{
		a[2*m-1-i].assign( h[0].const_term() );
		if (i < 2*m-1)
			multiply_by_x_plus_y(h, f, g);
	}

	a.remove_leading_zeros();
	b.set_coefficient(2*m);

	M.half_gcd(b, a, m+1);

	/* make monic */

	InvMod(t, (M(1,1)).lead_coeff(), p);
	multiply_by_scalar(x, M(1,1), t);
}

static
void build_prime_power_irred(Fp_polynomial& f, const bigint & p, lidia_size_t q, int e)
{
	debug_handler( "factoring.c", "build_prime_power_irred( Fp_polynomial&, bigint&, lidia_size_t, int )" );

	lidia_size_t n = (lidia_size_t)udigit_power(q, e);

	do
	{
		randomize(f, p, n-1);
		f.set_coefficient(n);
	} while (!iter_irred_test(f));
}



void build_irred(Fp_polynomial& f, const bigint& p, lidia_size_t n)
// Build a monic irreducible poly of degree n (over Z/pZ)
{
	debug_handler( "factoring.c", "build_irred( Fp_polynomial&, bigint&, lidia_size_t )" );

	if (n <= 0)
		lidia_error_handler( "factoring.c", "build_irred( Fp_polynomial&, bigint&, lidia_size_t )::n must be positive" );

	if (n == 1)
	{
	  	f.set_modulus(p);
		f.assign_x();
		return;
	}

	fac_vec fvec(n);

	int i, NumFactors = fvec.number_of_factors();

	f.set_max_degree(n);


	build_prime_power_irred(f, p, fvec[0].q, fvec[0].a);

	Fp_polynomial g;
	g.set_max_degree(n);

	for (i = 1; i < NumFactors; i++)
	{
		build_prime_power_irred(g, p, fvec[i].q, fvec[i].a);
		irred_combine(f, f, g);
	}
}


void build_random_irred(Fp_polynomial& f, const Fp_polynomial& g)
// g is a monic irreducible polynomial.
// constructs a random monic irreducible polynomial f of the same degree.
{
	debug_handler( "factoring.c", "build_random_irred( Fp_polynomial&, Fp_polynomial& )" );

	const bigint &p = g.modulus();
	poly_modulus G(g);
	Fp_polynomial h, ff;

	do
	{
		randomize(h, p, g.degree()-1);
		irred_poly(ff, h, g.degree(), G);
	} while (ff.degree() < g.degree());

	f.assign( ff );
}

