package qora;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ntp.NTP;
import qora.account.PrivateKeyAccount;
import qora.block.Block;
import qora.block.BlockFactory;
import qora.crypto.Crypto;
import qora.transaction.Transaction;
import settings.Settings;
import utils.TransactionFeeComparator;

import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;

import controller.Controller;
import database.DBSet;

public class BlockGenerator extends Thread
{	
	public static final int RETARGET = 10;
	public static final long MIN_BALANCE = 1l;
	public static final long MAX_BALANCE = 10000000000l;
	public static final int MIN_BLOCK_TIME = 1 * 60;
	public static final int MAX_BLOCK_TIME = 5 * 60;
	
	private Map<PrivateKeyAccount, Block> blocks;
	private Block solvingBlock;
	private List<PrivateKeyAccount> cachedAccounts;
	
	public BlockGenerator()
	{
		if(Settings.getInstance().isGeneratorKeyCachingEnabled())
		{
			this.cachedAccounts = new ArrayList<PrivateKeyAccount>();
		}
	}
	
	public void addUnconfirmedTransaction(Transaction transaction)
	{
		this.addUnconfirmedTransaction(DBSet.getInstance(), transaction, true);
	}
	
	public void addUnconfirmedTransaction(DBSet db, Transaction transaction, boolean process) 
	{
		//ADD TO TRANSACTION DATABASE 
		db.getTransactionMap().add(transaction);
	}
	
	public List<Transaction> getUnconfirmedTransactions()
	{
		return new ArrayList<Transaction>(DBSet.getInstance().getTransactionMap().getValues());
	}
	
	private List<PrivateKeyAccount> getKnownAccounts()
	{
		//CHECK IF CACHING ENABLED
		if(Settings.getInstance().isGeneratorKeyCachingEnabled())
		{
			List<PrivateKeyAccount> privateKeyAccounts = Controller.getInstance().getPrivateKeyAccounts();
			
			//IF ACCOUNTS EXISTS
			if(privateKeyAccounts.size() > 0)
			{
				//CACHE ACCOUNTS
				this.cachedAccounts = privateKeyAccounts;
			}
			
			//RETURN CACHED ACCOUNTS
			return this.cachedAccounts;
		}
		else
		{
			//RETURN ACCOUNTS
			return Controller.getInstance().getPrivateKeyAccounts();
		}
	}
	
	public void run()
	{
		while(true)
		{
			//CHECK IF WE ARE UPTODATE
			if(!Controller.getInstance().isUpToDate())
			{
				Controller.getInstance().update();
			}
			
			//CHECK IF WE HAVE CONNECTIONS
			if(Controller.getInstance().getStatus() == Controller.STATUS_OKE)
			{
				//GET LAST BLOCK
				byte[] lastBlockSignature = DBSet.getInstance().getBlockMap().getLastBlockSignature();
						
				//CHECK IF DIFFERENT FOR CURRENT SOLVING BLOCK
				if(this.solvingBlock == null || !Arrays.equals(this.solvingBlock.getSignature(), lastBlockSignature))
				{
					//SET NEW BLOCK TO SOLVE
					this.solvingBlock = DBSet.getInstance().getBlockMap().getLastBlock();
					
					//RESET BLOCKS
					this.blocks = new HashMap<PrivateKeyAccount, Block>();
				}
				
				//GENERATE NEW BLOCKS
				if(Controller.getInstance().doesWalletExists() /*&& Controller.getInstance().isWalletUnlocked()*/)
				{
					//PREVENT CONCURRENT MODIFY EXCEPTION
					List<PrivateKeyAccount> knownAccounts = this.getKnownAccounts();							
					synchronized(knownAccounts)
					{
						for(PrivateKeyAccount account: knownAccounts)
						{
							if(account.getGeneratingBalance().compareTo(BigDecimal.ONE) >= 0)
							{
								//CHECK IF BLOCK FROM USER ALREADY EXISTS USE MAP ACCOUNT BLOCK EASY
								if(!this.blocks.containsKey(account))
								{	
									//GENERATE NEW BLOCK FOR USER
									this.blocks.put(account, this.generateNextBlock(DBSet.getInstance(), account, this.solvingBlock));
								}
							}
						}
					}
				}
				
				//VALID BLOCK FOUND
				boolean validBlockFound = false;
						
				//CHECK IF BLOCK IS VALID
				for(PrivateKeyAccount account: this.blocks.keySet())
				{
					Block block = this.blocks.get(account);
					
					/*Date date = new Date(block.getTimestamp());
					DateFormat format = DateFormat.getDateTimeInstance();
					System.out.println(format.format(date));*/
					
					//CHECK IF BLACK TIMESTAMP IS VALID
					if(block.getTimestamp() <= NTP.getTime() && !validBlockFound)
					{
						//ADD TRANSACTIONS
						this.addUnconfirmedTransactions(DBSet.getInstance(), block);
						
						//ADD TRANSACTION SIGNATURE
						block.setTransactionsSignature(this.calculateTransactionsSignature(block, account));
						
						//PASS BLOCK TO CONTROLLER
						Controller.getInstance().newBlockGenerated(block);
						
						//BLOCK FOUND
						validBlockFound = true;
					}
				}
				
				//IF NO BLOCK FOUND
				if(!validBlockFound)
				{
					//SLEEP
					try 
					{
						Thread.sleep(100);
					} 
					catch (InterruptedException e) 
					{
						e.printStackTrace();
					}
				}
			}
			else
			{
				//SLEEP
				try 
				{
					Thread.sleep(100);
				} 
				catch (InterruptedException e) 
				{
					e.printStackTrace();
				}
			}
		}
	}
	
