/*
 * (PD) 2003 The Bitzi Corporation Please see http://bitzi.com/publicdomain for
 * more info.
 * 
 * $Id: MerkleTree.java,v 1.2 2008/04/28 18:44:59 sberlin Exp $
 */
package com.limegroup.gnutella.security;

import java.security.DigestException;
import java.security.MessageDigest;
import java.util.ArrayList;

/**
 * Implementation of Merkle Tree hash algorithm, (using the approach as revised
 * in December 2002, to add unique prefixes to leaf and node operations)
 * 
 * This class calculates the root of a MerkleTree, and keeps at most log(n) nodes
 * (one for each tree level) in memory while doing so.
 */
public class MerkleTree extends MessageDigest {
    private static final int BLOCKSIZE = 1024;
    public static final int HASHSIZE = 24;

    /** a Marker for the Stack */
    private static final byte[] MARKER = new byte[0];

    /** 1024 byte buffer */
    private final byte[] buffer;

    /** Buffer offset */
    private int bufferOffset;

    /** Number of bytes hashed until now. */
    private long byteCount;

    /** Internal Tiger MD instance */
    private final MessageDigest internalDigest;

    /** The List of Nodes */
    private final ArrayList<byte[]> nodes;

    /**
     * Constructor
     */
    public MerkleTree(MessageDigest internalDigest) {
        super("merkletree");
        buffer = new byte[BLOCKSIZE];
        bufferOffset = 0;
        byteCount = 0;
        nodes = new ArrayList<byte[]>();
        this.internalDigest = internalDigest;
    }

    @Override
    protected int engineGetDigestLength() {
        return HASHSIZE;
    }

    @Override
    protected void engineUpdate(byte in) {
        byteCount += 1;
        buffer[bufferOffset++] = in;
        if (bufferOffset == BLOCKSIZE) {
            blockUpdate();
            bufferOffset = 0;
        }
    }

    @Override
    protected void engineUpdate(byte[] in, int offset, int length) {
        byteCount += length;
        nodes.ensureCapacity(log2Ceil(byteCount / BLOCKSIZE));

        if (bufferOffset > 0) {
            int remaining = BLOCKSIZE - bufferOffset;
            System.arraycopy(in, offset, buffer, bufferOffset, remaining);
            blockUpdate();
            bufferOffset = 0;
            length -= remaining;
            offset += remaining;
        }

        while (length >= BLOCKSIZE) {
            blockUpdate(in, offset, BLOCKSIZE);
            length -= BLOCKSIZE;
            offset += BLOCKSIZE;
        }

        if (length > 0) {
            System.arraycopy(in, offset, buffer, 0, length);
            bufferOffset = length;
        }
    }

    @Override
    protected byte[] engineDigest() {
        byte[] hash = new byte[HASHSIZE];
        try {
            engineDigest(hash, 0, HASHSIZE);
        } catch (DigestException e) {
            return null;
        }
        return hash;
    }

    @Override
    protected int engineDigest(byte[] buf, int offset, int len) throws DigestException {
        if (len < HASHSIZE)
            throw new DigestException();

        // hash any remaining fragments
        blockUpdate();

        byte[] ret = collapse();

        assert(ret != MARKER);

        System.arraycopy(ret, 0, buf, offset, HASHSIZE);
        engineReset();
        return HASHSIZE;
    }

    /**
     * collapse whatever the tree is now to a root.
     */
    private byte[] collapse() {
        byte[] last = null;
        for (int i = 0; i < nodes.size(); i++) {
            byte[] current = nodes.get(i);
            if (current == MARKER)
                continue;

            if (last == null)
                last = current;
            else {
                internalDigest.reset();
                internalDigest.update((byte) 1);
                internalDigest.update(current);
                internalDigest.update(last);
                last = internalDigest.digest();
            }

            nodes.set(i, MARKER);
        }
        assert(last != null);
        return last;
    }

    @Override
    protected void engineReset() {
        bufferOffset = 0;
        byteCount = 0;
        nodes.clear();
        internalDigest.reset();
    }

    /**
     * Method overrides MessageDigest.clone()
     * 
     * @see java.security.MessageDigest#clone()
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    protected void blockUpdate() {
    	blockUpdate(buffer, 0, bufferOffset);
    }
    /**
     * Update the internal state with a single block of size 1024 (or less, in
     * final block) from the internal buffer.
     */
    protected void blockUpdate(byte [] buf, int pos, int len) {
        internalDigest.reset();
        internalDigest.update((byte) 0); // leaf prefix
        internalDigest.update(buf, pos, len);
        if ((len == 0) && (nodes.size() > 0))
            return; // don't remember a zero-size hash except at very beginning
        byte [] digest = internalDigest.digest();
        push(digest);
    }


    private void push(byte[] data) {
        if (!nodes.isEmpty()) {
            for (int i = 0; i < nodes.size(); i++) {
                byte[] node = nodes.get(i);
                if (node == MARKER) {
                    nodes.set(i, data);
                    return;
                }

                internalDigest.reset();
                internalDigest.update((byte) 1);
                internalDigest.update(node);
                internalDigest.update(data);
                data = internalDigest.digest();
                nodes.set(i, MARKER);
            }
        }
        nodes.add(data);
    }   

    // calculates the next n with 2^n > number
    public static int log2Ceil(long number) {
        int n = 0;
        while (number > 1) {
            number++; // for rounding up.
            number >>>= 1;
            n++;
        }
        return n;
    }

}
