#include "internal.h"

/***************** Howell form **********************/

/** @brief Compute u s.t. gcd(u, mod) = 1 and ux = gcd(x, mod)
 */
static int unit(ibz_t *unit, ibz_t *gcd, const ibz_t *x, const ibz_t *mod) {
    if (ibz_is_zero(x))
        return 0;
    ibz_t stab, nmod, nmod2, thrash, tmp;
    ibz_init(&stab); ibz_init(&nmod); ibz_init(&nmod2); ibz_init(&thrash); ibz_init(&tmp);
    ibz_xgcd(gcd, unit, &thrash, x, mod);
    ibz_div(&nmod, &thrash, mod, gcd);
    // Stabilizer(unit, nmod)
    ibz_gcd(&stab, unit, &nmod);
    ibz_div(&nmod2, &thrash, mod, &stab);
    ibz_div(&stab, &thrash, unit, &stab);
    // Split(nmod2, stab)
    for (int i = ibz_bitsize(&nmod2); i > 0; i >>= 1) {
        ibz_mul(&stab, &stab, &stab);
        ibz_mod(&stab, &stab, &nmod2);
    }
    ibz_gcd(&stab, &stab, &nmod2);
    ibz_div(&stab, &thrash, &nmod2, &stab);
#ifndef NDEBUG
    ibz_mul(&thrash, &stab, &nmod);
    ibz_add(&thrash, unit, &thrash);
    ibz_gcd(&thrash, &thrash, mod);
    ibz_gcd(&tmp, unit, &nmod);
    assert(ibz_cmp(&thrash, &tmp) == 0);
#endif
    // Finish off
    ibz_mul(&stab, &stab, &nmod);
    ibz_add(unit, unit, &stab);
    ibz_mod(unit, unit, mod);
#ifndef NDEBUG
    ibz_gcd(&stab, unit, mod);
    assert(ibz_is_one(&stab));
    ibz_mul(&stab, unit, x);
    ibz_mod(&stab, &stab, mod);
    assert(ibz_cmp(&stab, gcd) == 0);
#endif
    ibz_finalize(&stab); ibz_finalize(&nmod); ibz_finalize(&nmod2); ibz_finalize(&thrash); ibz_finalize(&tmp);
    return 1;
}

/** @brief Linear combination of two columns
 *
 * (mat[][j] | mat[][k]) <- (mat[][j] | mat[][k]) * U    (mod)
 *
 * only update columns between start (included) and end (excluded)
 */
static void gen_elem(int rows, int cols, ibz_t mat[rows][cols], int j, int k, int start, int end, const ibz_mat_2x2_t *U, const ibz_t *mod)
{
    ibz_t tmp1, tmp2;
    ibz_init(&tmp1); ibz_init(&tmp2);
    for (int i = start; i < end; i++) {
        ibz_mul(&tmp1, &mat[i][j], &(*U)[0][0]);
        ibz_mul(&tmp2, &mat[i][k], &(*U)[1][0]);
        ibz_add(&tmp1, &tmp1, &tmp2);
        
        ibz_mul(&tmp2, &mat[i][j], &(*U)[0][1]);
        ibz_mul(&mat[i][k], &mat[i][k], &(*U)[1][1]);
        ibz_add(&mat[i][k], &tmp2, &mat[i][k]);
        
        ibz_mod(&mat[i][j], &tmp1, mod);
        ibz_mod(&mat[i][k], &mat[i][k], mod);
    }
    ibz_finalize(&tmp1); ibz_finalize(&tmp2);
}

/** @brief Swap columns j and k of mat
 */
static inline void swap_col(int rows, int cols, ibz_t mat[rows][cols], int j, int k)
{
    ibz_t tmp;
    ibz_init(&tmp);
    for (int i = 0; i < rows; i++) {
        ibz_copy(&tmp, &mat[i][j]);
        ibz_copy(&mat[i][j], &mat[i][k]);
        ibz_copy(&mat[i][k], &tmp);
    }
    ibz_finalize(&tmp);
}

/** @brief Check if column is all zeros
 */
static inline int is_col_zero(int rows, int cols, ibz_t mat[rows][cols], int j)
{
    int is_zero = 1;
    for (int i = 0; i < rows; i++)
        is_zero &= ibz_is_zero(&mat[i][j]);
    return is_zero;
}

/** Check that mat * trans = howell
 */