	public Block generateNextBlock(DBSet db, PrivateKeyAccount account, Block block)
	{
		//CHECK IF ACCOUNT HAS BALANCE
		if(account.getGeneratingBalance(db) == BigDecimal.ZERO)
		{
			return null;
		}
		
		//CALCULATE SIGNATURE
		byte[] signature = this.calculateSignature(db, block, account);
		
		//CALCULATE HASH
		byte[] hash = Crypto.getInstance().digest(signature);
			
		//CONVERT HASH TO BIGINT
		BigInteger hashValue = new BigInteger(1, hash);
		
		//CALCULATE ACCOUNT TARGET
		byte[] targetBytes = new byte[32];
		Arrays.fill(targetBytes, Byte.MAX_VALUE);
		BigInteger target = new BigInteger(1, targetBytes);
								
		//DIVIDE TARGET BY BASE TARGET
		BigInteger baseTarget = BigInteger.valueOf(getBaseTarget(getNextBlockGeneratingBalance(db, block)));
		target = target.divide(baseTarget);
			
		//MULTIPLY TARGET BY USER BALANCE
		target = target.multiply(account.getGeneratingBalance(db).toBigInteger());
		
		//CALCULATE GUESSES
		//long guesses = hashValue.divide(target).longValue() + 1;
		BigInteger guesses = hashValue.divide(target).add(BigInteger.ONE);
		
		//CALCULATE TIMESTAMP
		//long timestamp = block.getTimestamp() + (guesses * 1000);
		BigInteger timestamp = guesses.multiply(BigInteger.valueOf(1000)).add(BigInteger.valueOf(block.getTimestamp()));
		
		//CHECK IF NOT HIGHER THAN MAX LONG VALUE
		if(timestamp.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) == 1)
		{
			timestamp = BigInteger.valueOf(Long.MAX_VALUE);
		}
		
		//CREATE NEW BLOCK
		int version = 1;
		Block newBlock = BlockFactory.getInstance().create(version, block.getSignature(), timestamp.longValue(), getNextBlockGeneratingBalance(db, block), account, signature);
		
