/*
 * @(#)MD5Buffer.java
 *
 * Copyright (c) 2002-2003 JSCAPE
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JSCAPE.
 */

package com.jscape.crypto;

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

import java.util.Arrays;

/**
 * The MD5 algorithm buffer.
 *
 * @author Alex Usun
 */
public class MD5Buffer {

    private static final int MESSAGE_LENGTH_INDEX = MD5Algorithm.BLOCK_SIZE - MD5Algorithm.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 LittleEndianBits();

    private final MD5Algorithm 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 MD5Buffer(MD5Algorithm algorithm) {
        Parameters.assertNotNull(algorithm);
        this.algorithm = algorithm;

        this.messageBlock = new byte[MD5Algorithm.BLOCK_SIZE];
        this.subBlocks = new int[MD5Algorithm.SUBLOCKS_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 = Math.min(available, length);

            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 "MD5Buffer {"
                + "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 < this.subBlocks.length; ++i, byteIndex += 4) {
            this.subBlocks[i] = ENDIAN_BITS.getInt(this.messageBlock, byteIndex);
        }

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

}
