package qora.assets;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.List;

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

import database.DBSet;
import qora.account.Account;
import qora.crypto.Base58;
import qora.transaction.Transaction;

public class Order implements Comparable<Order> {
	
	private static final int ID_LENGTH = 64;
	private static final int CREATOR_LENGTH = 25;
	private static final int HAVE_LENGTH = 8;
	private static final int WANT_LENGTH = 8;
	private static final int AMOUNT_LENGTH = 12;
	private static final int FULFILLED_LENGTH = 12;
	private static final int PRICE_LENGTH = 12;
	private static final int TIMESTAMP_LENGTH = 8;
	private static final int BASE_LENGTH = ID_LENGTH + CREATOR_LENGTH + HAVE_LENGTH + WANT_LENGTH + AMOUNT_LENGTH + FULFILLED_LENGTH + PRICE_LENGTH + TIMESTAMP_LENGTH;
	
	private BigInteger id;
	private Account creator;
	private long have;
	private long want;
	private BigDecimal amount;
	private BigDecimal fulfilled;
	private BigDecimal price;
	private long timestamp;
	
	public Order(BigInteger id, Account creator, long have, long want, BigDecimal amount, BigDecimal price, long timestamp)
	{
		this.id = id;
		this.creator = creator;
		this.have = have;
		this.want = want;
		this.amount = amount;
		this.fulfilled = BigDecimal.ZERO.setScale(8);
		this.price = price;
		this.timestamp = timestamp;
	}
	
	public Order(BigInteger id, Account creator, long have, long want, BigDecimal amount, BigDecimal fulfilled, BigDecimal price, long timestamp)
	{
		this.id = id;
		this.creator = creator;
		this.have = have;
		this.want = want;
		this.amount = amount;
		this.fulfilled = fulfilled;
		this.price = price;
		this.timestamp = timestamp;
	}
	
	//GETTERS/SETTERS
	
	public BigInteger getId()
	{
		return this.id;
	}

	public Account getCreator() 
	{
		return this.creator;
	}

	public long getHave() 
	{
		return this.have;
	}
	
	public Asset getHaveAsset() 
	{
		return this.getHaveAsset(DBSet.getInstance());
	}
	
	public Asset getHaveAsset(DBSet db)
	{
		return db.getAssetMap().get(this.have);
	}

	public long getWant() 
	{
		return this.want;
	}
	
	public Asset getWantAsset() 
	{
		return this.getWantAsset(DBSet.getInstance());
	}
	
	public Asset getWantAsset(DBSet db)
	{
		return db.getAssetMap().get(this.want);
	}

	public BigDecimal getAmount() 
	{
		return this.amount;
	}
	
	public BigDecimal getAmountLeft()
	{
		return this.amount.subtract(this.fulfilled);
	}

	public BigDecimal getPrice() 
	{
		return this.price;
	}
	
	public BigDecimal getFulfilled()
	{
		return this.fulfilled;
	}
	
	public long getTimestamp() 
	{
		return this.timestamp;
	}
	
	public void setFulfilled(BigDecimal fulfilled)
	{
		this.fulfilled = fulfilled;
	}
	
	public boolean isFulfilled()
	{
		return this.fulfilled.compareTo(this.amount) == 0;
	}
	
	public List<Trade> getInitiatedTrades()
	{
		return this.getInitiatedTrades(DBSet.getInstance());
	}
	
	public List<Trade> getInitiatedTrades(DBSet db)
	{
		return db.getTradeMap().getInitiatedTrades(this);
	}
	
	public boolean isConfirmed() 
	{
		return DBSet.getInstance().getOrderMap().contains(this.id) || DBSet.getInstance().getCompletedOrderMap().contains(this.id);
	}
	
	//PARSE/CONVERT
	