static int howell_check_matrices(int rows, int cols, const ibz_t howell[rows][rows+1], const ibz_t trans[rows+1][rows+1], const ibz_t mat[rows][cols], ibz_t *mod)
{
    const int extra = rows + 1 - cols;
    ibz_t test[rows][rows+1], res[rows][rows+1];
    ibz_mat_init(rows, rows+1, test);
    ibz_mat_init(rows, rows+1, res);
    
    // copy mat to the right of test
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            ibz_mod(&test[i][j+extra], &mat[i][j], mod);

    ibz_mat_mulmod(rows, rows+1, rows+1, res, test, trans, mod);

    int ok = 1;
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < rows+1; j++)
            ok &= ibz_cmp(&howell[i][j], &res[i][j]) == 0;

    ibz_mat_finalize(rows, rows+1, res);
    ibz_mat_finalize(rows, rows+1, test);

    return ok;
}

void ibz_mat_mulmod(int from, int through, int to, ibz_t res[from][to], const ibz_t A[from][through], const ibz_t B[through][to], const ibz_t *mod)
{
    ibz_t tmp;
    ibz_init(&tmp);
    for (int i = 0; i < from; i++) {
        for (int j = 0; j < to; j++) {
            ibz_set(&res[i][j], 0);
            for (int k = 0; k < through; k++) {
                ibz_mul(&tmp, &A[i][k], &B[k][j]);
                ibz_add(&res[i][j], &res[i][j], &tmp);
                ibz_mod(&res[i][j], &res[i][j], mod);
            }
        }
    }
    ibz_finalize(&tmp);
}

