package org.bouncycastle.crypto.digests;

import org.bouncycastle.crypto.CryptoServiceProperties;
import org.bouncycastle.crypto.CryptoServicePurpose;
import org.bouncycastle.crypto.CryptoServicesRegistrar;
import org.bouncycastle.util.Memoable;
import org.bouncycastle.util.MemoableResetException;
import org.bouncycastle.util.Pack;

/**
 * FIPS 180-4 implementation of SHA-512/t
 */
public class SHA512tDigest
    extends LongDigest
{
    private int digestLength;      // non-final due to old flow analyser.

    private long  H1t, H2t, H3t, H4t, H5t, H6t, H7t, H8t;

    /**
     * Standard constructor
     */
    public SHA512tDigest(int bitLength)
    {
        this(bitLength, CryptoServicePurpose.ANY);
    }

    public SHA512tDigest(int bitLength, CryptoServicePurpose purpose)
    {
        if (bitLength >= 512)
        {
            throw new IllegalArgumentException("bitLength cannot be >= 512");
        }

        if (bitLength % 8 != 0)
        {
            throw new IllegalArgumentException("bitLength needs to be a multiple of 8");
        }

        if (bitLength == 384)
        {
            throw new IllegalArgumentException("bitLength cannot be 384 use SHA384 instead");
        }

        this.digestLength = bitLength / 8;

        CryptoServicesRegistrar.checkConstraints(cryptoServiceProperties());

        tIvGenerate(digestLength * 8);

        reset();
    }

    /**
     * Copy constructor.  This will copy the state of the provided
     * message digest.
     */
    public SHA512tDigest(SHA512tDigest t)
    {
        super(t);

        this.digestLength = t.digestLength;

        CryptoServicesRegistrar.checkConstraints(cryptoServiceProperties());

        reset(t);
    }

    public SHA512tDigest(byte[] encodedState)
    {
        this(readDigestLength(encodedState), CryptoServicePurpose.values()[encodedState[encodedState.length - 1]]);

        CryptoServicesRegistrar.checkConstraints(cryptoServiceProperties());

        restoreState(encodedState);
    }

    private static int readDigestLength(byte[] encodedState)
    {
        return Pack.bigEndianToInt(encodedState, encodedState.length - 5);
    }

    public String getAlgorithmName()
    {
        return "SHA-512/" + Integer.toString(digestLength * 8);
    }

    public int getDigestSize()
    {
        return digestLength;
    }

    public int doFinal(
        byte[]  out,
        int     outOff)
    {
        finish();

        longToBigEndian(H1, out, outOff, digestLength);
        longToBigEndian(H2, out, outOff + 8, digestLength - 8);
        longToBigEndian(H3, out, outOff + 16, digestLength - 16);
        longToBigEndian(H4, out, outOff + 24, digestLength - 24);
        longToBigEndian(H5, out, outOff + 32, digestLength - 32);
        longToBigEndian(H6, out, outOff + 40, digestLength - 40);
        longToBigEndian(H7, out, outOff + 48, digestLength - 48);
        longToBigEndian(H8, out, outOff + 56, digestLength - 56);

        reset();

        return digestLength;
    }

    /**
     * reset the chaining variables
     */
    public void reset()
    {
        super.reset();

        /*
         * initial hash values use the iv generation algorithm for t.
         */
        H1 = H1t;
        H2 = H2t;
        H3 = H3t;
        H4 = H4t;
        H5 = H5t;
        H6 = H6t;
        H7 = H7t;
        H8 = H8t;
    }

    private void tIvGenerate(int bitLength)
    {
        H1 = 0x6a09e667f3bcc908L ^ 0xa5a5a5a5a5a5a5a5L;
        H2 = 0xbb67ae8584caa73bL ^ 0xa5a5a5a5a5a5a5a5L;
        H3 = 0x3c6ef372fe94f82bL ^ 0xa5a5a5a5a5a5a5a5L;
        H4 = 0xa54ff53a5f1d36f1L ^ 0xa5a5a5a5a5a5a5a5L;
        H5 = 0x510e527fade682d1L ^ 0xa5a5a5a5a5a5a5a5L;
        H6 = 0x9b05688c2b3e6c1fL ^ 0xa5a5a5a5a5a5a5a5L;
        H7 = 0x1f83d9abfb41bd6bL ^ 0xa5a5a5a5a5a5a5a5L;
        H8 = 0x5be0cd19137e2179L ^ 0xa5a5a5a5a5a5a5a5L;

        update((byte)0x53);
        update((byte)0x48);
        update((byte)0x41);
        update((byte)0x2D);
        update((byte)0x35);
        update((byte)0x31);
        update((byte)0x32);
        update((byte)0x2F);

        if (bitLength > 100)
        {
            update((byte)(bitLength / 100 + 0x30));
            bitLength = bitLength % 100;
            update((byte)(bitLength / 10 + 0x30));
            bitLength = bitLength % 10;
            update((byte)(bitLength + 0x30));
        }
        else if (bitLength > 10)
        {
            update((byte)(bitLength / 10 + 0x30));
            bitLength = bitLength % 10;
            update((byte)(bitLength + 0x30));
        }
        else
        {
            update((byte)(bitLength + 0x30));
        }

        finish();

        H1t = H1;
        H2t = H2;
        H3t = H3;
        H4t = H4;
        H5t = H5;
        H6t = H6;
        H7t = H7;
        H8t = H8;
    }

    private static void longToBigEndian(long n, byte[] bs, int off, int max)
    {
        if (max > 0)
        {
            intToBigEndian((int)(n >>> 32), bs, off, max);

            if (max > 4)
            {
                intToBigEndian((int)(n & 0xffffffffL), bs, off + 4, max - 4);
            }
        }
    }

    private static void intToBigEndian(int n, byte[] bs, int off, int max)
    {
        int num = Math.min(4, max);
        while (--num >= 0)
        {
            int shift = 8 * (3 - num);
            bs[off + num] = (byte)(n >>> shift);
        }
    }

    public Memoable copy()
    {
        return new SHA512tDigest(this);
    }

    public void reset(Memoable other)
    {
        SHA512tDigest t = (SHA512tDigest)other;

        if (this.digestLength != t.digestLength)
        {
            throw new MemoableResetException("digestLength inappropriate in other");
        }

        super.copyIn(t);

        this.H1t = t.H1t;
        this.H2t = t.H2t;
        this.H3t = t.H3t;
        this.H4t = t.H4t;
        this.H5t = t.H5t;
        this.H6t = t.H6t;
        this.H7t = t.H7t;
        this.H8t = t.H8t;
    }

    public byte[] getEncodedState()
    {
        final int baseSize = getEncodedStateSize();
        byte[] encoded = new byte[baseSize + 4 + 1];
        populateState(encoded);
        Pack.intToBigEndian(digestLength * 8, encoded, baseSize);

        encoded[encoded.length - 1] = (byte)purpose.ordinal();

        return encoded;
    }

    protected CryptoServiceProperties cryptoServiceProperties()
    {
        return Utils.getDefaultProperties(this, this.getDigestSize() * 8, purpose);
    }
}