		return newBlock;
	}
	
	private byte[] calculateSignature(DBSet db, Block solvingBlock, PrivateKeyAccount account) 
	{	
		byte[] data = new byte[0];
		
		//WRITE PARENT GENERATOR SIGNATURE
		byte[] generatorSignature = Bytes.ensureCapacity(solvingBlock.getGeneratorSignature(), Block.GENERATOR_SIGNATURE_LENGTH, 0);
		data = Bytes.concat(data, generatorSignature);
		
		//WRITE GENERATING BALANCE
		byte[] baseTargetBytes = Longs.toByteArray(getNextBlockGeneratingBalance(db, solvingBlock));
		baseTargetBytes = Bytes.ensureCapacity(baseTargetBytes, Block.GENERATING_BALANCE_LENGTH, 0);
		data = Bytes.concat(data,baseTargetBytes);
		
		//WRITE GENERATOR
		byte[] generatorBytes = Bytes.ensureCapacity(account.getPublicKey(), Block.GENERATOR_LENGTH, 0);
		data = Bytes.concat(data, generatorBytes);
								
		//CALC SIGNATURE OF NEWBLOCKHEADER
		byte[] signature = Crypto.getInstance().sign(account, data);
		
		return signature;
	}
	
	public byte[] calculateTransactionsSignature(Block block, PrivateKeyAccount account) 
	{	
		byte[] data = block.getGeneratorSignature();
		
		//WRITE TRANSACTION SIGNATURE
		for(Transaction transaction: block.getTransactions())
		{
			data = Bytes.concat(data, transaction.getSignature());
		}
		
		return Crypto.getInstance().sign(account, data);
	}
	
	public void addUnconfirmedTransactions(DBSet db, Block block)
	{
		long totalBytes = 0;
		boolean transactionProcessed;
			
		//CREATE FORK OF GIVEN DATABASE
		DBSet newBlockDb = db.fork();
					
		//ORDER TRANSACTIONS BY FEE PER BYTE
		List<Transaction> orderedTransactions = new ArrayList<Transaction>(db.getTransactionMap().getValues());
		Collections.sort(orderedTransactions, new TransactionFeeComparator());
		//Collections.sort(orderedTransactions, Collections.reverseOrder());
					
		do
		{
			transactionProcessed = false;
						
			for(Transaction transaction: orderedTransactions)
			{
				//CHECK TRANSACTION TIMESTAMP AND DEADLINE
				if(transaction.getTimestamp() <= block.getTimestamp() && transaction.getDeadline() > block.getTimestamp())
				{
					//CHECK IF VALID
					if(transaction.isValid(newBlockDb) == Transaction.VALIDATE_OKE)
					{
						//CHECK IF ENOUGH ROOM
						if(totalBytes + transaction.getDataLength() <= Block.MAX_TRANSACTION_BYTES)
						{
							//ADD INTO BLOCK
							block.addTransaction(transaction);
										
							//REMOVE FROM LIST
							orderedTransactions.remove(transaction);
										
							//PROCESS IN NEWBLOCKDB
							transaction.process(newBlockDb);
										
							//TRANSACTION PROCESSES
							transactionProcessed = true;
							break;
						}
					}
				}
						
			}
		}
		while(transactionProcessed == true);
	}
	
	/*public void addObserver(Observer o)
	{
		o.update(null, new ObserverMessage(ObserverMessage.LIST_TRANSACTION_TYPE, DBSet.getInstance().getTransactionMap().getValues()));
	}*/
	
	public static long getNextBlockGeneratingBalance(DBSet db, Block block)
	{
		int height = block.getHeight(db);
		if(height % RETARGET == 0)
		{
			//CALCULATE THE GENERATING TIME FOR LAST 10 BLOCKS
			long generatingTime = block.getTimestamp();
				
			//GET FIRST BLOCK OF TARGET
			Block firstBlock = block;
			for(int i=1; i<RETARGET; i++)
			{
				firstBlock = firstBlock.getParent(db);
			}
					
			generatingTime -= firstBlock.getTimestamp();
			
			//CALCULATE EXPECTED FORGING TIME
			long expectedGeneratingTime = getBlockTime(block.getGeneratingBalance()) * RETARGET * 1000;
			
			//CALCULATE MULTIPLIER
			double multiplier = (double) expectedGeneratingTime / (double) generatingTime;
			
			//CALCULATE NEW GENERATING BALANCE
			long generatingBalance = (long) (block.getGeneratingBalance() * multiplier);
			
			return minMaxBalance(generatingBalance);
		}
		
		return block.getGeneratingBalance();
	}
		
	public static long getBaseTarget(long generatingBalance)
	{
		generatingBalance = minMaxBalance(generatingBalance);
		
		long baseTarget = generatingBalance * getBlockTime(generatingBalance);
		
		return baseTarget;
	}
	
	public static long getBlockTime(long generatingBalance)
	{
		generatingBalance = minMaxBalance(generatingBalance);
		
		double percentageOfTotal = (double) generatingBalance / MAX_BALANCE;
		long actualBlockTime = (long) (MIN_BLOCK_TIME + ((MAX_BLOCK_TIME - MIN_BLOCK_TIME) * (1 - percentageOfTotal)));
		
		return actualBlockTime;
	}
	
	private static long minMaxBalance(long generatingBalance)
	{
		if(generatingBalance < MIN_BALANCE)
		{
			return MIN_BALANCE;
		}
		
		if(generatingBalance > MAX_BALANCE)
		{
			return MAX_BALANCE;
		}
		
		return generatingBalance;
	}
}
