package org.bouncycastle.crypto.prng;

import org.bouncycastle.crypto.BlockCipher;

public class X931RNG
{
    private static final long       BLOCK64_RESEED_MAX = 1L << (16 - 1);
    private static final long       BLOCK128_RESEED_MAX = 1L << (24 - 1);
    private static final int        BLOCK64_MAX_BITS_REQUEST = 1 << (13 - 1);
    private static final int        BLOCK128_MAX_BITS_REQUEST = 1 << (19 - 1);
    
    private final BlockCipher engine;
    private final EntropySource entropySource;

    private final byte[] DT;
    private final byte[] I;
    private final byte[] R;;

    private byte[] V;

    private long reseedCounter = 1;

    /**
     *
     * @param engine
     * @param entropySource
     */
    public X931RNG(BlockCipher engine, byte[] dateTimeVector, EntropySource entropySource)
    {
        this.engine = engine;
        this.entropySource = entropySource;

        this.DT = new byte[engine.getBlockSize()];

        System.arraycopy(dateTimeVector, 0, DT, 0, DT.length);

        this.I = new byte[engine.getBlockSize()];
        this.R = new byte[engine.getBlockSize()];
    }

    /**
     * Populate a passed in array with random data.
     *
     * @param output output array for generated bits.
     * @param predictionResistant true if a reseed should be forced, false otherwise.
     *
     * @return number of bits generated, -1 if a reseed required.
     */
    int generate(byte[] output, boolean predictionResistant)
    {
        if (R.length == 8) // 64 bit block size
        {
            if (reseedCounter > BLOCK64_RESEED_MAX)
            {
                return -1;
            }

            if (isTooLarge(output, BLOCK64_MAX_BITS_REQUEST / 8))
            {
                throw new IllegalArgumentException("Number of bits per request limited to " + BLOCK64_MAX_BITS_REQUEST);
            }
        }
        else
        {
            if (reseedCounter > BLOCK128_RESEED_MAX)
            {
                return -1;
            }

            if (isTooLarge(output, BLOCK128_MAX_BITS_REQUEST / 8))
            {
                throw new IllegalArgumentException("Number of bits per request limited to " + BLOCK128_MAX_BITS_REQUEST);
            }
        }
        
        if (predictionResistant || V == null)
        {
            V = entropySource.getEntropy();
            if (V.length != engine.getBlockSize())
            {
                throw new IllegalStateException("Insufficient entropy returned");
            }
        }

        int m = output.length / R.length;

        for (int i = 0; i < m; i++)
        {
            engine.processBlock(DT, 0, I, 0);
            process(R, I, V);
            process(V, R, I);

            System.arraycopy(R, 0, output, i * R.length, R.length);

            increment(DT);
        }

        int bytesToCopy = (output.length - m * R.length);

        if (bytesToCopy > 0)
        {
            engine.processBlock(DT, 0, I, 0);
            process(R, I, V);
            process(V, R, I);

            System.arraycopy(R, 0, output, m * R.length, bytesToCopy);

            increment(DT);
        }

        reseedCounter++;

        return output.length;
    }

    /**
     * Reseed the RNG.
     */
    void reseed()
    {
        V = entropySource.getEntropy();
        if (V.length != engine.getBlockSize())
        {
            throw new IllegalStateException("Insufficient entropy returned");
        }
        reseedCounter = 1;
    }

    private void process(byte[] res, byte[] a, byte[] b)
    {
        for (int i = 0; i != res.length; i++)
        {
            res[i] = (byte)(a[i] ^ b[i]);
        }

        engine.processBlock(res, 0, res, 0);
    }

    private void increment(byte[] val)
    {
        for (int i = val.length - 1; i >= 0; i--)
        {
            if (++val[i] != 0)
            {
                break;
            }
        }
    }
    
    private static boolean isTooLarge(byte[] bytes, int maxBytes)
    {
        return bytes != null && bytes.length > maxBytes;
    }
}
