/*
 * 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.tools;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;

/**
 * Defines a set of utility methods related to {@code Exponent}.
 */
public class ExponentTools {
	
	/**
	 * Non-public constructor
	 */
	private ExponentTools() {
	}
	
    public static Exponent innerProduct(final Exponent[] e1, final Exponent[] e2, final BigInteger order,
            final Exponent challengeInnerProduct) {

        try {

            Exponent result = new Exponent(order, BigInteger.ZERO);

            Exponent accumulator = challengeInnerProduct;
            for (int i = 0, length = e1.length; i < length; i++) {
                result = result.add(e1[i].multiply(e2[i]).multiply(accumulator));
                accumulator = accumulator.multiply(challengeInnerProduct);
            }

            return result;

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

    public static Exponent[] hadamardProduct(final Exponent[] e1, final Exponent[] e2) {

        try {

            final Exponent[] result = new Exponent[e1.length];
            for (int i = 0, length = result.length; i < length; i++) {
                result[i] = e1[i].multiply(e2[i]);
            }
            return result;

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

    public static Exponent[] multiplyByScalar(final Exponent[] expo, final Exponent factor) {

        try {

            final Exponent[] result = new Exponent[expo.length];
            for (int i = 0, length = result.length; i < length; i++) {
                result[i] = expo[i].multiply(factor);
            }
            return result;

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

    public static Exponent[] negate(final Exponent[] expo) {

        final Exponent[] result = new Exponent[expo.length];
        for (int i = 0, length = result.length; i < length; i++) {
            result[i] = expo[i].negate();
        }
        return result;
    }

    public static Exponent[] addTwoVectors(final Exponent[] exp1, final Exponent[] exp2) {

        try {

            final Exponent[] result = new Exponent[exp1.length];
            for (int i = 0, length = result.length; i < length; i++) {
                result[i] = exp1[i].add(exp2[i]);
            }
            return result;

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

    public static Exponent[] get0Vector(final int length, final BigInteger order) {

        try {

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

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

    public static Exponent[] get1Vector(final int length, final BigInteger order) {

        try {

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

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

    public static Exponent[] getMinus1Vector(final int length, final BigInteger order) {

        try {

            final Exponent[] result = new Exponent[length];
            final Exponent minusOne = new Exponent(order, new BigInteger("-1"));
            for (int i = 0, size = result.length; i < size; i++) {
                result[i] = minusOne;
            }
            return result;

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

    }

    public static boolean compareTwoVectors(final Exponent[] exp1, final Exponent[] exp2) {

        if (exp1.length == exp2.length) {
            for (int i = 0, length = exp1.length; i < length; i++) {
                if (!exp1[i].equals(exp2[i])) {
                    return false;
                }
            }
        } else {
            return false;
        }
        return true;
    }

    public static Exponent evaluatePolynomialFrom1(final Exponent[] e, final Exponent challenge) {

        try {

            Exponent result = e[e.length - 1];
            for (int i = e.length - 2; i >= 0; i--) {
                result = result.multiply(challenge).add(e[i]);
            }
            return result;

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

    public static Exponent evaluatePolynomialFromX(final Exponent[] e, final Exponent challenge) {

        try {

            return evaluatePolynomialFrom1(e, challenge).multiply(challenge);

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

    public static Exponent evaluateBackwardsPolynomialFrom1(final Exponent[] e, final Exponent challenge) {

        try {

            Exponent result = e[0];
            for (int i = 1, length = e.length; i < length; i++) {
                result = result.multiply(challenge).add(e[i]);
            }
            return result;

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

    public static Exponent evaluateBackwardsPolynomialFromX(final Exponent[] e, final Exponent challenge) {

        try {

            return evaluatePolynomialFrom1(e, challenge).multiply(challenge);

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

    public static Exponent[] evaluateVecPolyTransposeFrom1(final Exponent[][] e, final Exponent challenge) {

        Exponent[] result = e[e.length - 1].clone();
        for (int j = e.length - 2; j >= 0; j--) {
            result = ExponentTools.multiplyByScalar(result, challenge);
            result = ExponentTools.addTwoVectors(result, e[j]);
        }
        return result;
    }

    public static Exponent[] evaluateVecPolyTransposeFromX(final Exponent[][] e, final Exponent challenge) {
        return ExponentTools.multiplyByScalar(evaluateVecPolyTransposeFrom1(e, challenge), challenge);
    }

    public static Exponent[] evaluateVecBackwardsPolyTransposeFrom1(final Exponent[][] e, final Exponent challenge) {

        Exponent[] result = e[0].clone();
        for (int j = 1; j < e.length; j++) {
            result = ExponentTools.multiplyByScalar(result, challenge);
            result = ExponentTools.addTwoVectors(result, e[j]);
        }
        return result;
    }

    public static Exponent[] evaluateVecBackwardsPolyTransposeFromX(final Exponent[][] e, final Exponent challenge) {
        return ExponentTools.multiplyByScalar(evaluateVecPolyTransposeFrom1(e, challenge), challenge);
    }

    // copied from Exponent class

    public static Exponent getRandomExponent(final BigInteger order) {

        try {

            return new Exponent(order, BigIntTools.generateBigInteger(order));

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

    public static Exponent[] getVectorRandomExponent(final int length, final BigInteger order) {
        final Exponent[] result = new Exponent[length];
        for (int i = 0; i < result.length; i++) {
            result[i] = getRandomExponent(order);
        }
        return result;
    }

    public static List<Exponent> getRandomListExponent(final int length, final BigInteger order) {

        final List<Exponent> result = new ArrayList<>();
        for (int i = 0; i < length; i++) {
            result.add(getRandomExponent(order));
        }
        return result;
    }

    public static Exponent getRandomInvertibleExponent(final BigInteger order) {

        try {

            return new Exponent(order, BigIntTools.generateInvertibleBigInteger(order));

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

    public static Exponent getInverse(final Exponent e) {

        try {

            return new Exponent(e.getQ(), e.getValue().modInverse(e.getQ()));

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

    public static Exponent divide(final Exponent e1, final Exponent e2) {

        try {

            return e1.multiply(getInverse(e2));

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

    public static boolean isExponent(Exponent exponent) {
        return exponent.getValue().compareTo(BigInteger.ZERO) >= 0
                && exponent.getValue().compareTo(exponent.getQ()) < 0;
    }
}
