package qora.transaction;

import java.math.BigDecimal;
import java.math.MathContext;
import java.util.Arrays;
import java.util.List;

import org.json.simple.JSONObject;

import database.DBSet;
import qora.account.Account;
import qora.crypto.Base58;
import settings.Settings;

public abstract class Transaction {
	
	//VALIDATION CODE
	public static final int VALIDATE_OKE = 1;
	public static final int INVALID_ADDRESS = 2;
	public static final int NEGATIVE_AMOUNT = 3;
	public static final int NEGATIVE_FEE = 4;
	public static final int NO_BALANCE = 5;
	public static final int INVALID_REFERENCE = 6;
	
	public static final int INVALID_NAME_LENGTH = 7;
	public static final int INVALID_VALUE_LENGTH = 8;
	public static final int NAME_ALREADY_REGISTRED = 9;
	
	public static final int NAME_DOES_NOT_EXIST = 10;
	public static final int INVALID_NAME_OWNER = 11;
	public static final int NAME_ALREADY_FOR_SALE = 12;
	public static final int NAME_NOT_FOR_SALE = 13;
	public static final int BUYER_ALREADY_OWNER = 14;
	public static final int INVALID_AMOUNT = 15;
	public static final int INVALID_SELLER = 16;
	
	public static final int NAME_NOT_LOWER_CASE = 17;
	
	public static final int INVALID_DESCRIPTION_LENGTH = 18;
	public static final int INVALID_OPTIONS_LENGTH = 19;
	public static final int INVALID_OPTION_LENGTH = 20;
	public static final int DUPLICATE_OPTION = 21;
	public static final int POLL_ALREADY_CREATED = 22;
	public static final int POLL_ALREADY_HAS_VOTES = 23;
	public static final int POLL_NO_EXISTS = 24;
	public static final int OPTION_NO_EXISTS = 25;
	public static final int ALREADY_VOTED_FOR_THAT_OPTION = 26;
	public static final int INVALID_DATA_LENGTH = 27;
	
	public static final int INVALID_QUANTITY = 28;
	public static final int ASSET_DOES_NOT_EXIST = 29;
	public static final int INVALID_RETURN = 30;
	public static final int HAVE_EQUALS_WANT = 31;
	public static final int ORDER_DOES_NOT_EXIST = 32;
	public static final int INVALID_ORDER_CREATOR = 33;
	public static final int INVALID_PAYMENTS_LENGTH = 34;
	public static final int NEGATIVE_PRICE = 35;
	
	public static final int NOT_YET_RELEASED = 1000;
	
	//TYPES
	public static final int GENESIS_TRANSACTION = 1;
	public static final int PAYMENT_TRANSACTION = 2;
	
	public static final int REGISTER_NAME_TRANSACTION = 3;
	public static final int UPDATE_NAME_TRANSACTION = 4;
	public static final int SELL_NAME_TRANSACTION = 5;
	public static final int CANCEL_SELL_NAME_TRANSACTION = 6;
	public static final int BUY_NAME_TRANSACTION = 7;
	
	public static final int CREATE_POLL_TRANSACTION = 8;
	public static final int VOTE_ON_POLL_TRANSACTION = 9;
	
	public static final int ARBITRARY_TRANSACTION = 10;
	
	public static final int ISSUE_ASSET_TRANSACTION = 11;
	public static final int TRANSFER_ASSET_TRANSACTION = 12;
	public static final int CREATE_ORDER_TRANSACTION = 13;
	public static final int CANCEL_ORDER_TRANSACTION = 14;
	public static final int MULTI_PAYMENT_TRANSACTION = 15;
	
	//MINIMUM FEE
	public static final BigDecimal MINIMUM_FEE = BigDecimal.ONE;
	
	//RELEASES
	public static final long VOTING_RELEASE = 1403715600000l;
	public static final long ARBITRARY_TRANSACTIONS_RELEASE = 1405702800000l;
	//public static final long ASSETS_RELEASE = 1411308000000l;
	public static final long ASSETS_RELEASE = 0l;
	
	//PROPERTIES LENGTH
	protected static final int TYPE_LENGTH = 4;
	public static final int TIMESTAMP_LENGTH = 8;
	protected static final int REFERENCE_LENGTH = 64;
		