int ibz_mat_howell(int rows, int cols, ibz_t howell[rows][rows+1], ibz_t trans[rows+1][rows+1], const ibz_t mat[rows][cols], ibz_t *mod) {
    assert(cols <= rows);
    const int extra = rows + 1 - cols;

    ibz_mat_2x2_t U;
    ibz_t gcd, u, q;
    ibz_mat_2x2_init(&U);
    ibz_init(&gcd); ibz_init(&u); ibz_init(&q);
    
    // copy mat to the right of howell
    for (int i = 0; i < rows; i++) {
        for (int j = cols; j < rows+1; j++)
            ibz_set(&howell[i][j-cols], 0);
        for (int j = 0; j < cols; j++)
            ibz_mod(&howell[i][j+extra], &mat[i][j], mod);
    }
    // initialize trans to identity
    if (trans) {
        for (int i = 0; i < rows+1; i++) {
            for (int j = 0; j < rows+1; j++)
                if (j != i) ibz_set(&trans[i][j], 0);
            ibz_set(&trans[i][i], 1);
        }
        assert(howell_check_matrices(rows, cols, howell, trans, mat, mod));
    }
    
    // Put in upper triangular form
    for (int i = rows-1; i >= extra-1; i--) {
        for (int j = extra; j <= i; j++) {
            if (ibz_is_zero(&howell[i][j]))
                continue;
            ibz_xgcd_ann(&gcd, &U[0][0], &U[1][0], &U[0][1], &U[1][1], &howell[i][j], &howell[i][i+1]);
            gen_elem(rows, rows+1, howell, j, i+1, 0, i, &U, mod);
            ibz_set(&howell[i][j], 0);
            ibz_copy(&howell[i][i+1], &gcd);
            //
            if (trans) {
                gen_elem(rows+1, rows+1, trans, j, i+1, 0, rows+1, &U, mod);
                assert(howell_check_matrices(rows, cols, howell, trans, mat, mod));
            }
        }
    }
    
    // Put in reduced Howell form
    for (int i = rows-1; i >= 0; i--) {
        /* normalize diagonal coefficient */
        if (unit(&u, &gcd, &howell[i][i+1], mod)) {
            for (int k = 0; k < i; k++) {
                ibz_mul(&howell[k][i+1], &howell[k][i+1], &u);
                ibz_mod(&howell[k][i+1], &howell[k][i+1], mod);
            }
            ibz_copy(&howell[i][i+1], &gcd);
            //
            if (trans) {
                for (int k = 0; k < rows+1; k++) {
                    ibz_mul(&trans[k][i+1], &trans[k][i+1], &u);
                    ibz_mod(&trans[k][i+1], &trans[k][i+1], mod);
                }
                assert(howell_check_matrices(rows, cols, howell, trans, mat, mod));
            }
        }
        
        /* reduce right of the diagonal */
        ibz_t *pivot = &howell[i][i+1];
        if (!ibz_is_zero(pivot)) {
            for (int j = i+2; j < rows+1; j++) {
                assert(ibz_cmp(pivot, &ibz_const_zero) > 0);
                ibz_div(&q, &howell[i][j], &howell[i][j], pivot);
                // howell[][j] -= q howell[][i+1]
                for (int k = 0; k < i; k++) {
                    ibz_mul(&u, &q, &howell[k][i+1]);
                    ibz_sub(&howell[k][j], &howell[k][j], &u);
                    ibz_mod(&howell[k][j], &howell[k][j], mod);
                }
                // trans[][j] -= q trans[][i+1]
                if (trans) {
                    for (int k = 0; k < rows+1; k++) {
                        ibz_mul(&u, &q, &trans[k][i+1]);
                        ibz_sub(&trans[k][j], &trans[k][j], &u);
                        ibz_mod(&trans[k][j], &trans[k][j], mod);
                    }
                    assert(howell_check_matrices(rows, cols, howell, trans, mat, mod));
                }
            }
        }
        
        /* ensure Howell property */
        if (i > 0) {
            ibz_gcd(&gcd, pivot, mod);
            if (!ibz_is_one(&gcd)) {
                // Ann(pivot)
                ibz_div(&u, &gcd, mod, &gcd);
                for (int k = 0; k < rows; k++) {
                    if (k < i) {
                        ibz_mul(&howell[k][0], &howell[k][i+1], &u);
                        ibz_mod(&howell[k][0], &howell[k][0], mod);
                    } else {
                        ibz_set(&howell[k][0], 0);
                    }
                }
                // trans[][0] += u trans[][i+1]
                if (trans) {
                    for (int k = 0; k < rows+1; k++) {
                        ibz_mul(&q, &u, &trans[k][i+1]);
                        ibz_add(&trans[k][0], &trans[k][0], &q);
                        ibz_mod(&trans[k][0], &trans[k][0], mod);
                    }
                    assert(howell_check_matrices(rows, cols, howell, trans, mat, mod));
                }
                
                for (int i2 = i-1; i2 >= 0; i2--) {
                    if (ibz_is_zero(&howell[i2][0]))
                        continue;
                    if (ibz_is_zero(&howell[i2][i2+1])) {
                        swap_col(rows, rows+1, howell, 0, i2+1);
                        if (trans) {
                            swap_col(rows+1, rows+1, trans, 0, i2+1);
                            assert(howell_check_matrices(rows, cols, howell, trans, mat, mod));
                        }
                        continue;
                    }
                    ibz_xgcd_ann(&gcd, &U[0][0], &U[1][0], &U[0][1], &U[1][1], &howell[i2][0], &howell[i2][i2+1]);
                    gen_elem(rows, rows+1, howell, 0, i2+1, 0, i2, &U, mod);
                    ibz_set(&howell[i2][0], 0);
                    ibz_copy(&howell[i2][i2+1], &gcd);
                    //
                    if (trans) {
                        gen_elem(rows, rows+1, trans, 0, i2+1, 0, rows+1, &U, mod);
                        assert(howell_check_matrices(rows, cols, howell, trans, mat, mod));
                    }
                }
            }
        }
    }

    /* put zero columns first */
    int read, write;
    for (read = rows, write = rows; read >= 1; read--) {
        if (!is_col_zero(rows, rows+1, howell, read)) {
            if (read < write) {
                swap_col(rows, rows+1, howell, read, write);
                if (trans) {
                    swap_col(rows+1, rows+1, trans, read, write);
                    assert(howell_check_matrices(rows, cols, howell, trans, mat, mod));
                }
            }
            write--;
        }
    }
            
    // Finalize
    ibz_mat_2x2_finalize(&U);
    ibz_finalize(&gcd); ibz_finalize(&u); ibz_finalize(&q);

    return write + 1;
}