	public static Order parse(byte[] data) throws Exception
	{
		//CHECK IF CORRECT LENGTH
		if(data.length < BASE_LENGTH)
		{
			throw new Exception("Data does not match order length");
		}
		
		int position = 0;
		
		//READ ID
		byte[] idBytes = Arrays.copyOfRange(data, position, position + ID_LENGTH);
		BigInteger id = new BigInteger(idBytes);
		position += ID_LENGTH;
		
		//READ CREATOR
		byte[] creatorBytes = Arrays.copyOfRange(data, position, position + CREATOR_LENGTH);
		Account creator = new Account(Base58.encode(creatorBytes));
		position += CREATOR_LENGTH;
		
		//READ HAVE
		byte[] haveBytes = Arrays.copyOfRange(data, position, position + HAVE_LENGTH);
		long have = Longs.fromByteArray(haveBytes);	
		position += HAVE_LENGTH;
		
		//READ HAVE
		byte[] wantBytes = Arrays.copyOfRange(data, position, position + WANT_LENGTH);
		long want = Longs.fromByteArray(wantBytes);	
		position += WANT_LENGTH;
		
		//READ AMOUNT
		byte[] amountBytes = Arrays.copyOfRange(data, position, position + AMOUNT_LENGTH);
		BigDecimal amount = new BigDecimal(new BigInteger(amountBytes), 8);
		position += AMOUNT_LENGTH;		
		
		//READ FULFILLED
		byte[] fulfilledBytes = Arrays.copyOfRange(data, position, position + FULFILLED_LENGTH);
		BigDecimal fulfilled = new BigDecimal(new BigInteger(fulfilledBytes), 8);
		position += FULFILLED_LENGTH;		
		
		//READ PRICE
		byte[] priceBytes = Arrays.copyOfRange(data, position, position + PRICE_LENGTH);
		BigDecimal price = new BigDecimal(new BigInteger(priceBytes), 8);
		position += PRICE_LENGTH;	
		
		//READ TIMESTAMP
		byte[] timestampBytes = Arrays.copyOfRange(data, position, position + TIMESTAMP_LENGTH);
		long timestamp = Longs.fromByteArray(timestampBytes);	
		position += TIMESTAMP_LENGTH;
		
		return new Order(id, creator, have, want, amount, fulfilled, price, timestamp);
	}
	
	public byte[] toBytes()
	{
		byte[] data = new byte[0];
		
		//WRITE ID
		byte[] idBytes = this.id.toByteArray();
		byte[] fill = new byte[ID_LENGTH - idBytes.length];
		idBytes = Bytes.concat(fill, idBytes);
		data = Bytes.concat(data, idBytes);
		
		//WRITE CREATOR
		try
		{
			data = Bytes.concat(data , Base58.decode(this.creator.getAddress()));
		}
		catch(Exception e)
		{
			//DECODE EXCEPTION
		}
		
		//WRITE HAVE
		byte[] haveBytes = Longs.toByteArray(this.have);
		haveBytes = Bytes.ensureCapacity(haveBytes, HAVE_LENGTH, 0);
		data = Bytes.concat(data, haveBytes);
		
		//WRITE WANT
		byte[] wantBytes = Longs.toByteArray(this.want);
		wantBytes = Bytes.ensureCapacity(wantBytes, WANT_LENGTH, 0);
		data = Bytes.concat(data, wantBytes);
		
		//WRITE AMOUNT
		byte[] amountBytes = this.amount.unscaledValue().toByteArray();
		fill = new byte[AMOUNT_LENGTH - amountBytes.length];
		amountBytes = Bytes.concat(fill, amountBytes);
		data = Bytes.concat(data, amountBytes);
		
		//WRITE FULFILLED
		byte[] fulfilledBytes = this.fulfilled.unscaledValue().toByteArray();
		fill = new byte[FULFILLED_LENGTH - fulfilledBytes.length];
		fulfilledBytes = Bytes.concat(fill, fulfilledBytes);
		data = Bytes.concat(data, fulfilledBytes);
		
		//WRITE PRICE
		byte[] priceBytes = this.price.unscaledValue().toByteArray();
		fill = new byte[PRICE_LENGTH - priceBytes.length];
		priceBytes = Bytes.concat(fill, priceBytes);
		data = Bytes.concat(data, priceBytes);
		
		//WRITE TIMESTAMP
		byte[] timestampBytes = Longs.toByteArray(this.timestamp);
		timestampBytes = Bytes.ensureCapacity(timestampBytes, TIMESTAMP_LENGTH, 0);
		data = Bytes.concat(data, timestampBytes);
		
		return data;
	}
	
	public int getDataLength() 
	{
		return BASE_LENGTH;
	}
	
	//PROCESS/ORPHAN

