package com.jwetherell.bitcoin.data_model;

import java.nio.ByteBuffer;
import java.util.Arrays;

import com.jwetherell.bitcoin.common.HashUtils;

public class Block {

    private static final int    FROM_LENGTH             = 4;
    private static final int    BOOLEAN_LENGTH          = 2;
    private static final int    NUM_OF_ZEROS_LENGTH     = 4;
    private static final int    NONCE_LENGTH            = 4;
    private static final int    BLOCK_LENGTH            = 4;
    private static final int    LENGTH_LENGTH           = 4;

    public String               from;
    public boolean              confirmed               = false;
    public int                  numberOfZeros;
    public int                  nonce;
    public int                  blockLength;
    public Transaction[]        transactions;
    public byte[]               prev;
    public byte[]               hash;

    public Block() { }

    public Block(String from, byte[] prevHash, byte[] hash, Transaction[] transactions, int blockLength) {
        this.from = from;
        this.prev = prevHash;
        this.hash = hash;
        this.transactions = transactions;
        this.blockLength = blockLength;
    }

    public int getBufferLength() {
        int transactionsLength = 0;
        for (Transaction t : transactions)
            transactionsLength += LENGTH_LENGTH + t.getBufferLength();
        return  FROM_LENGTH + from.length() +
                BOOLEAN_LENGTH + 
                NUM_OF_ZEROS_LENGTH +
                NONCE_LENGTH +
                BLOCK_LENGTH +
                LENGTH_LENGTH + prev.length + 
                LENGTH_LENGTH + hash.length + 
                LENGTH_LENGTH + transactionsLength;
    }

    public void toBuffer(ByteBuffer buffer) {
        final byte[] fBytes = from.getBytes();
        buffer.putInt(fBytes.length);
        buffer.put(fBytes);
        
        buffer.putChar(getBoolean(confirmed));
        buffer.putInt(numberOfZeros);
        buffer.putInt(nonce);
        buffer.putInt(blockLength);

        buffer.putInt(prev.length);
        buffer.put(prev);

        buffer.putInt(hash.length);
        buffer.put(hash);

        buffer.putInt(transactions.length);
        for (Transaction t : transactions) {
            buffer.putInt(t.getBufferLength());
            t.toBuffer(buffer);
        }
    }

    public void fromBuffer(ByteBuffer buffer) {
        final int fLength = buffer.getInt();
        final byte[] fBytes = new byte[fLength];
        buffer.get(fBytes, 0, fLength);
        from = new String(fBytes);

        confirmed = parseBoolean(buffer.getChar());
        numberOfZeros = buffer.getInt();
        nonce = buffer.getInt();
        blockLength = buffer.getInt();

        { // previous hash
            final int length = buffer.getInt();
            prev = new byte[length];
            buffer.get(prev);
        }

        { // next hash
            final int length = buffer.getInt();
            hash = new byte[length];
            buffer.get(hash);
        }

        int tLength = buffer.getInt();
        transactions =  new Transaction[tLength];
        for (int i=0; i < tLength; i++) {
            int length = buffer.getInt();
            final byte[] bytes = new byte[length];
            buffer.get(bytes);
            final ByteBuffer bb = ByteBuffer.wrap(bytes);
            final Transaction t = new Transaction();
            t.fromBuffer(bb);
            transactions[i] = t;
        }
    }

    private static final char getBoolean(boolean bool) {
        return (bool?'T':'F');
    }

    private static final boolean parseBoolean(char bool) {
        return (bool=='T'?true:false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        int hashCode = 0;
        hashCode += from.length();
        if (confirmed)
            hashCode += 1;
        hashCode += nonce;
        hashCode += blockLength;
        hashCode += numberOfZeros;
        hashCode += transactions.length;
        for (Transaction t : transactions)
            hashCode += t.hashCode();
        for (byte b : prev)
            hashCode += b;
        for (byte b : hash)
            hashCode += b;
        return 31 * hashCode;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Block))
            return false;
        Block c = (Block) o;
        if (!(c.from.equals(from)))
            return false;
        if (confirmed != c.confirmed)
            return false;
        if (nonce != c.nonce)
            return false;
        if (blockLength != c.blockLength)
            return false;
        if (numberOfZeros != c.numberOfZeros)
            return false;
        if (c.transactions.length != this.transactions.length)
            return false;
        { // compare transactions
            for (int i=0; i<c.transactions.length; i++) {
                if (!(c.transactions[i].equals(this.transactions[i])))
                    return false;
            }
        }
        if (!(Arrays.equals(c.prev, prev)))
            return false;
        if (!(Arrays.equals(c.hash, hash)))
            return false;
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("isValid=").append(confirmed).append("\n");
        builder.append("numberOfZerosToCompute=").append(numberOfZeros).append("\n");
        builder.append("nonce=").append(nonce).append("\n");
        builder.append("blockLength=").append(blockLength).append("\n");
        builder.append("prev=[").append(HashUtils.bytesToHex(prev)).append("]\n");
        builder.append("hash=[").append(HashUtils.bytesToHex(hash)).append("]\n");
        builder.append("block={").append("\n");
        for (Transaction t : transactions) {
            builder.append("transaction={").append("\n");
            builder.append(t.toString()).append("\n");
            builder.append("}").append("\n");
        }
        builder.append("}");
        return builder.toString();
    }
}