/*
 * Copyright (c) IBM Corporation 2018. All Rights Reserved.
 * Project name: pross
 * This project is licensed under the MIT License, see LICENSE.
 */

package com.ibm.pross.common.util.shamir;

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

import com.ibm.pross.common.DerivationResult;
import com.ibm.pross.common.config.CommonConfiguration;
import com.ibm.pross.common.util.Exponentiation;
import com.ibm.pross.common.util.crypto.ecc.EcCurve;
import com.ibm.pross.common.util.crypto.ecc.EcPoint;
import com.ibm.pross.common.util.crypto.rsa.threshold.sign.exceptions.BadArgumentException;

public class Polynomials {

	// Static fields
	final public static EcCurve curve = CommonConfiguration.CURVE;
	final public static BigInteger r = curve.getR();
	final public static EcPoint G = curve.getG();

	/**
	 * Evaluates a polynomial defined by the coefficients list (assumed to be in
	 * order from x^0 to x^n-1) at the given coordinate x. All computations are
	 * performed mod m.
	 * 
	 * @param coefficients
	 * @param x
	 * @param modulus
	 * @return A point representing the evaluation of the polynomial F at F(x).
	 */
	public static ShamirShare evaluatePolynomial(final BigInteger[] coefficients, final BigInteger x,
			final BigInteger m) {
		BigInteger y = BigInteger.ZERO;
		BigInteger exponent = BigInteger.ZERO;
		for (int i = 0; i < coefficients.length; i++) {
			BigInteger xTerm = Exponentiation.modPow(x, exponent, m);
			y = y.add(coefficients[i].multiply(xTerm));
			exponent = exponent.add(BigInteger.ONE);
		}
		return new ShamirShare(x, y.mod(m));
	}

	/**
	 * Uses Lagrange polynomial interpolation of the provided x-coordinates to
	 * determine a multiplier to use when solving for another x-coordinate at
	 * position "i" using an x-coordinate position at "j". This can be used as a
	 * step in partial rebuilding.
	 * 
	 * @param points The x-coordinates of the values being used in this
	 *               interpolation
	 * @param i      The x-coordinate which we are considering solving for
	 * @param j      The given x-coordinate we have and are considering using to
	 *               solve for
	 * @param m
	 * @return Lambda_ij which when multiplied by the y-coordinate at j, will be a
	 *         "partial" slice, which can be summed with others to yield F(i)
	 * @throws BadArgumentException
	 * @throws Exception            When the numerator is not evenly divisible by
	 *                              the denominator
	 */
	public static BigInteger interpolatePartial(final BigInteger[] xCoords, final BigInteger i, final BigInteger j,
			final BigInteger modulo) {
		BigInteger numerator = BigInteger.ONE;
		BigInteger denominator = BigInteger.ONE;

		for (int k = 0; k < xCoords.length; k++) {
			BigInteger jPrime = xCoords[k];
			if (!jPrime.equals(j)) {
				numerator = numerator.multiply(i.subtract(jPrime)).mod(modulo);
				denominator = denominator.multiply(j.subtract(jPrime)).mod(modulo);
			}
		}

		final BigInteger invDenominator = denominator.modInverse(modulo);
		return numerator.multiply(invDenominator).mod(modulo);
	}

	public static BigInteger interpolateComplete(final Collection<ShamirShare> shares, final int threshold,
			final int x) {
		if (shares.size() < threshold) {
			throw new IllegalArgumentException("Fewer than a threshold number of results provided!");
		}

		// Determine coordinates
		final BigInteger[] xCoords = new BigInteger[threshold];
		final List<ShamirShare> shareList = new ArrayList<>(shares);
		for (int i = 0; i < threshold; i++) {
			xCoords[i] = shareList.get(i).getX();
		}

		// Position to solve for
		final BigInteger xPosition = BigInteger.valueOf(x);

		// Interpolate polynomial
		BigInteger sum = BigInteger.ZERO;
		for (int i = 0; i < threshold; i++) {
			final ShamirShare share = shareList.get(i);

			final BigInteger j = share.getX();
			final BigInteger L_ij = Polynomials.interpolatePartial(xCoords, xPosition, j, r);

			final BigInteger product = share.getY().multiply(L_ij).mod(r);

			sum = sum.add(product).mod(r);
		}

		return sum;
	}

	/**
	 * Uses matrix inversion to recover the polynomial coefficients from at least a
	 * threshold number of shares
	 * 
	 * @param shares
	 * @param threshold
	 * 
	 * @return An array of BigIntegers representing the coefficients of the
	 *         polynomial that produced the shares
	 */
	public static BigInteger[] interpolateCoefficients(final Collection<ShamirShare> shares, final int threshold) {
		if (shares.size() < threshold) {
			throw new IllegalArgumentException("Fewer than a threshold number of results provided!");
		}

		// Determine coordinates
		final BigInteger[] xCoords = new BigInteger[threshold];
		final List<ShamirShare> shareList = new ArrayList<>(shares);
		for (int i = 0; i < threshold; i++) {
			xCoords[i] = shareList.get(i).getX();
		}

		final BigInteger[][] inverseMatrix = Matrices.generateInvertedCustomVandermondeFormMatrix(xCoords);

		// Solve for coefficiencts of the polynomial by multiplying inverse matrix
		// against shares
		final BigInteger[] coefficients = Matrices.multiplyShareVector(inverseMatrix, shareList);

		return coefficients;
	}