	public void process(DBSet db, Transaction transaction) 
	{
		//REMOVE HAVE
		this.creator.setConfirmedBalance(this.have, this.creator.getConfirmedBalance(this.have, db).subtract(this.amount), db);
		
		//ADD ORDER TO DATABASE
		db.getOrderMap().add(this.copy());
		
		//GET ALL ORDERS(WANT, HAVE) LOWEST PRICE FIRST
		List<Order> orders = db.getOrderMap().getOrders(this.want, this.have);
		
		//TRY AND COMPLETE ORDERS
		boolean completedOrder = true;
		int i = 0;
		while(completedOrder && i < orders.size())
		{
			//RESET COMPLETED
			completedOrder = false;
			
			//GET ORDER
			Order order = orders.get(i);
			
			//CALCULATE BUYING PRICE
			BigDecimal buyingPrice = BigDecimal.ONE.setScale(8).divide(order.getPrice(), RoundingMode.DOWN);
			
			//CHECK IF OWNERS OF BOTH ORDER ARE NOT THE SAME
	
				//CHECK IF BUYING PRICE IS HIGHER OR EQUAL THEN OUR SELLING PRICE
				if(buyingPrice.compareTo(this.price) >= 0)
				{
					//CALCULATE THE MAXIMUM AMOUNT WE COULD BUY
					BigDecimal amount = order.getAmountLeft();
					amount = amount.min(this.getAmountLeft().multiply(BigDecimal.ONE.setScale(8).divide(order.getPrice(), RoundingMode.DOWN)).setScale(8, RoundingMode.DOWN));
									
					//CHECK IF WE CAN BUY ANYTHING
					if(amount.compareTo(BigDecimal.ZERO) > 0)
					{
						//CALCULATE THE INCREMENTS AT WHICH WE HAVE TO BUY
						BigDecimal increment = this.calculateBuyIncrement(order, db);
						
						//CALCULATE THE AMOUNT WE CAN BUY
						amount = amount.subtract(amount.remainder(increment));
						
						//CALCULATE THE PRICE WE HAVE TO PAY
						BigDecimal price = amount.multiply(order.getPrice()).setScale(8);
						
						//CHECK IF AMOUNT AFTER ROUNDING IS NOT ZERO
						if(amount.compareTo(BigDecimal.ZERO) > 0)
						{
							//CREATE TRADE
							Trade trade = new Trade(this.getId(), order.getId(), amount, price, transaction.getTimestamp());
							trade.process(db);
							this.fulfilled = this.fulfilled.add(price);
						}
						
						//COMPLETED ORDER
						completedOrder = true;
					}
				}
			
			//INCREMENT I
			i++;
		}	
	}
	
	public void orphan(DBSet db) {
		
		//ORPHAN TRADES
		for(Trade trade: this.getInitiatedTrades(db))
		{
			trade.orphan(db);
		}
		
		//REMOVE ORDER FROM DATABASE
		db.getOrderMap().delete(this);	
		
		//REMOVE HAVE
		this.creator.setConfirmedBalance(this.have, this.creator.getConfirmedBalance(this.have, db).add(this.amount), db);
	}
	
	private BigDecimal calculateBuyIncrement(Order order, DBSet db)
	{
		BigInteger multiplier = BigInteger.valueOf(100000000l);
		
		//CALCULATE THE MINIMUM INCREMENT AT WHICH I CAN BUY USING GCD
		BigInteger haveAmount = BigInteger.ONE.multiply(multiplier);
		BigInteger priceAmount = order.getPrice().multiply(new BigDecimal(multiplier)).toBigInteger();
		BigInteger gcd = haveAmount.gcd(priceAmount);
		haveAmount = haveAmount.divide(gcd);
		priceAmount = priceAmount.divide(gcd);
		
		//CALCULATE GCD IN COMBINATION WITH DIVISIBILITY
		if(this.getWantAsset(db).isDivisible())
		{
			haveAmount = haveAmount.multiply(multiplier);
		}
		if(this.getHaveAsset(db).isDivisible())
		{
			priceAmount = priceAmount.multiply(multiplier);
		}
		gcd = haveAmount.gcd(priceAmount);
		
		//CALCULATE THE INCREMENT AT WHICH WE HAVE TO BUY
		BigDecimal increment = new BigDecimal(haveAmount.divide(gcd));
		if(this.getWantAsset(db).isDivisible())
		{
			increment = increment.divide(new BigDecimal(multiplier));
		}
		
		//RETURN
		return increment;
	}

	//COMPARE
	
	@Override
	public int compareTo(Order order) 
	{	
		//COMPARE ONLY BY PRICE
		return this.price.compareTo(order.getPrice());	
	}
	
	//COPY
	
	public Order copy() 
	{	
		try 
		{
			return parse(this.toBytes());
		} 
		catch (Exception e) 
		{
			return null;
		}	
	}
}
