package io.everitoken.sdk.java;

import java.math.BigInteger;
import java.security.SecureRandom;

import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.DSA;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.crypto.signers.DSAKCalculator;
import org.bouncycastle.math.ec.ECAlgorithms;
import org.bouncycastle.math.ec.ECConstants;
import org.bouncycastle.math.ec.ECMultiplier;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier;

/**
 * EC-DSA as described in X9.62
 */
public class Signer implements ECConstants, DSA {
    private final DSAKCalculator kCalculator;

    private ECKeyParameters key;
    private SecureRandom random;

    /**
     * Configuration with an alternate, possibly deterministic calculator of K.
     *
     * @param kCalculator
     *                        a K value calculator.
     */
    public Signer(DSAKCalculator kCalculator) {
        this.kCalculator = kCalculator;
    }

    protected static BigInteger calculateE(BigInteger n, byte[] message) {
        int log2n = n.bitLength();
        int messageBitLength = message.length * 8;

        BigInteger e = new BigInteger(1, message);
        if (log2n < messageBitLength) {
            e = e.shiftRight(messageBitLength - log2n);
        }
        return e;
    }

    // 5.3 pg 28

    protected static ECMultiplier createBasePointMultiplier() {
        return new FixedPointCombMultiplier();
    }

    // 5.4 pg 29

    protected static SecureRandom initSecureRandom(boolean needed, SecureRandom provided) {
        return !needed ? null : (provided != null) ? provided : new SecureRandom();
    }

    @Override
    public void init(boolean forSigning, CipherParameters param) {
        SecureRandom providedRandom = null;

        if (forSigning) {
            if (param instanceof ParametersWithRandom) {
                ParametersWithRandom rParam = (ParametersWithRandom) param;

                key = (ECPrivateKeyParameters) rParam.getParameters();
                providedRandom = rParam.getRandom();
            } else {
                key = (ECPrivateKeyParameters) param;
            }
        } else {
            key = (ECPublicKeyParameters) param;
        }

        random = initSecureRandom(forSigning && !kCalculator.isDeterministic(), providedRandom);
    }

    /**
     * generate a signature for the given message using the key we were initialised
     * with. For conventional DSA the message should be a SHA-1 hash of the message
     * of interest.
     *
     * @param message
     *                    the message that will be verified later.
     */
    @Override
    public BigInteger[] generateSignature(byte[] message) {
        ECDomainParameters ec = key.getParameters();
        BigInteger n = ec.getN();
        BigInteger e = calculateE(n, message);
        BigInteger d = ((ECPrivateKeyParameters) key).getD();

        if (kCalculator.isDeterministic()) {
            kCalculator.init(n, d, message);
        } else {
            kCalculator.init(n, random);
        }

        BigInteger r, s;

        ECMultiplier basePointMultiplier = createBasePointMultiplier();

        int sLen = 0;
        // 5.3.2
        do // generate s
        {
            BigInteger k;
            int rLen = 0;
            do // generate r
            {
                k = kCalculator.nextK();

                ECPoint p = basePointMultiplier.multiply(ec.getG(), k).normalize();

                // 5.3.3
                r = p.getAffineXCoord().toBigInteger().mod(n);
                rLen = r.toByteArray().length;
            } while (r.equals(ZERO) || rLen != 32); // make sure that length of r is 32 bytes

            s = k.modInverse(n).multiply(e.add(d.multiply(r))).mod(n);
            sLen = s.toByteArray().length;
        } while (s.equals(ZERO) || sLen != 32);

        return new BigInteger[] { r, s };
    }

    /**
     * return true if the value r and s represent a DSA signature for the passed in
     * message (for standard DSA the message should be a SHA-1 hash of the real
     * message to be verified).
     */
    @Override
    public boolean verifySignature(byte[] message, BigInteger r, BigInteger s) {
        ECDomainParameters ec = key.getParameters();
        BigInteger n = ec.getN();
        BigInteger e = calculateE(n, message);

        // r in the range [1,n-1]
        if (r.compareTo(ONE) < 0 || r.compareTo(n) >= 0) {
            return false;
        }

        // s in the range [1,n-1]
        if (s.compareTo(ONE) < 0 || s.compareTo(n) >= 0) {
            return false;
        }

        BigInteger c = s.modInverse(n);

        BigInteger u1 = e.multiply(c).mod(n);
        BigInteger u2 = r.multiply(c).mod(n);

        ECPoint G = ec.getG();
        ECPoint Q = ((ECPublicKeyParameters) key).getQ();

        ECPoint point = ECAlgorithms.sumOfTwoMultiplies(G, u1, Q, u2).normalize();

        // components must be bogus.
        if (point.isInfinity()) {
            return false;
        }

        BigInteger v = point.getAffineXCoord().toBigInteger().mod(n);

        return v.equals(r);
    }
}