#include "fmpz_mpoly_factor.h"
#include "n_poly.h"


static void fmpz_mpoly_convert_perm(
    fmpz_mpoly_t A,
    flint_bitcnt_t Abits,
    const fmpz_mpoly_ctx_t Actx,
    const fmpz_mpoly_t B,
    const fmpz_mpoly_ctx_t Bctx,
    const slong * perm)
{
    slong n = Bctx->minfo->nvars;
    slong m = Actx->minfo->nvars;
    slong i, k, l;
    slong NA, NB;
    ulong * Aexps;
    ulong * Bexps;
    TMP_INIT;

    FLINT_ASSERT(A->bits <= FLINT_BITS);
    FLINT_ASSERT(B->bits <= FLINT_BITS);
    FLINT_ASSERT(B->length > 0);
    TMP_START;

    Aexps = (ulong *) TMP_ALLOC(m*sizeof(ulong));
    Bexps = (ulong *) TMP_ALLOC(n*sizeof(ulong));

    NA = mpoly_words_per_exp(Abits, Actx->minfo);
    NB = mpoly_words_per_exp(B->bits, Bctx->minfo);

    fmpz_mpoly_fit_length_reset_bits(A, B->length, Abits, Actx);
    A->length = B->length;
    for (i = 0; i < B->length; i++)
    {        
        fmpz_set(A->coeffs + i, B->coeffs + i);
        mpoly_get_monomial_ui(Bexps, B->exps + NB*i, B->bits, Bctx->minfo);
        for (k = 0; k < m; k++)
        {
            l = perm[k];
            Aexps[k] = l < 0 ? 0 : Bexps[l];
        }
        mpoly_set_monomial_ui(A->exps + NA*i, Aexps, Abits, Actx->minfo);
     }  
    TMP_END;
    fmpz_mpoly_sort_terms(A, Actx);
}


static int fmpz_mpoly_evaluate_except_two(
    fmpz_bpoly_t e,
    const fmpz_mpoly_t A,
    const fmpz * alphas,
    slong v,
    const fmpz_mpoly_ctx_t ctx)
{
    int success;
    slong i;
    fmpz_mpoly_t t;

    fmpz_mpoly_init(t, ctx);
    fmpz_mpoly_set(t, A, ctx);

    for (i = 1; i < ctx->minfo->nvars; i++)
    {
        if (i == v)
            continue;

        if (!fmpz_mpoly_evaluate_one_fmpz(t, t, i, alphas + i - 1, ctx))
        {
            success = 0;
            goto cleanup;
        }
    }

    fmpz_mpoly_get_bpoly(e, t, 0, v, ctx);
    success = 1;

cleanup:

    fmpz_mpoly_clear(t, ctx);

    return success;
}

int fmpz_mpoly_evaluate_rest_except_one(
    fmpz_poly_t e,
    const fmpz_mpoly_t A,
    const fmpz * alphas,
    slong v,
    const fmpz_mpoly_ctx_t ctx)
{
    int success;
    slong i;
    fmpz_mpoly_t t;

    fmpz_mpoly_init(t, ctx);
    fmpz_mpoly_set(t, A, ctx);

    for (i = 1; i < ctx->minfo->nvars; i++)
    {
        if (i == v)
            continue;

        if (!fmpz_mpoly_evaluate_one_fmpz(t, t, i, alphas + i - 1, ctx))
        {
            success = 0;
            goto cleanup;
        }
    }

    success = fmpz_mpoly_is_fmpz_poly(t, v, ctx) &&
              fmpz_mpoly_get_fmpz_poly(e, t, v, ctx);

cleanup:

    fmpz_mpoly_clear(t, ctx);

    return success;
}