	/**
	 * Combines a threshold number of derivation results computed from individual
	 * shares to recover the derived result based on the secret represent by those
	 * shares
	 * 
	 * @param responses A response computed using one of the shares
	 * @param threshold The recovery threshold for the secret sharing
	 * @return The EcPoint which is equal to the point derived from multiplying the
	 *         input point with the secret
	 * @throws IllegalArgumentException
	 */
	public static EcPoint interpolateExponents(final List<DerivationResult> responses, final int threshold,
			final int xPosition) throws IllegalArgumentException {

		if (responses.size() < threshold) {
			throw new IllegalArgumentException("Fewer than a threshold number of results provided!");
		}

		final BigInteger r = CommonConfiguration.CURVE.getR();

		// Determine coordinates
		final BigInteger[] xCoords = new BigInteger[threshold];
		for (int i = 0; i < threshold; i++) {
			final DerivationResult toprfResponse = responses.get(i);
			xCoords[i] = toprfResponse.getIndex();
		}

		// Interpolate polynomial
		EcPoint sum = null;
		for (int i = 0; i < threshold; i++) {
			final DerivationResult toprfResponse = responses.get(i);

			final BigInteger j = toprfResponse.getIndex();
			final EcPoint outputShare = toprfResponse.getDerivedSharePoint();
			final BigInteger L_ij = Polynomials.interpolatePartial(xCoords, BigInteger.valueOf(xPosition), j, r);

			final EcPoint product = CommonConfiguration.CURVE.multiply(outputShare, L_ij);

			if (sum == null)
				sum = product;
			else
				sum = CommonConfiguration.CURVE.addPoints(sum, product);
		}

		return sum;
	}

	/**
	 * Uses matrix inversion to recover the Feldman coefficients from at least a
	 * threshold number of share results
	 * 
	 * @param shares
	 * @param threshold
	 * 
	 * @return An array of EcPoints representing the Feldman coefficients g^ai of
	 *         the polynomial that produced the share results g^si
	 */
	public static EcPoint[] interpolateCoefficientsExponents(final List<DerivationResult> responses,
			final int threshold) {
		if (responses.size() < threshold) {
			throw new IllegalArgumentException("Fewer than a threshold number of results provided!");
		}

		// Determine coordinates
		final BigInteger[] xCoords = new BigInteger[threshold];
		for (int i = 0; i < threshold; i++) {
			final DerivationResult toprfResponse = responses.get(i);
			xCoords[i] = toprfResponse.getIndex();
		}

		final BigInteger[][] inverseMatrix = Matrices.generateInvertedCustomVandermondeFormMatrix(xCoords);

		// Solve for Feldman coefficiencts of the polynomial by multiplying inverse
		// matrix against shares
		final EcPoint[] feldmanCoefficients = Matrices.multiplyPointVector(inverseMatrix, responses);

		return feldmanCoefficients;
	}

	/**
	 * Computes n!
	 * 
	 * @param n
	 * @return
	 */
	public static BigInteger factorial(BigInteger n) {
		BigInteger result = BigInteger.ONE;

		while (!n.equals(BigInteger.ZERO)) {
			result = result.multiply(n);
			n = n.subtract(BigInteger.ONE);
		}

		return result;
	}
	

	/**
	 * Uses Lagrange polynomial interpolation of the provided x-coordinates to
	 * determine a multiplier to use when solving for another x-coordinate at
	 * position "i" using an x-coordinate position at "j". This can be used as a
	 * step in partial rebuilding.
	 * 
	 * @param points
	 *            The x-coordinates of the values being used in this
	 *            interpolation
	 * @param delta
	 *            An optional additional multiplier to prevent use of fractions,
	 *            e.g. n! when n is the maximum possible x-coordinate, otherwise
	 *            may be 1.
	 * @param i
	 *            The x-coordinate which we are considering solving for
	 * @param j
	 *            The given x-coordinate we have and are considering using to
	 *            solve for
	 * @param m
	 * @return Lambda_ij which when multiplied by the y-coordinate at j, will be
	 *         a "partial" slice, which can be summed with others to yield F(i)
	 * @throws BadArgumentException 
	 * @throws Exception
	 *             When the numerator is not evenly divisible by the denominator
	 */
	public static BigInteger interpolateNoModulus(final BigInteger[] xCoords, final BigInteger delta, final BigInteger i, final BigInteger j) throws BadArgumentException  {
		BigInteger numerator = delta;
		BigInteger denominator = BigInteger.ONE;

		for (int k = 0; k < xCoords.length; k++) {
			BigInteger jPrime = xCoords[k];
			if (!jPrime.equals(j)) {
				numerator = numerator.multiply(i.subtract(jPrime));
				denominator = denominator.multiply(j.subtract(jPrime));
			}
		}

		if (!(numerator.mod(denominator.abs()).equals(BigInteger.ZERO))) {
			throw new BadArgumentException("Denominator does not evenly divide numerator, use a different delta!");
		}

		return numerator.divide(denominator);
	}
}