	protected byte[] reference;
	protected BigDecimal fee;
	protected int type;
	protected byte[] signature;
	protected long timestamp;
	
	protected Transaction(int type, BigDecimal fee, long timestamp, byte[] reference, byte[] signature)
	{
		this.fee = fee;
		this.type = type;
		this.signature = signature;
		this.timestamp = timestamp;
		this.reference = reference;
	}
	
	//GETTERS/SETTERS
	
	public int getType()
	{
		return this.type;
	}
	
	public long getTimestamp()
	{
		return this.timestamp;
	}
	
	public long getDeadline()
	{
		//24HOUR DEADLINE TO INCLUDE TRANSACTION IN BLOCK
		return this.timestamp + (1000*60*60*24);
	}
	
	public BigDecimal getFee()
	{
		return this.fee;
	}
	
	public byte[] getSignature()
	{
		return this.signature;
	}
	
	public byte[] getReference()
	{
		return this.reference;
	}
	
	public BigDecimal feePerByte()
	{
		return this.fee.divide(new BigDecimal(this.getDataLength()), MathContext.DECIMAL32);
	}
	
	public boolean hasMinimumFee()
	{
		return this.fee.compareTo(MINIMUM_FEE) >= 0;
	}
	
	public boolean hasMinimumFeePerByte()
	{
		BigDecimal minFeePerByte = BigDecimal.ONE.divide(BigDecimal.valueOf(Settings.getInstance().getMaxBytePerFee()), MathContext.DECIMAL32);
		
		return this.feePerByte().compareTo(minFeePerByte) >= 0;
	}
	
	//PARSE/CONVERT
	
	@SuppressWarnings("unchecked")
	protected JSONObject getJsonBase()
	{
		JSONObject transaction = new JSONObject();
		
		transaction.put("type", this.type);
		transaction.put("fee", this.fee.toPlainString());
		transaction.put("timestamp", this.timestamp);
		transaction.put("reference", Base58.encode(this.reference));
		transaction.put("signature", Base58.encode(this.signature));
		transaction.put("confirmations", this.getConfirmations());
		
		return transaction;
	}
	
	public abstract JSONObject toJson();
	
	public abstract byte[] toBytes();
	
	public abstract int getDataLength();
	
	//VALIDATE
	
	public abstract boolean isSignatureValid();
	
	public int isValid()
	{
		return this.isValid(DBSet.getInstance());
	}
	
	public abstract int isValid(DBSet db);
	
	//PROCESS/ORPHAN
	
	public void process()
	{
		this.process(DBSet.getInstance());
	}
		
	public abstract void process(DBSet db);

	public void orphan()
	{
		this.orphan(DBSet.getInstance());
	}
	
	public abstract void orphan(DBSet db);
	
	//REST
	
	public abstract Account getCreator();
	
	public abstract List<Account> getInvolvedAccounts();
		
	public abstract boolean isInvolved(Account account);
	
	public abstract BigDecimal getAmount(Account account);
	
	@Override 
	public boolean equals(Object object)
	{
		if(object instanceof Transaction)
		{
			Transaction transaction = (Transaction) object;
			
			return Arrays.equals(this.getSignature(), transaction.getSignature());
		}
		
		return false;
	}

	public boolean isConfirmed()
	{
		return this.isConfirmed(DBSet.getInstance());
	}
	
	public boolean isConfirmed(DBSet db)
	{
		return DBSet.getInstance().getTransactionMap().contains(this);
	}
	
	public int getConfirmations()
	{
		//CHECK IF IN TRANSACTIONDATABASE
		if(DBSet.getInstance().getTransactionMap().contains(this))
		{
			return 0;
		}
		
		//CALCULATE CONFIRMATIONS
		int lastBlockHeight = DBSet.getInstance().getHeightMap().get(DBSet.getInstance().getBlockMap().getLastBlockSignature());
		int transactionBlockHeight = DBSet.getInstance().getHeightMap().get(DBSet.getInstance().getTransactionParentMap().getParent(this.signature));
		
		//RETURN
		return 1 + lastBlockHeight - transactionBlockHeight;
	}

}