static void _make_bases_coprime(
    fmpz_poly_factor_t A,
    fmpz_poly_factor_t B)
{
    slong i, j;
    slong Alen = A->num;
    slong Blen = B->num;
    fmpz_poly_t g;

    fmpz_poly_init(g);

    for (i = 0; i < Alen; i++)
    for (j = 0; j < Blen; j++)
    {
        fmpz_poly_gcd(g, A->p + i, B->p + j);
        if (fmpz_poly_degree(g) > 0)
        {
            fmpz_poly_div(A->p + i, A->p + i, g);
            fmpz_poly_div(B->p + j, B->p + j, g);
            fmpz_poly_factor_fit_length(A, A->num + 1);
            fmpz_poly_set(A->p + A->num, g);
            A->exp[A->num] = A->exp[i];
            A->num++;
            fmpz_poly_factor_fit_length(B, B->num + 1);
            fmpz_poly_set(B->p + B->num, g);
            B->exp[B->num] = B->exp[j];
            B->num++;
        }
    }

    for (i = 0; i < A->num; i++)
    {
        if (fmpz_poly_degree(A->p + i) > 0)
            continue;
        A->num--;
        fmpz_poly_swap(A->p + i, A->p + A->num);
        SLONG_SWAP(A->exp[i], A->exp[A->num]);
        i--;
    }

    for (i = 0; i < B->num; i++)
    {
        if (fmpz_poly_degree(B->p + i) > 0)
            continue;
        B->num--;
        fmpz_poly_swap(B->p + i, B->p + B->num);
        SLONG_SWAP(B->exp[i], B->exp[B->num]);
        i--;
    }

    fmpz_poly_clear(g);
}

void fmpz_poly_vector_insert_poly(fmpz_bpoly_t v, const fmpz_poly_t a)
{
    slong i;

    for (i = 0; i < v->length; i++)
        if (fmpz_poly_equal(v->coeffs + i, a))
            return;

    fmpz_bpoly_fit_length(v, v->length + 1);
    fmpz_poly_set(v->coeffs + v->length, a);
    v->length++;
}


void fmpz_poly_factor_print_pretty(fmpz_poly_factor_t f, const char * x)
{
    slong i;
    fmpz_print(&f->c);
    for (i = 0; i < f->num; i++)
    {
        flint_printf("*(");
        fmpz_poly_print_pretty(f->p + i, x);
        flint_printf(")^%wd", f->exp[i]);
    }
}

