//
// LiDIA - a library for computational number theory
//   Copyright (c) 1994, 1995 by the LiDIA Group
//
// File        : plain_arith.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>
#else
#include <LiDIA/Fp_polynomial.h>
#endif



/***************************************************************
	This File contains the implementation of the functions
	-	plain_mul, plain_sqr 
	-	plain_div_rem, plain_div, plain_rem 
	-	plain_inv
	These always use "classical" algorithms.
****************************************************************/


void plain_mul(Fp_polynomial &c, const Fp_polynomial &a, const Fp_polynomial &b)
{
    debug_handler_l( "Fp_polynomial",
            "plain_mul ( Fp_polynomial&, Fp_polynomial&, Fp_polynomial& )", 5 );

    c.MOD = a.MOD;

    lidia_size_t deg_a = a.degree(), deg_b = b.degree();
    if (deg_a < 0 && deg_b < 0)
    {
        c.assign_zero();
        return;
    }
    const bigint *ap, *bp;
    bigint *cp;

    Fp_polynomial tmp_a, tmp_b;
    if (&c==&a)
    {
        tmp_a.assign(a);
        ap=tmp_a.coeff;
    }
    else
        ap=a.coeff;
    if (&c==&b)
    {
        tmp_b.assign(b);
        bp=tmp_b.coeff;
    }
    else
        bp=b.coeff;

    lidia_size_t i, j, jmin, jmax, deg_ab = deg_a + deg_b;
    c.set_degree(deg_ab);

    bigint temp, accum;
    cp = c.coeff;
    const bigint& p = a.modulus();

    for (i = 0; i <= deg_ab; i++)
    {
        accum.assign_zero();
        jmin = comparator<lidia_size_t>::max(0, i-deg_b);
        jmax = comparator<lidia_size_t>::min(deg_a, i);
        for (j = jmin; j <= jmax; j++)
        {
            multiply(temp, ap[j], bp[i-j]);
            add(accum, accum, temp);
        }
        Remainder(cp[i], accum, p);
    }
    c.remove_leading_zeros();
}

void plain_sqr(Fp_polynomial &x, const Fp_polynomial &a)
{
    debug_handler_l ( "Fp_polynomial",
        "plain_sqr( Fp_polynomial&, Fp_polynomial& )", 5 );

    x.MOD = a.MOD;

    lidia_size_t deg_a = a.degree();
    if (deg_a < 0)
    {
        x.assign_zero();
        return;
    }

    lidia_size_t d = 2*deg_a;
    const bigint *ap;

    Fp_polynomial temp_poly;
    if (&x == &a)
    {
        temp_poly.assign(a);
        ap = temp_poly.coeff;
    }
    else
        ap = a.coeff;

    x.set_degree(d);
    bigint *xp = x.coeff;

    lidia_size_t i, j, jmin, jmax;
    lidia_size_t m, m2;
    bigint accum, temp;
    const bigint& p = a.modulus();

    for (i = 0; i <= d; i++)
    {
        accum.assign_zero();
        jmin = comparator<lidia_size_t>::max(0, i-deg_a);
        jmax = comparator<lidia_size_t>::min(deg_a, i);
        m = jmax - jmin + 1;
            //i>deg_a: m = 2*deg_a - i + 1
            //i<deg_a: m = i + 1
            //i=deg_a: m = i + 1 = deg_a + 1
            //m(i) ist Anzahl der zu add. Produkte fuer Koeff. i
        m2 = m >> 1;
            //m2 = m/2
            //(wegen Symmetrie nur halb so viele; falls m ungerade, wird
            // nach der Schleife direkt quadriert)
        jmax = jmin + m2 - 1;
        for (j = jmin; j <= jmax; j++)
        {
            multiply(temp, ap[j], ap[i-j]);
            add(accum, accum, temp);
        }
        accum.multiply_by_2();
        if (m & 1)
        {
            square(temp, ap[jmax + 1]);
            add(accum, accum, temp);
        }
        Remainder(xp[i], accum, p);
    }
    x.remove_leading_zeros();
}

void plain_div_rem(Fp_polynomial& q, Fp_polynomial& r, const Fp_polynomial& a, const Fp_polynomial& b)
{
    debug_handler_l( "Fp_polynomial", "plain_div_rem( Fp_polynomial&, Fp_polynomial&, Fp_polynomial&, Fp_polynomial& )", 5 );

    q.MOD = a.MOD;
    r.MOD = a.MOD;
    int lc_is_one;
    lidia_size_t da, db, dq, i, j;
    const bigint *bp;
    bigint *qp;
    bigint *xp;

    bigint LCInv, t;
    bigint s;  //static
    const bigint& p = a.modulus();

    da = a.degree();
    db = b.degree();

    if (db < 0)
        lidia_error_handler( "Fp_polynomial", "plain_div_rem( Fp_polynomial&, Fp_polynomial&, Fp_polynomial&, Fp_polynomial& )::division by zero" );

    if (da < db)
    {
        r.assign( a );
        q.assign_zero();
        return;
    }

    Fp_polynomial lb;

    if (&q == &b)
    {
        lb = b;
        bp = lb.coeff;
    }
    else
        bp = b.coeff;
    
	if (bp[db].is_one())
        lc_is_one = 1;
    else
    {
        lc_is_one = 0;
        InvMod(LCInv, bp[db], p);
    }

    xp = new bigint[da+1];
	if (!xp)
	  	lidia_error_handler( "Fp_polynomial", "plain_div_rem::out of memory" );
    for (i = 0; i <= da; i++)
        xp[i].assign( a.coeff[i] );

    dq = da - db;
    q.set_degree(dq);
    qp = q.coeff;

    for (i = dq; i >= 0; i--)
    {
        Remainder(t, xp[i+db], p);
        if (!lc_is_one)
            MulMod(t, t, LCInv, p);
        qp[i].assign(t);

        negate(t, t);

        for (j = db-1; j >= 0; j--)
        {
            multiply(s, t, bp[j]);
            add(xp[i+j], xp[i+j], s);
        }
    }

    r.set_degree(db-1);
    for (i = 0; i < db; i++)
        Remainder(r.coeff[i], xp[i], p);
    r.remove_leading_zeros();

    delete[] xp;
}



