package BFTSmartBlockchain;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.commons.codec.binary.Hex;

import bftsmart.tom.MessageContext;
import bftsmart.tom.ServiceReplica;
import bftsmart.tom.server.defaultservices.DefaultSingleRecoverable;

public class BlockchainServer extends DefaultSingleRecoverable {

	private static final int blockSize = 3;
	
	private TreeMap<String, Block> blocksByHash;
	private TreeSet<Transaction> txPool;
	private Block head;

	private int nodeId;
	
	public BlockchainServer(int id) {
		blocksByHash = new TreeMap<String, Block>();
		txPool = new TreeSet<Transaction>();
		head = null;

		nodeId = id;
		new ServiceReplica(id, this, this);
	}

	@SuppressWarnings("unchecked")
	@Override
	public byte[] appExecuteOrdered(byte[] command, MessageContext msgCtx) {
		byte[] reply = null;
		Transaction tx = null;
		Block newBlock = null;
		byte[] blockHash = null;

		boolean hasReply = false;
		try (ByteArrayInputStream byteIn = new ByteArrayInputStream(command);
				ObjectInput objIn = new ObjectInputStream(byteIn);
				ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
				ObjectOutput objOut = new ObjectOutputStream(byteOut);) {
			BlockchainRequestType reqType = (BlockchainRequestType)objIn.readObject();
			switch (reqType) {
				case ADD_TRANSACTION:
					tx = (Transaction) objIn.readObject();
					if (txPool.add(tx)) {
						if (txPool.size() == blockSize) {
							if (head != null) {
								newBlock = new Block((TreeSet<Transaction>) txPool.clone(), head.blockHash);
							} else {
								newBlock = new Block((TreeSet<Transaction>) txPool.clone(), null);
							}
							blocksByHash.put(Hex.encodeHexString(newBlock.blockHash), newBlock);
							objOut.writeObject(BlockchainReplyType.BLOCK);
							objOut.writeObject(newBlock);
						}
						objOut.writeObject(BlockchainReplyType.OK);
					} else {
						objOut.writeObject(BlockchainReplyType.DUPLICATE);
					}
					hasReply = true;
					break;
				case GET_HEAD:
					if (head != null) {
						objOut.writeObject(head);
						hasReply = true;
					}
					break;
				case GET_BLOCK:
					blockHash = (byte[]) objIn.readObject();
					if (blocksByHash.containsKey(Hex.encodeHexString(blockHash))) {
						objOut.writeObject(blocksByHash.get(Hex.encodeHexString(blockHash)));
						hasReply = true;
					}
					break;
			}
			if (hasReply) {
				objOut.flush();
				byteOut.flush();
				reply = byteOut.toByteArray();
			} else {
				reply = new byte[0];
			}

		} catch (IOException | ClassNotFoundException e) {
			System.out.println(e);
		}
		return reply;
	}

	@Override
	public byte[] appExecuteUnordered(byte[] command, MessageContext msgCtx) {
		byte[] reply = null;
		byte[] blockHash = null;

		boolean hasReply = false;
		try (ByteArrayInputStream byteIn = new ByteArrayInputStream(command);
				ObjectInput objIn = new ObjectInputStream(byteIn);
				ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
				ObjectOutput objOut = new ObjectOutputStream(byteOut);) {
			BlockchainRequestType reqType = (BlockchainRequestType)objIn.readObject();
			switch (reqType) {
				case GET_HEAD:
					if (head != null) {
						objOut.writeObject(head);
						hasReply = true;
					}
					break;
				case GET_BLOCK:
					blockHash = (byte[]) objIn.readObject();
					if (blocksByHash.containsKey(Hex.encodeHexString(blockHash))) {
						objOut.writeObject(blocksByHash.get(Hex.encodeHexString(blockHash)));
						hasReply = true;
					}
					break;
				default:
					System.err.println("Only GET_HEAD and GET_BLOCK are allowed unordered!");	
			}
			if (hasReply) {
				objOut.flush();
				byteOut.flush();
				reply = byteOut.toByteArray();
			} else {
				reply = new byte[0];
			}

		} catch (IOException | ClassNotFoundException e) {
			System.out.println(e);
		}
		return reply;
	}

	@Override
	public byte[] getSnapshot() {
		try (ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
				ObjectOutput objOut = new ObjectOutputStream(byteOut)) {
			objOut.writeObject(blocksByHash);
			objOut.writeObject(txPool);
			objOut.writeObject(head);
			return byteOut.toByteArray();
		} catch (IOException e) {
			System.err.println("Exception while getting snapshot: " + e);
		}
		return new byte[0];
	}

	@SuppressWarnings("unchecked")
	@Override
	public void installSnapshot(byte[] state) {
		try (ByteArrayInputStream byteIn = new ByteArrayInputStream(state);
				ObjectInput objIn = new ObjectInputStream(byteIn)) {
			blocksByHash = (TreeMap<String, Block>) objIn.readObject();
			txPool = (TreeSet<Transaction>) objIn.readObject();
			head = (Block) objIn.readObject();
		} catch (IOException | ClassNotFoundException e) {
			System.err.println("Exception while installing snapshot: " + e);
		}
	}
	
	@Override
	public String toString() {
		String r = "Node : " + Integer.toString(nodeId) + "\n");
		for (Block b : blocksByHash.values()) {
			r += b.toString() + "\n";
		}
		if (head != null) {
			r += "Head: " + head.toString();
		}
		for (Transaction tx : txPool) {
			r += tx.toString() + "\n";
		}
		return r;
	}
}