static int _try_lift(
    fmpz_mpoly_struct * lifts,  /* length r */
    const fmpz_mpoly_t A,
    fmpz_poly_struct * Auf,     /* length r */
    slong r,
    const fmpz * alphas,
    slong v,
    const fmpz_mpoly_ctx_t Actx)
{
    int success;
    slong i, k;
    slong mvars, nvars = Actx->minfo->nvars;
    slong * Adegs, * Bdegs, * perm, * iperm;
    fmpz * Balphas;
    slong dummyvars[1];
    ulong dummydegs[1];
    fmpz_mpoly_t lcA, At, newA;
    fmpz_mpoly_ctx_t Bctx;
    flint_bitcnt_t Bbits;
    fmpz_mpoly_t Bt;
    fmpz_mpolyv_t fac, tfac;
    fmpz_mpoly_struct * Bevals, * Blcevals;
    fmpz_mpoly_univar_t u;
    fmpz_t q;

    FLINT_ASSERT(0 < v && v < nvars);

    if (r < 2)
    {
        FLINT_ASSERT(r == 1);
        fmpz_mpoly_init(At, Actx);
        fmpz_mpoly_univar_init(u, Actx);
        fmpz_mpoly_to_univar(u, A, v, Actx);
        success = _fmpz_mpoly_vec_content_mpoly(At, u->coeffs, u->length, Actx);
        if (success)
        {
            success = fmpz_mpoly_divides(lifts + 0, A, At, Actx);
            FLINT_ASSERT(success);
            fmpz_mpoly_unit_normalize(lifts + 0, Actx);
        }
        fmpz_mpoly_clear(At, Actx);
        fmpz_mpoly_univar_clear(u, Actx);
        return success;
    }

    Adegs = FLINT_ARRAY_ALLOC(nvars, slong);
    Bdegs = FLINT_ARRAY_ALLOC(nvars, slong);
    perm = FLINT_ARRAY_ALLOC(nvars, slong);
    iperm = FLINT_ARRAY_ALLOC(nvars, slong);
    fmpz_init(q);
    fmpz_mpoly_init(lcA, Actx);
    fmpz_mpoly_init(At, Actx);
    fmpz_mpoly_init(newA, Actx);

    dummyvars[0] = v;
    dummydegs[0] = fmpz_mpoly_degree_si(A, v, Actx);
    fmpz_mpoly_get_coeff_vars_ui(lcA, A, dummyvars, dummydegs, 1, Actx);
    fmpz_mpoly_pow_ui(At, lcA, r - 1, Actx);
    fmpz_mpoly_mul(newA, A, At, Actx);

    if (newA->bits >= FLINT_BITS)
    {
        success = 0;
        goto cleanup_less;
    }

    fmpz_mpoly_degrees_si(Adegs, newA, Actx);

    perm[0] = v;
    mvars = 1;
    for (i = 0; i < nvars; i++)
    {
        if (i == v)
            continue;
        iperm[i] = -1;
        if (Adegs[i] > 0)
        {
            FLINT_ASSERT(i > 0);
            perm[mvars] = i;
            mvars++;
        }
    }

    /* TODO following code works if mvars = 1, but add special case? */

    fmpz_mpoly_ctx_init(Bctx, mvars, ORD_LEX);
    fmpz_mpoly_init(Bt, Bctx);
    fmpz_mpolyv_init(fac, Bctx);
    fmpz_mpolyv_init(tfac, Bctx);
    fmpz_mpoly_univar_init(u, Bctx);
    Balphas = _fmpz_vec_init(nvars);
    Bevals = FLINT_ARRAY_ALLOC(mvars, fmpz_mpoly_struct);
    Blcevals = FLINT_ARRAY_ALLOC(mvars, fmpz_mpoly_struct);
    for (i = 0; i < mvars; i++)
    {
        fmpz_mpoly_init(Bevals + i, Bctx);
        fmpz_mpoly_init(Blcevals + i, Bctx);
    }

    Bbits = mpoly_fix_bits(newA->bits, Bctx->minfo);

    /* invert perm */
    for (i = 0; i < mvars; i++)
    {
        iperm[perm[i]] = i;
        Bdegs[i] = Adegs[perm[i]];
        if (i > 0)
            fmpz_set(Balphas + i - 1, alphas + perm[i] - 1);
    }

    fmpz_mpoly_convert_perm(Bevals + mvars - 1, Bbits, Bctx, newA, Actx, perm);
    fmpz_mpoly_convert_perm(Blcevals + mvars - 1, Bbits, Bctx, lcA, Actx, perm);

    for (i = mvars - 2; i >= 0; i--)
    {
        fmpz_mpoly_evaluate_one_fmpz(Bevals + i, Bevals + i + 1, i + 1, Balphas + i, Bctx);
        fmpz_mpoly_evaluate_one_fmpz(Blcevals + i, Blcevals + i + 1, i + 1, Balphas + i, Bctx);
    }

    fmpz_mpolyv_fit_length(fac, r, Bctx);
    fac->length = r;
    for (i = 0; i < r; i++)
    {
        FLINT_ASSERT(fmpz_mpoly_is_fmpz(Blcevals + 0, Bctx));
        FLINT_ASSERT(fmpz_mpoly_length(Blcevals + 0, Bctx) == 1);
        FLINT_ASSERT(fmpz_divisible(Blcevals[0].coeffs + 0, Auf[i].coeffs + Auf[i].length - 1));
        fmpz_divexact(q, Blcevals[0].coeffs + 0, Auf[i].coeffs + Auf[i].length - 1);
        _fmpz_mpoly_set_fmpz_poly(fac->coeffs + i, Bbits, Auf[i].coeffs, Auf[i].length, 0, Bctx);
        fmpz_mpoly_scalar_mul_fmpz(fac->coeffs + i, fac->coeffs + i, q, Bctx);
    }

    fmpz_mpolyv_fit_length(tfac, r, Bctx);
    tfac->length = r;
    for (k = 1; k <= mvars - 1; k++)
    {
        for (i = 0; i < r; i++)
            _fmpz_mpoly_set_lead0(tfac->coeffs + i, fac->coeffs + i, Blcevals + k, Bctx);

        success = fmpz_mpoly_hlift(k, tfac->coeffs, r, Balphas, Bevals + k, Bdegs, Bctx);
        if (!success)
            goto cleanup_more;

        fmpz_mpolyv_swap(tfac, fac, Bctx);
    }

    for (i = 0; i < r; i++)
    {
        fmpz_mpoly_to_univar(u, fac->coeffs + i, 0, Bctx);
        success = _fmpz_mpoly_vec_content_mpoly(Bt, u->coeffs, u->length, Bctx);
        if (!success)
            goto cleanup_more;
        success = fmpz_mpoly_divides(Bt, fac->coeffs + i, Bt, Bctx);
        FLINT_ASSERT(success);
        fmpz_mpoly_convert_perm(lifts + i, A->bits, Actx, Bt, Bctx, iperm);
        fmpz_mpoly_unit_normalize(lifts + i, Actx);
    }

    success = 1;

cleanup_more:

    fmpz_mpoly_clear(Bt, Bctx);
    fmpz_mpolyv_clear(fac, Bctx);
    fmpz_mpolyv_clear(tfac, Bctx);
    fmpz_mpoly_univar_clear(u, Bctx);
    _fmpz_vec_clear(Balphas, nvars);
    for (i = 0; i < mvars; i++)
    {
        fmpz_mpoly_clear(Bevals + i, Bctx);
        fmpz_mpoly_clear(Blcevals + i, Bctx);
    }
    flint_free(Bevals);
    flint_free(Blcevals);

    fmpz_mpoly_ctx_clear(Bctx);

cleanup_less:

    flint_free(Adegs);
    flint_free(Bdegs);
    flint_free(perm);
    flint_free(iperm);

    fmpz_clear(q);
    fmpz_mpoly_clear(lcA, Actx);
    fmpz_mpoly_clear(At, Actx);
    fmpz_mpoly_clear(newA, Actx);

    return success;
}

