package com.jscape.crypto;

import com.jscape.util.BigEndianBits;
import com.jscape.util.EndianBits;
import com.jscape.util.Parameters;

import java.util.Arrays;

/**
 * The SHA-1 hash buffer.
 *
 * @author Alex Usun
 */
public class SHA1Buffer {

    private static final int MESSAGE_LENGTH_INDEX = SHA1Algorithm.BLOCK_SIZE - SHA1Algorithm.MESSAGE_LENGTH_SIZE;

    private static final byte[] PADDING = {
            (byte) 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0
    };

    private static final EndianBits ENDIAN_BITS = new BigEndianBits();

    private final SHA1Algorithm algorithm;
    private final byte[] messageBlock;
    private final int[] subBlocks;
    private long messageLength;
    private int blockIndex;

    /**
     * Creates a new buffer instance.
     *
     * @param algorithm an algorithm instance
     */
    public SHA1Buffer(SHA1Algorithm algorithm) {
        Parameters.assertNotNull(algorithm);
        this.algorithm = algorithm;

        this.messageBlock = new byte[SHA1Algorithm.BLOCK_SIZE];
        this.subBlocks = new int[SHA1Algorithm.EXTENDED_SUBBLOCKS_COUNT];

        init();
    }

    /**
     * Resets the buffer state.
     */
    public void reset() {
        init();
    }

    /**
     * Puts a new data to the buffer.
     *
     * @param data a new data
     */
    public void put(byte data) {
        this.messageBlock[this.blockIndex] = data;
        ++this.blockIndex;
        ++this.messageLength;

        if (this.blockIndex >= this.messageBlock.length) {
            bufferCompleted();
        }
    }

    /**
     * Puts a new data to the buffer.
     *
     * @param data   a new data
     * @param offset a data offset
     * @param length a data length
     */
    public void put(byte[] data, int offset, int length) {
        while (length != 0) {
            int available = this.messageBlock.length - this.blockIndex;
            int bytesToCopy = available > length ? length : available;

            System.arraycopy(data, offset, this.messageBlock, this.blockIndex, bytesToCopy);
            this.blockIndex += bytesToCopy;
            this.messageLength += bytesToCopy;
            offset += bytesToCopy;
            length -= bytesToCopy;

            if (this.blockIndex >= this.messageBlock.length) {
                bufferCompleted();
            }
        }
    }

    /**
     * Completes buffer data.
     */
    public void complete() {
        int paddingLength = MESSAGE_LENGTH_INDEX - this.blockIndex;

        if (paddingLength > 0) {
            System.arraycopy(PADDING, 0, this.messageBlock, this.blockIndex, paddingLength);
        } else {
            paddingLength = this.messageBlock.length - this.blockIndex;
            System.arraycopy(PADDING, 0, this.messageBlock, this.blockIndex, paddingLength);
            bufferCompleted();
            System.arraycopy(PADDING, paddingLength, this.messageBlock, 0, PADDING.length - paddingLength);
        }

        long bitsLength = this.messageLength << 3; // this.messageLength * bitsPerByte
        ENDIAN_BITS.putLong(bitsLength, this.messageBlock, MESSAGE_LENGTH_INDEX);

        bufferCompleted();
    }

    @Override
    public String toString() {
        return "SHA1Buffer {"
                + "algorithm=" + this.algorithm
                + ", messageBlock=" + Arrays.toString(this.messageBlock)
                + ", subBlocks=" + Arrays.toString(this.subBlocks)
                + ", messageLength=" + this.messageLength
                + ", blockIndex=" + this.blockIndex
                + '}';
    }

    private void init() {
        this.messageLength = 0;
        this.blockIndex = 0;
    }

    private void bufferCompleted() {
        for (int i = 0, byteIndex = 0; i < SHA1Algorithm.SUBBLOCKS_COUNT; ++i, byteIndex += 4) {
            this.subBlocks[i] = ENDIAN_BITS.getInt(this.messageBlock, byteIndex);
        }

        this.algorithm.update(this.subBlocks);
        this.blockIndex = 0;
    }

}
