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.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

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

public class BlockchainServer extends DefaultSingleRecoverable {

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

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

		new ServiceReplica(id, this, this);
	}

	public static void main(String[] args) {
		if (args.length < 1) {
			System.out.println("Usage: demo.map.MapServer <server id>");
			System.exit(-1);
		}
		new BlockchainServer(Integer.parseInt(args[0]));
	}

	@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);
							}
							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.asByteArray());
						hasReply = true;
					}
					break;
				case GET_BLOCK:
					key = (K)objIn.readObject();
					value = replicaMap.remove(key);
					if (value != null) {
						objOut.writeObject(value);
						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;
	}

	@SuppressWarnings("unchecked")
	@Override
	public byte[] appExecuteUnordered(byte[] command, MessageContext msgCtx) {
		byte[] reply = null;
		K key = null;
		V value = 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:
					key = (K)objIn.readObject();
					value = replicaMap.get(key);
					if (value != null) {
						objOut.writeObject(value);
						hasReply = true;
					}
					break;
				case GET_BLOCK:
					int size = replicaMap.size();
					objOut.writeInt(size);
					hasReply = true;
					break;
				default:
					System.err.println("Only GET_HEAD and GET_BLOCK are allowed unorderd");
			}
			if (hasReply) {
				objOut.flush();
				byteOut.flush();
				reply = byteOut.toByteArray();
			} else {
				reply = new byte[0];
			}
		} catch (IOException | ClassNotFoundException e) {
			logger.log(Level.SEVERE, "Ocurred during map operation execution", e);
		}

		return reply;
	}

	@Override
	public byte[] getSnapshot() {
		try (ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
				ObjectOutput objOut = new ObjectOutputStream(byteOut)) {
			objOut.writeObject(replicaMap);
			return byteOut.toByteArray();
		} catch (IOException e) {
			logger.log(Level.SEVERE, "Error while taking 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)) {
			replicaMap = (Map<K, V>)objIn.readObject();
		} catch (IOException | ClassNotFoundException e) {
			logger.log(Level.SEVERE, "Error while installing snapshot", e);
		}
	}
}