/* assume content(b) = 1 for now */
void fmpz_mpoly_factor_divexact_mpoly_pow_ui(
    fmpz_mpoly_factor_t A,
    const fmpz_mpoly_t b_in,
    ulong e,
    const fmpz_mpoly_ctx_t ctx)
{
    int sgn;
    slong i;
    fmpz_mpoly_t b_copy;
    fmpz_mpoly_struct * b = (fmpz_mpoly_struct *) b_in;

    fmpz_mpoly_init(b_copy, ctx);

    i = 0; /* index strickly before which everything is coprime to b */

    while (i < A->num && !fmpz_mpoly_is_fmpz(b, ctx))
    {
        fmpz_mpoly_factor_fit_length(A, A->num + 1, ctx);
        fmpz_mpoly_gcd_cofactors(A->poly + A->num, A->poly + i, b_copy,
                                                          A->poly + i, b, ctx);
        b = b_copy;
        if (fmpz_mpoly_is_fmpz(A->poly + A->num, ctx))
        {
            i++;
            continue;
        }

        fmpz_sub_ui(A->exp + A->num, A->exp + i, e);
        sgn = fmpz_sgn(A->exp + A->num);
        if (sgn < 0)
        {
            flint_printf("non-exact division fmpz_mpoly_factor_divexact_mpoly_pow_ui");
            flint_abort();
        }
        else if (sgn > 0)
        {
            A->num++;
        }

        if (fmpz_mpoly_is_fmpz(A->poly + i, ctx))
        {
            A->num--;
            fmpz_mpoly_swap(A->poly + i, A->poly + A->num, ctx);
            fmpz_swap(A->exp + i, A->exp + A->num);
        }
        else
        {
            i++;
        }
    }

    if (!fmpz_mpoly_is_fmpz(b, ctx))
    {
        flint_printf("non-exact division fmpz_mpoly_factor_divexact_mpoly_pow_ui");
        flint_abort();
    }

    fmpz_mpoly_clear(b_copy, ctx);
}