// should re-write the following
void plain_rem(Fp_polynomial& r, const Fp_polynomial& a, const Fp_polynomial& b)
{
    debug_handler_l( "Fp_polynomial", "plain_rem ( Fp_polynomial&, Fp_polynomial&, Fp_polynomial& )", 5 );
    Fp_polynomial tmp;
    plain_div_rem(tmp, r, a, b);
}


void plain_div(Fp_polynomial &q, const Fp_polynomial &a, const Fp_polynomial &b)
{
    q.MOD = a.MOD;
    lidia_size_t deg_b = b.degree(), deg_a = a.degree();
    lidia_size_t deg_q = deg_a - deg_b;
    
    if (deg_b < 0)
	lidia_error_handler( "Fp_polynomial", "plain_div ( Fp_polynomial&, Fp_polynomial&, Fp_polynomial& )::division by zero" );

    if (deg_q < 0)
    {
	q.assign_zero();
	return;
    }
    
    if (deg_q <= deg_b/2)
    {/* variant 1 (multiplying with 'inverse'); faster if (deg_a/deg_b < 1.5)*/
	Fp_polynomial P1, P2, P3;
	copy_reverse(P3, b, 0, deg_b);
	invert(P2, P3, deg_q + 1);
	copy_reverse(P1, P2, 0, deg_q);

	shift_right(P3, a, deg_b);
	multiply(P2, P3, P1);
	trunc(P2, P2, 2*deg_q+1);
	shift_right(q, P2, deg_q);
    }/* end variant 1 */
    else
    {/* variant 2 (plain); cmp. plain_div_rem(...) */
	const bigint& p = a.modulus();
	lidia_size_t i,j;
	int lc_is_one;
	bigint LCInv, t, s;
	const bigint *bp;
	
	if (b.lead_coeff().is_one())
	    lc_is_one = 1;
	else
	{
	    lc_is_one = 0;
	    InvMod(LCInv, b.lead_coeff(), p);
	}

	Fp_polynomial lb;
        if (&q == &b)
	{
	    lb = b;
	    bp = lb.coeff;
	}
	else
	    bp = b.coeff;

	bigint *xp = new bigint[deg_a+1];
	if (!xp)
	    lidia_error_handler( "Fp_polynomial", "plain_div_rem::out of memory" );
	for (i = 0; i <= deg_a; i++)
	    xp[i].assign( a.coeff[i] );

	q.set_degree(deg_q);
	bigint *qp = q.coeff;

	for (i = deg_q; i >= 0; i--)
	{
	    Remainder(t, xp[i+deg_b], p);
	    if (!lc_is_one)
		MulMod(t, t, LCInv, p);
	    Remainder(qp[i], t, p);

	    if (i > 0)
	    {
		negate(t, t);
		for (j = deg_b - 1; j >= 0; j--)
		{
		    multiply(s, t, bp[j]);
		    add(xp[i+j], xp[i+j], s);
		}
	    }
	}/* end for */
	delete[] xp;
    }/* end variant 2 */
}



// x = (1/a) % X^m, input not output, constant term a is nonzero
void plain_inv(Fp_polynomial& x, const Fp_polynomial& a, lidia_size_t m)
{
    debug_handler( "Fp_polynomial", "plain_inv( Fp_polynomial&, Fp_polynomial&, lidia_size_t )" );
    x.MOD = a.MOD;

    lidia_size_t i, k, n, lb;
    bigint v, t;
    bigint s; // static
    const bigint &p = a.modulus();

    const bigint* ap;
    bigint* xp;

    n = a.degree();
    if (n < 0) lidia_error_handler( "Fp_polynomial", "plain_inv( Fp_polynomial&, Fp_polynomial&, lidia_size_t )::division by zero" );

    InvMod(s, a.const_term(), p);

    if (n == 0)
    {
	x.set_degree(0);
	x.coeff[0].assign( s );
        return;
    }

    ap = a.coeff;
    x.set_degree(m-1);
    xp = x.coeff;

    xp[0] = s;
    bool ct_is_one = s.is_one();

    for (k = 1; k < m; k++)
    {
        v.assign_zero();
        lb = comparator<lidia_size_t>::max(k-n, 0);
        for (i = lb; i <= k-1; i++)
        {
            multiply(t, xp[i], ap[k-i]);
            add(v, v, t);
        }
        Remainder(xp[k], v, p);
        NegateMod(xp[k], xp[k], p);
        if (!ct_is_one)
            MulMod(xp[k], xp[k], s, p);
    }
    x.remove_leading_zeros();
}

