package hsr.dsa.blockchain;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;
import org.web3j.abi.datatypes.Type;
import org.web3j.crypto.Credentials;

import hsr.dsa.blockchain.exceptions.BlockChainException;
import hsr.dsa.blockchain.exceptions.SmartContractException;
import hsr.dsa.data.models.ChatMessage;
import hsr.dsa.data.models.LocalUser;
import hsr.dsa.data.models.NotaryMessage;
import hsr.dsa.data.models.NotaryMessage.State;
import hsr.dsa.data.models.RemoteUser;
import hsr.dsa.data.models.User;
import io.iconator.testonator.ConvertException;
import io.iconator.testonator.FunctionBuilder;
import io.iconator.testonator.TestBlockchain;

public class BlockChainClient implements Runnable {
	private static final Logger log = Logger.getLogger(BlockChainClient.class);
	private static final String contractAddress = "0xec51dfa793dce4cdfd724f25e5a0c5552a4618a3";
	private static final String blockchainUrl = "https://rinkeby.infura.io/";
	private static final String ApiKey = "XXC6CZUUZW9DAEC3A81NCI9SXI7P366UV6";

	private final ScheduledExecutorService service;
	private final TestBlockchain blockchain;
	private final Credentials credential;
	private final LocalUser user;

	public BlockChainClient(String privateKey, LocalUser user) throws BlockChainException {
		try {
			this.blockchain = TestBlockchain.runRemote(blockchainUrl + ApiKey);
			this.credential = BlockChainUtils.fromECPrivateKey(privateKey);
		} catch (Exception e) {
			throw new BlockChainException("Unable to connect to the BlockChain: '" + blockchainUrl + "'", e);
		}

		this.user = user;
		this.service = Executors.newSingleThreadScheduledExecutor();
		this.service.scheduleWithFixedDelay(this, 0, 10, TimeUnit.SECONDS);
	}

	public synchronized BigInteger getBalance() throws SmartContractException {
		try {
			log.debug("BLOCKCHAIN GET: balance");
			return blockchain.balance(this.credential);
		} catch (ExecutionException | InterruptedException | IOException e) {
			throw new SmartContractException("Unable to get Balance", e);
		}
	}