int fmpz_mpoly_factor_lcc_kaltofen_step(
    fmpz_mpoly_struct * divs,   /* length r */
    slong r,
    fmpz_mpoly_factor_t Af, /* squarefree factorization of A */
    const fmpz_poly_struct * Au,
    slong v,                      /* minor bivar var*/
    const fmpz * alphas,
    const fmpz_mpoly_ctx_t ctx)
{
    int success;
    slong i, j, k, Af_deg_v;
    fmpz_poly_factor_struct * Auf;
    fmpz_mpoly_t Afp, tt;
    fmpz_mpolyv_t lfp;
    fmpz_bpoly_t f;
    fmpz_poly_t t, fp;

    fmpz_mpoly_init(Afp, ctx);
    fmpz_mpoly_init(tt, ctx);
    fmpz_bpoly_init(f);
    fmpz_poly_init(fp);
    fmpz_poly_init(t);
    fmpz_mpolyv_init(lfp, ctx);
    Auf = FLINT_ARRAY_ALLOC(r, fmpz_poly_factor_struct);
    for (i = 0; i < r; i++)
    {
        fmpz_poly_factor_init(Auf + i);
        fmpz_poly_factor_squarefree(Auf + i, Au + i);
    }

    Af_deg_v = 0;
    fmpz_mpoly_one(Afp, ctx);
    for (i = 0; i < Af->num; i++)
    {
        slong this_degree = fmpz_mpoly_degree_si(Af->poly + i, v, ctx);
        Af_deg_v += this_degree;
        /* we don't need to introduce needless content into Afp */
        if (this_degree != 0)
            fmpz_mpoly_mul(Afp, Afp, Af->poly + i, ctx);
    }

    if (!fmpz_mpoly_evaluate_rest_except_one(t, Afp, alphas, v, ctx) ||
        fmpz_poly_degree(t) < 1 ||
        fmpz_poly_degree(t) != Af_deg_v)
    {
        success = 0;
        goto cleanup;
    }

    for (i = 0; i < r - 1; i++)
    for (j = i + 1; j < r; j++)
        _make_bases_coprime(Auf + i, Auf + j);

    f->length = 0;
    for (i = 0; i < r; i++)
    for (j = 0; j < Auf[i].num; j++)
        fmpz_poly_vector_insert_poly(f, Auf[i].p + j);

    fmpz_poly_primitive_part(t, t);

    fmpz_poly_one(fp);
    for (i = 0; i < f->length; i++)
        fmpz_poly_mul(fp, fp, f->coeffs + i);

    success = fmpz_poly_equal(fp, t);
    if (!success)
        goto cleanup;

    fmpz_mpolyv_fit_length(lfp, f->length, ctx);
    lfp->length = f->length;
    success = _try_lift(lfp->coeffs, Afp, f->coeffs, f->length, alphas, v, ctx);
    if (!success)
        goto cleanup;

    for (i = 0; i < r; i++)
    {
        fmpz_poly_factor_struct * auf = Auf + i;
        for (j = 0; j < auf->num; j++)
        {
            for (k = 0; k < f->length; k++)
            {
                if (fmpz_poly_equal(f->coeffs + k, auf->p + j))
                {
                    fmpz_mpoly_factor_divexact_mpoly_pow_ui(Af,
                                            lfp->coeffs + k, auf->exp[j], ctx);
                    fmpz_mpoly_pow_ui(tt, lfp->coeffs + k, auf->exp[j], ctx);
                    fmpz_mpoly_mul(divs + i, divs + i, tt, ctx);
                }
            }
        }
    }

    success = 1;

cleanup:

    fmpz_mpoly_clear(Afp, ctx);
    fmpz_mpoly_clear(tt, ctx);
    fmpz_bpoly_clear(f);
    fmpz_poly_clear(fp);
    fmpz_poly_clear(t);
    fmpz_mpolyv_clear(lfp, ctx);
    for (i = 0; i < r; i++)
        fmpz_poly_factor_clear(Auf + i);
    flint_free(Auf);

    return success;
}


