/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.products.ov.mixnet.commons.mathematical.tools;

import static java.util.Arrays.fill;

import java.math.BigInteger;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;
import com.scytl.products.ov.mixnet.commons.tools.ExponentTools;

/**
 *
 */
public class LUDecomposition {

    private static final Logger LOG = LoggerFactory.getLogger("std");

    private final Exponent[][] lu;

    private final int[] pivot;

    private final BigInteger order;

    private boolean singular;

    public LUDecomposition(final Exponent[][] matrix, final BigInteger order) {

        try {
            this.order = order;

            if (!isSquare(matrix)) {
                throw new IllegalArgumentException("A square matrix is required");
            }

            final int m = matrix.length;
            lu = matrix;

            pivot = new int[m];

            for (int row = 0; row < m; row++) {
                pivot[row] = row;
            }

            boolean even = true;
            singular = false;
            Exponent zero = new Exponent(this.order, BigInteger.ZERO);
            // loop over columns
            for (int col = 0; col < m; col++) {
                Exponent sum;

                // upper
                for (int row = 0; row < col; row++) {
                    final Exponent[] luRow = lu[row];
                    sum = luRow[col];
                    for (int i = 0; i < row; i++) {
                        sum = sum.subtract(luRow[i].multiply(lu[i][col]));
                    }
                    luRow[col] = sum;
                }

                // lower
                int max = col; // permutation row
                boolean maxSumIsZero = true;
                for (int row = col; row < m; row++) {
                    final Exponent[] luRow = lu[row];
                    sum = luRow[col];
                    for (int i = 0; i < col; i++) {
                        sum = sum.subtract(luRow[i].multiply(lu[i][col]));
                    }
                    luRow[col] = sum;

                    // avoid zero pivots
                    if (maxSumIsZero && !sum.equals(zero)) {
                        max = row;
                        maxSumIsZero = false;
                    }
                }

                // Singularity check
                if (maxSumIsZero) {
                    singular = true;
                    LOG.error("matrix is singular!");
                    return;
                }

                // Pivot if necessary
                if (max != col) {
                    Exponent tmp;
                    final Exponent[] luMax = lu[max];
                    final Exponent[] luCol = lu[col];
                    for (int i = 0; i < m; i++) {
                        tmp = luMax[i];
                        luMax[i] = luCol[i];
                        luCol[i] = tmp;
                    }
                    int temp = pivot[max];
                    pivot[max] = pivot[col];
                    pivot[col] = temp;
                    even = !even;
                }

                // Divide the lower elements by the "winning" diagonal elt.
                final Exponent luDiag = lu[col][col];
                for (int row = col + 1; row < m; row++) {
                    lu[row][col] = ExponentTools.divide(lu[row][col], luDiag);
                }
            }

        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException(e);
        }
    }

    public static void main(final String[] args) {

        try {
            final BigInteger order = new BigInteger("11");

            Exponent zero = new Exponent(order, BigInteger.ZERO);
            Exponent one = new Exponent(order, BigInteger.ONE);

            Exponent[][] matrixA = {{one, zero }, {one, one } };

            LUDecomposition lu = new LUDecomposition(matrixA, order);
            Exponent[][] matrixB = lu.getInverseMatrix();
            LOG.error("inverse");
            LOG.error(" row | " + matrixB[0][0].getValue() + " " + matrixB[0][1].getValue());
            LOG.error(" row | " + matrixB[1][0].getValue() + " " + matrixB[1][1].getValue());

            LOG.error("result " + ((2607 + 5216 + 1304 + 6519) % 7823));

        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException(e);
        }

    }

    private static boolean isSquare(final Exponent[][] matrix) {
        for (final Exponent[] row : matrix) {
            if (row.length != matrix.length) {
                return false;
            }
        }
        return true;
    }

    private Exponent[][] getIdentityMatrix(final int size) {

        try {
            Exponent[][] result = new Exponent[size][size];
            Exponent zero = new Exponent(order, BigInteger.ZERO);
            Exponent one = new Exponent(order, BigInteger.ONE);
            for (int i = 0; i < size; i++) {
                fill(result[i], zero);
                result[i][i] = one;
            }
            return result;

        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException(e);
        }
    }

    private Exponent[][] solve(final Exponent[][] b) {

        try {

            final int m = pivot.length;
            if (b.length != m) {
                LOG.error(" matrix B doesn't have the right dimensions");
                return null;
            }
            if (singular) {
                LOG.error(" matrix A is singular");
                return null;
            }

            final int nColB = b.length;

            // Apply permutations to b
            final Exponent[][] bp = new Exponent[m][nColB];
            for (int row = 0; row < m; row++) {
                final Exponent[] bpRow = bp[row];
                final int pRow = pivot[row];
                System.arraycopy(b[pRow], 0, bpRow, 0, nColB);
            }

            // Solve LY = b
            for (int col = 0; col < m; col++) {
                final Exponent[] bpCol = bp[col];
                for (int i = col + 1; i < m; i++) {
                    final Exponent[] bpI = bp[i];
                    final Exponent luICol = lu[i][col];
                    for (int j = 0; j < nColB; j++) {
                        bpI[j] = bpI[j].subtract(bpCol[j].multiply(luICol));
                    }
                }
            }

            // Solve UX = Y
            for (int col = m - 1; col >= 0; col--) {
                final Exponent[] bpCol = bp[col];
                final Exponent luDiag = lu[col][col];
                for (int j = 0; j < nColB; j++) {
                    bpCol[j] = ExponentTools.divide(bpCol[j], luDiag);

                }
                for (int i = 0; i < col; i++) {
                    final Exponent[] bpI = bp[i];
                    final Exponent luICol = lu[i][col];
                    for (int j = 0; j < nColB; j++) {
                        bpI[j] = bpI[j].subtract(bpCol[j].multiply(luICol));
                    }
                }
            }

            return bp;

        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException(e);
        }
    }

    public Exponent[][] getInverseMatrix() {
        return solve(getIdentityMatrix(lu.length));
    }
}