	public synchronized void upload(byte[] hash, String recipient) throws SmartContractException {
		try {
			log.debug("BLOCKCHAIN upload: " + hash + " to " + recipient);
			FunctionBuilder functionBuilder = new FunctionBuilder("upload");
			functionBuilder.addInput("bytes32", hash);
			functionBuilder.addInput("address", recipient);
			
			blockchain.call(this.credential, contractAddress, functionBuilder);
		} catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException
				| ConvertException | IOException | ExecutionException | InterruptedException e) {
			throw new SmartContractException("Error executing SmartContract: upload", e);
		}
	}

	public synchronized long verifyUpload(byte[] hash, String recipient) throws SmartContractException {
		try {
			log.debug("BLOCKCHAIN verify upload: " + hash + " to " + recipient);
			FunctionBuilder functionBuilder = new FunctionBuilder("verifyUpload");
			functionBuilder.addInput("address", recipient);
			functionBuilder.addInput("bytes32", hash);
			functionBuilder.outputs("uint256");

			return this.parseReturnValueLong(blockchain.callConstant(this.credential, contractAddress, functionBuilder));
		} catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException
				| ConvertException | IOException | ExecutionException | InterruptedException e) {
			throw new SmartContractException("Error executing SmartContract: verifyUpload", e);
		}
	}

	public synchronized void received(byte[] hash) throws SmartContractException {
		try {
			log.debug("BLOCKCHAIN received: " + hash);
			FunctionBuilder functionBuilder = new FunctionBuilder("received");
			functionBuilder.addInput("bytes32", hash);

			blockchain.call(this.credential, contractAddress, functionBuilder);
		} catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException
				| ConvertException | IOException | ExecutionException | InterruptedException e) {
			throw new SmartContractException("Error executing SmartContract: received", e);
		}
	}

	public synchronized long verifyRecived(byte[] hash, String recipient) throws SmartContractException {
		try {
			log.debug("BLOCKCHAIN verify recived: " + hash + " to " + recipient);

			FunctionBuilder functionBuilder = new FunctionBuilder("verifyReceived");
			functionBuilder.addInput("address", recipient);
			functionBuilder.addInput("bytes32", hash);
			functionBuilder.outputs("uint256");
			
			return this.parseReturnValueLong(blockchain.callConstant(this.credential, contractAddress, functionBuilder));
		} catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException
				| ConvertException | IOException | ExecutionException | InterruptedException e) {
			throw new SmartContractException("Error executing SmartContract: verifyRecived", e);
		}
	}

	public synchronized void acknowledge(byte[] hash, boolean accept) throws SmartContractException {
		try {
			log.debug("BLOCKCHAIN acknowledge: " + hash);
			FunctionBuilder functionBuilder = new FunctionBuilder("acknowledge");
			functionBuilder.addInput("bytes32", hash);
			functionBuilder.addInput("bool", accept);

			blockchain.call(this.credential, contractAddress, functionBuilder);
		} catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException
				| ConvertException | IOException | ExecutionException | InterruptedException e) {
			throw new SmartContractException("Error executing SmartContract: acknowledge", e);
		}
	}

	public synchronized long verifyAcknowledged(byte[] hash, String recipient) throws SmartContractException {
		try {
			log.debug("BLOCKCHAIN verify acknowledged: " + hash + " to " + recipient);
			
			FunctionBuilder functionBuilder = new FunctionBuilder("verifyAcknowledged");
			functionBuilder.addInput("address", recipient);
			functionBuilder.addInput("bytes32", hash);
			functionBuilder.outputs("uint256");

			return this.parseReturnValueLong(blockchain.callConstant(this.credential, contractAddress, functionBuilder));
		} catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException
				| ConvertException | IOException | ExecutionException | InterruptedException e) {
			throw new SmartContractException("Error executing SmartContract: verifyAcknowledged", e);
		}
	}

	public synchronized boolean isAccepted(byte[] hash, String recipient) throws SmartContractException {
		try {
			FunctionBuilder functionBuilder = new FunctionBuilder("isAccepted");
			functionBuilder.addInput("address", recipient);
			functionBuilder.addInput("bytes32", hash);
			functionBuilder.outputs("bool");

			return this.parseReturnValueBoolean(blockchain.callConstant(this.credential, contractAddress, functionBuilder));
		} catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException
				| ConvertException | IOException | ExecutionException | InterruptedException e) {
			throw new SmartContractException("Error executing SmartContract: isAccepted", e);
		}
	}

	@SuppressWarnings("rawtypes")
	private long parseReturnValueLong(List<Type> returnTypes) {
		long valueRet = 0;
		for (Type returnType : returnTypes) {
			Object value = returnType.getValue();
			if (value instanceof BigInteger) {
				BigInteger integer = (BigInteger) value;
				valueRet = integer.longValue();
				if(valueRet != 0) {
					return valueRet;
				}
			}
		}
		return 0;
	}

	@SuppressWarnings("rawtypes")
	private boolean parseReturnValueBoolean(List<Type> returnTypes) {
		for (Type returnType : returnTypes) {
			Object value = returnType.getValue();
			if (value instanceof Boolean) {
				 return (Boolean) value;
			}
		}
		return false;
	}

	public void close() throws BlockChainException {
		try {
			this.blockchain.shutdown();
		} catch (Exception e) {
			throw new BlockChainException("Error trying to shut down", e);
		}
		this.service.shutdown();
	}

	@Override
	public void run() {
		if(this.user != null) {
			for(User user : this.user.getFriends()) {
				if(user instanceof RemoteUser) {
					RemoteUser friend = (RemoteUser) user;
					for(ChatMessage message : friend.getChatMessages()) {
						if(message instanceof NotaryMessage) {
							try {
								if(message.getSender().equals(this.user)) {
									this.updateMessageState((NotaryMessage)message, friend);
								} else {
									this.updateMessageState((NotaryMessage)message, this.user);
								}
							} catch (SmartContractException e) {
								e.printStackTrace();
							}
						}
					}
				}
			}
		}
	}

	private void updateMessageState(NotaryMessage message, User user) throws SmartContractException {
		if(message.getState() == NotaryMessage.State.uploaded) {
			long time = this.verifyRecived(message.getHash(), user.getBlockChainPublicKey());
			if(time > 0) {
				LocalDateTime recived = Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()).toLocalDateTime();
				message.setReceived(recived);
			}
		} else if(message.getState() == State.received) {
			long time = this.verifyAcknowledged(message.getHash(), user.getBlockChainPublicKey());
			if(time > 0) {
				LocalDateTime accepted = Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()).toLocalDateTime();
				if(this.isAccepted(message.getHash(), user.getBlockChainPublicKey())) {
					message.setAccepted(accepted);
				} else {
					message.setRejected(accepted);
				}
			}
		}
	}
}