/*
    return 1: lcc is probably correct and complete
           0: lcc is incomplete
          -1: alphas are definitely bad
*/
int fmpz_mpoly_factor_lcc_kaltofen(
    fmpz_mpoly_struct * divs,
    const fmpz_mpoly_factor_t lcAf_,
    const fmpz_mpoly_t A,
    slong r,
    const fmpz * alphas,
    slong * degs,
    const fmpz_poly_factor_t uf,
    const fmpz_mpoly_ctx_t ctx)
{
    int success;
    slong i, v, nvars = ctx->minfo->nvars;
    fmpz_mpoly_factor_t lcAf;
    fmpz_poly_struct * ulcs;
    fmpz_tpoly_struct * bfacs;
    fmpz_bpoly_t beval;
    fmpz_poly_t bcont, ut2;
    fmpz_t g1, g2, g3;
    fmpz * content_divs;

    FLINT_ASSERT(r > 1);

    fmpz_mpoly_factor_init(lcAf, ctx);
    fmpz_mpoly_factor_set(lcAf, lcAf_, ctx);

    fmpz_init(g1);
    fmpz_init(g2);
    fmpz_init(g3);

    content_divs = _fmpz_vec_init(r);

    fmpz_poly_init(ut2);
    fmpz_poly_init(bcont);
    fmpz_bpoly_init(beval);
    bfacs = FLINT_ARRAY_ALLOC(nvars, fmpz_tpoly_struct);
    for (i = 0; i < nvars; i++)
        fmpz_tpoly_init(bfacs + i);

    ulcs = FLINT_ARRAY_ALLOC(r, fmpz_poly_struct);
    for (i = 0; i < r; i++)
    {
        fmpz_poly_init(ulcs + i);
        fmpz_mpoly_one(divs + i, ctx);
    }

    for (v = 1; v < nvars; v++)
    {
        fmpz_mpoly_evaluate_except_two(beval, A, alphas, v, ctx);
        FLINT_ASSERT(fmpz_bpoly_degree0(beval) == degs[0]);
        if (fmpz_bpoly_degree1(beval) != degs[v])
        {
            bfacs[v].length = 0;
            goto continue_outer;
        }

        success = fmpz_bpoly_factor_ordered(bcont, bfacs + v, beval,
                                                           alphas + v - 1, uf);
        if (success < 1)
        {
            if (success == 0)
            {
                success = -1;
                goto cleanup;
            }

            bfacs[v].length = 0;
            goto continue_outer;
        }

        for (i = 0; i < r; i++)
        {
            fmpz_mpoly_evaluate_rest_except_one(ut2, divs + i, alphas, v, ctx);
            if (fmpz_poly_is_zero(ut2))
            {
                bfacs[v].length = 0;
                goto continue_outer;
            }

            fmpz_poly_primitive_part(ut2, ut2);
            if (!fmpz_poly_divides(ulcs + i, fmpz_bpoly_lead(bfacs[v].coeffs + i), ut2))
            {
                bfacs[v].length = 0;
                goto continue_outer;
            }
        }

        fmpz_mpoly_factor_lcc_kaltofen_step(divs, r, lcAf, ulcs, v, alphas, ctx);
continue_outer:;
    }

    if (lcAf->num == 0)
    {
        for (i = 0; i < r; i++)
            fmpz_one(content_divs + i);

        for (v = 1; v < nvars; v++)
        {
            if (bfacs[v].length != r)
                continue;

            for (i = 0; i < r; i++)
            {
                fmpz_poly_struct * ut1 = fmpz_bpoly_lead(bfacs[v].coeffs + i);
                fmpz_mpoly_evaluate_rest_except_one(ut2, divs + i, alphas, v, ctx);
                _fmpz_vec_content(g1, ut1->coeffs, ut1->length);
                _fmpz_vec_content(g2, ut2->coeffs, ut2->length);
                fmpz_gcd(g3, g1, g2);
                fmpz_divexact(g1, g1, g3);
                fmpz_lcm(content_divs + i, content_divs + i, g1);
            }
        }

        for (i = 0; i < r; i++)
        {
            if (!fmpz_divisible(lcAf->constant, content_divs + i))
                continue;
            fmpz_divexact(lcAf->constant, lcAf->constant, content_divs + i);
            fmpz_mpoly_scalar_mul_fmpz(divs + i, divs + i, content_divs + i, ctx);
        }

        success = 1;
    }
    else
    {
        success = 0;
    }

cleanup:

    fmpz_mpoly_factor_clear(lcAf, ctx);

    fmpz_clear(g1);
    fmpz_clear(g2);
    fmpz_clear(g3);

    _fmpz_vec_clear(content_divs, r);

    fmpz_poly_clear(ut2);
    fmpz_poly_clear(bcont);
    fmpz_bpoly_clear(beval);
    for (i = 0; i < nvars; i++)
        fmpz_tpoly_clear(bfacs + i);
    flint_free(bfacs);

    for (i = 0; i < r; i++)
        fmpz_poly_clear(ulcs + i);
    flint_free(ulcs);

    return success;
}

