package org.bouncycastle.pqc.crypto.newhope;

import java.security.SecureRandom;

import org.bouncycastle.crypto.digests.SHA3Digest;

/**
 * This implementation is based heavily on the C reference implementation from https://cryptojedi.org/crypto/index.shtml.
 */
class NewHope
{
    private static final boolean STATISTICAL_TEST = false;

    public static final int AGREEMENT_SIZE = 32;
    public static final int POLY_SIZE = Params.N;
    public static final int SENDA_BYTES = Params.POLY_BYTES + Params.SEED_BYTES;
    public static final int SENDB_BYTES = Params.POLY_BYTES + Params.REC_BYTES;

    public static void keygen(SecureRandom rand, byte[] send, short[] sk)
    {
        byte[] seed = new byte[Params.SEED_BYTES];
        rand.nextBytes(seed);
        
        sha3(seed);     // don't expose RNG output

        short[] a = new short[Params.N];
        generateA(a, seed);

        byte[] noiseSeed = new byte[32];
        rand.nextBytes(noiseSeed);

        Poly.getNoise(sk, noiseSeed, (byte)0);
        Poly.toNTT(sk);

        short[] e = new short[Params.N];
        Poly.getNoise(e, noiseSeed, (byte)1);
        Poly.toNTT(e);

        short[] r = new short[Params.N];
        Poly.pointWise(a, sk, r);

        short[] pk = new short[Params.N];
        Poly.add(r, e, pk);

        encodeA(send, pk, seed);
    }

    public static void sharedB(SecureRandom rand, byte[] sharedKey, byte[] send, byte[] received)
    {
        if (received.length != SENDA_BYTES)
        {
            throw new IllegalArgumentException("'received' has invalid length");
        }

        short[] pkA = new short[Params.N];
        byte[] seed = new byte[Params.SEED_BYTES];
        decodeA(pkA, seed, received);

        short[] a = new short[Params.N];
        generateA(a, seed);

        byte[] noiseSeed = new byte[32];
        rand.nextBytes(noiseSeed);

        short[] sp = new short[Params.N];
        Poly.getNoise(sp, noiseSeed, (byte)0);
        Poly.toNTT(sp);

        short[] ep = new short[Params.N];
        Poly.getNoise(ep, noiseSeed, (byte)1);
        Poly.toNTT(ep);

        short[] bp = new short[Params.N];
        Poly.pointWise(a, sp, bp);
        Poly.add(bp, ep, bp);

        short[] v = new short[Params.N];
        Poly.pointWise(pkA, sp, v);
        Poly.fromNTT(v);

        short[] epp = new short[Params.N];
        Poly.getNoise(epp, noiseSeed, (byte)2);
        Poly.add(v, epp, v);

        short[] c = new short[Params.N];
        ErrorCorrection.helpRec(c, v, noiseSeed, (byte)3);

        encodeB(send, bp, c);

        ErrorCorrection.rec(sharedKey, v, c);

        if (!STATISTICAL_TEST)
        {
            sha3(sharedKey);
        }
    }

    public static void sharedA(byte[] sharedKey, short[] sk, byte[] received)
    {
        if (received.length != SENDB_BYTES)
        {
            throw new IllegalArgumentException("'received' has invalid length");
        }

        short[] bp = new short[Params.N];
        short[] c = new short[Params.N];
        decodeB(bp, c, received);

        short[] v = new short[Params.N];
        Poly.pointWise(sk, bp, v);
        Poly.fromNTT(v);

        ErrorCorrection.rec(sharedKey, v, c);

        if (!STATISTICAL_TEST)
        {
            sha3(sharedKey);
        }
    }

    static void decodeA(short[] pk, byte[] seed, byte[] r)
    {
        Poly.fromBytes(pk, r);
        System.arraycopy(r, Params.POLY_BYTES, seed, 0, Params.SEED_BYTES);
    }

    static void decodeB(short[] b, short[] c, byte[] r)
    {
        Poly.fromBytes(b, r);

        for (int i = 0; i < Params.N / 4; ++i)
        {
            int j = 4 * i;
            int ri = r[Params.POLY_BYTES + i] & 0xFF;
            c[j + 0] = (short)(ri & 0x03);
            c[j + 1] = (short)((ri >>> 2) & 0x03);
            c[j + 2] = (short)((ri >>> 4) & 0x03);
            c[j + 3] = (short)(ri >>> 6);
        }
    }

    static void encodeA(byte[] r, short[] pk, byte[] seed)
    {
        Poly.toBytes(r, pk);
        System.arraycopy(seed, 0, r, Params.POLY_BYTES, Params.SEED_BYTES);        
    }

    static void encodeB(byte[] r, short[] b, short[] c)
    {
        Poly.toBytes(r, b);

        for (int i = 0; i < Params.N / 4; ++i)
        {
            int j = 4 * i;
            r[Params.POLY_BYTES + i] = (byte)(c[j] | (c[j + 1] << 2) | (c[j + 2] << 4) | (c[j + 3] << 6));
        }
    }

    static void generateA(short[] a, byte[] seed)
    {
        Poly.uniform(a, seed);
    }
    
    static void sha3(byte[] sharedKey)
    {
        SHA3Digest d = new SHA3Digest(256);
        d.update(sharedKey, 0, 32);
        d.doFinal(sharedKey, 0);
    }
}