void ibz_mat_right_ker_mod(int rows, int cols, ibz_t ker[cols][cols], const ibz_t mat[rows][cols], ibz_t *mod)
{
    assert(cols <= rows);
    const int extra = rows + 1 - cols;

    ibz_t tmp;
    ibz_mat_2x2_t U;
    ibz_t howell[rows][rows+1], trans[rows+1][rows+1], preker[rows+1][rows+1], near_ker[cols][rows+1];
    
    ibz_init(&tmp);
    ibz_mat_2x2_init(&U);
    ibz_mat_init(rows, rows+1, howell);
    ibz_mat_init(rows+1, rows+1, trans);
    ibz_mat_init(rows+1, rows+1, preker);
    ibz_mat_init(cols, rows+1, near_ker);

    // Compute Howell form of mat
    int zeros = ibz_mat_howell(rows, cols, howell, trans, mat, mod);

    // Compute right kernel of Howell form
    for (int j = rows, i = rows-1; j >= 0; j--) {
        while (i >= 0 && ibz_is_zero(&howell[i][j]))
            i--;
        if (i < 0) {
            ibz_set(&preker[j][j], 1);
            continue;
        }
        ibz_t *pivot = &howell[i][j];

        // Ann(pivot)
        ibz_gcd(&tmp, pivot, mod);
        if (!ibz_is_one(&tmp))
            ibz_div(&preker[j][j], &tmp, mod, &tmp);

        for (int j2 = j+1; j2 <= rows; j2++) {
            // howell[i][j+1..rows] * preker[j+1..rows][j2]
            for (int k = j+1; k <= rows; k++) {
                ibz_mul(&tmp, &howell[i][k], &preker[k][j2]);
                ibz_add(&preker[j][j2], &preker[j][j2], &tmp);
            }
            ibz_mod(&preker[j][j2], &preker[j][j2], mod);
            //
            ibz_div(&preker[j][j2], &tmp, &preker[j][j2], pivot);
            assert(ibz_is_zero(&tmp));
            if (!ibz_is_zero(&preker[j][j2]))
                ibz_sub(&preker[j][j2], mod, &preker[j][j2]);
        }
    }

#ifndef NDEBUG
    // Check that preker is indeed a kernel of howell
    ibz_t acc;
    ibz_init(&acc);
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < rows+1; j++) {
            ibz_set(&acc, 0);
            for (int k = 0; k < rows+1; k++) {
                ibz_mul(&tmp, &howell[i][k], &preker[k][j]);
                ibz_add(&acc, &acc, &tmp);
            }
            ibz_mod(&acc, &acc, mod);
            assert(ibz_is_zero(&acc));
        }
    }
    ibz_finalize(&acc);
#endif

    // Apply (bottom part of) transition matrix to computed kernel
    for (int i = 0; i < cols; i++) {
        for (int j = 0; j < rows+1; j++) {
            for (int k = 0; k < rows+1; k++) {
                ibz_mul(&tmp, &trans[i+extra][k], &preker[k][j]);
                ibz_add(&near_ker[i][j], &near_ker[i][j], &tmp);
            }
            ibz_mod(&near_ker[i][j], &near_ker[i][j], mod);
        }
    }

    // Move zero columns to the start
    int read, write;
    for (read = rows, write = rows; read >= 0; read--) {
        if (!is_col_zero(cols, rows+1, near_ker, read)) {
            if (read < write) {
                swap_col(cols, rows+1, near_ker, read, write);
            }
            write--;
        }
    }

    // Put in upper triangular form
    const int diag_shift = rows - cols + 1;  // i-th diagonal is at (i+diag_shift) column
    for (int i = cols-1; i >= 0; i--) {
        for (int j = write+1; j < i+diag_shift; j++) {
            if (ibz_is_zero(&near_ker[i][j]))
                continue;
            ibz_xgcd_ann(&tmp, &U[0][0], &U[1][0], &U[0][1], &U[1][1], &near_ker[i][j], &near_ker[i][i+diag_shift]);
            gen_elem(cols, rows+1, near_ker, j, i+diag_shift, 0, i+1, &U, mod);
            ibz_set(&howell[i][j], 0);
            ibz_copy(&howell[i][i+diag_shift], &tmp);
        }
    }
    
#ifndef NDEBUG
    // Check that ker is indeed a kernel of mat
    ibz_t check[rows][rows+1];
    ibz_mat_init(rows, rows+1, check);
    ibz_mat_mulmod(rows, cols, rows+1, check, mat, near_ker, mod);
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < rows+1; j++)
            assert(ibz_is_zero(&check[i][j]));
    ibz_mat_finalize(rows, rows+1, check);
#endif

    // Copy result
    for (int i = 0; i < cols; i++)
        for (int j = 0; j < cols; j++)
            ibz_copy(&ker[i][j], &near_ker[i][j+rows-cols+1]);
    // Finalize
    ibz_finalize(&tmp);
    ibz_mat_2x2_finalize(&U);
    ibz_mat_finalize(rows, rows+1, howell);
    ibz_mat_finalize(rows+1, rows+1, trans);
    ibz_mat_finalize(rows+1, rows+1, preker);
    ibz_mat_finalize(cols, rows+1, near_ker);
}
