package org.cryptocoinpartners.schema;

import java.math.BigDecimal;
import java.math.RoundingMode;

import javax.persistence.Transient;

import org.cryptocoinpartners.util.RemainderHandler;

/**
 * @author Tim Olson
 */
@SuppressWarnings("UnusedDeclaration")
public class DiscreteAmount extends Amount {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1169846665999381787L;

	/**
	 * helper for constructing an Amount with a double basis instead of long inverted basis. a double basis like 0.05 is easier for human configurers
	 * than the inverted basis of 20
	 */
	public static long invertBasis(double basis) {
		return Math.round(1 / basis);
	}

	/** for convenience of representation in properties files. we round to the nearest whole iBasis */
	public static DiscreteAmountBuilder withBasis(double basis) {
		assert basis > 0;
		return new DiscreteAmountBuilder(invertBasis(basis));
	}

	public static DiscreteAmountBuilder withIBasis(long iBasis) {
		assert iBasis > 0;
		return new DiscreteAmountBuilder(iBasis);
	}

	public static long roundedCountForBasis(BigDecimal amount, double basis) {
		// 8040.39365889771 precision 15 scale 11
		//precision-scale = left decimals
		BigDecimal basisBd = BigDecimal.valueOf(basis);

		BigDecimal temp = amount.setScale(basisBd.scale(), RoundingMode.HALF_EVEN);
		BigDecimal scaledtemp = temp.multiply(BigDecimal.valueOf(1 / basis));
		//8040393658899.999
		//8040394000000 00000008040394 0000000000000 40
		int precision = temp.precision(); //7
		int scale = temp.scale(); //-13
		//7--13=>20
		//precision cannnot be more that 20
		BigDecimal tranctedTemp = scaledtemp.setScale((scale < 0 ? Math.max(0, scale) : 0), RoundingMode.HALF_UP);
		//long scalefactor = (tranctedTemp.precision() - precision > 0 ? tranctedTemp.precision() - precision - 1 : 0L);
		long longValue = tranctedTemp.setScale(0).longValue();

		return longValue;
	}

	public static class DiscreteAmountBuilder {
		public DiscreteAmount fromCount(long count) {
			return new DiscreteAmount(count, iBasis);
		}

		public DiscreteAmount fromValue(BigDecimal input, RemainderHandler remainderHandler) {
			return new DecimalAmount(input).toIBasis(iBasis, remainderHandler);
		}

		private DiscreteAmountBuilder(long iBasis) {
			this.iBasis = iBasis;
		}

		private final long iBasis;
	}

	public DiscreteAmount(long count, long invertedBasis) {
		this.count = count;
		this.iBasis = invertedBasis;
	}

	public DiscreteAmount(long count, double basis) {
		this(count, invertBasis(basis));
	}

	@Transient
	public long getCount() {
		return count;
	}

	protected synchronized void setCount(long count) {
		this.count = count;
	}

	/** adds one basis to the value by incrementing the count */
	public DiscreteAmount increment() {
		return new DiscreteAmount(count + 1, iBasis);
	}

	/** adds to the value by incrementing the count by pips */
	public DiscreteAmount increment(long pips) {
		return new DiscreteAmount(count + pips, iBasis);
	}

	/** subtracts one basis from the value by decrementing the count */
	public DiscreteAmount decrement() {
		return new DiscreteAmount(count - 1, iBasis);
	}

	/** adds to the value by decrementing the count by pips */
	public DiscreteAmount decrement(long pips) {
		return new DiscreteAmount(count - pips, iBasis);
	}

	@Override
	public DiscreteAmount negate() {
		return new DiscreteAmount(-count, iBasis);
	}

	public DiscreteAmount invertAsDiscreteAmount() {

		double value = ((double) count / (double) (iBasis * iBasis));

		return value == 0 ? new DiscreteAmount(0, iBasis) : new DiscreteAmount((Math.round(1 / value)), iBasis);

	}

	@Override
	public Amount invert() {

		return new DecimalAmount(BigDecimal.ONE.divide(asBigDecimal(), mc));
	}

	@Override
	public Amount plus(Amount o) {
		if (o == null)
			System.out.println("test");
		if (o instanceof DiscreteAmount) {
			DiscreteAmount discreteOther = (DiscreteAmount) o;
			if (iBasis == discreteOther.iBasis)
				return new DiscreteAmount(count + discreteOther.count, iBasis);
		}
		return new DecimalAmount(asBigDecimal().add(o.asBigDecimal()));

	}

	@Override
	public Amount minus(Amount o) {
		if (o instanceof DiscreteAmount) {
			DiscreteAmount discreteOther = (DiscreteAmount) o;
			if (iBasis == discreteOther.iBasis)
				return new DiscreteAmount(count - discreteOther.count, iBasis);
		}
		return new DecimalAmount(asBigDecimal().subtract(o.asBigDecimal()));

	}

	@Override
	public Amount times(Amount o, RemainderHandler remainderHandler) {
		// todo shouldn't this always maintain the basis?
		if (o instanceof DiscreteAmount) {
			DiscreteAmount discreteOther = (DiscreteAmount) o;
			if (iBasis == discreteOther.iBasis)
				return new DiscreteAmount(count * discreteOther.count, iBasis * discreteOther.iBasis);
		}
		return new DecimalAmount(asBigDecimal().multiply(o.asBigDecimal()));
	}

	@Override
	public Amount dividedBy(Amount o, RemainderHandler remainderHandler) {
		BigDecimal bdDivisor = o.asBigDecimal();

		return new DecimalAmount(asBigDecimal().divide(bdDivisor.setScale(Math.max(bdDivisor.scale(), mc.getPrecision())), BigDecimal.ROUND_HALF_EVEN));

	}

	@Override
	public double asDouble() {
		return ((double) count) / iBasis;
	}

	@Override
	public BigDecimal asBigDecimal() {
		if (bd == null)
			bd = new BigDecimal(count).divide(new BigDecimal(iBasis), mc);
		return bd;
	}

	public long asLong() {
		return (count) / iBasis;

	}

	@Override
	public DiscreteAmount toIBasis(long newIBasis, RemainderHandler remainderHandler) {
		if (newIBasis % iBasis == 0) {
			// new basis is a multiple of old basis and has higher resolution.  no remainder
			return new DiscreteAmount(count * (newIBasis / iBasis), newIBasis);
		}
		BigDecimal oldAmount = asBigDecimal();
		long newCount = oldAmount.multiply(new BigDecimal(newIBasis), remainderHandler.getMathContext()).longValue();
		DiscreteAmount newAmount = new DiscreteAmount(newCount, newIBasis);
		BigDecimal remainder = oldAmount.subtract(newAmount.asBigDecimal(), remainderHandler.getMathContext());
		remainderHandler.handleRemainder(newAmount, remainder);
		return newAmount;
	}

	@Override
	@Transient
	public int getScale() {

		int length = (int) (Math.log10(iBasis));
		return length;
	}

	@Transient
	public double getBasis() {

		BigDecimal bd = new BigDecimal(iBasis);
		return (bd.ONE.divide(bd)).doubleValue();

	}

	@Override
	public int compareTo(@SuppressWarnings("NullableProblems") Amount o) {
		if (o instanceof DiscreteAmount) {
			DiscreteAmount discreteAmount = (DiscreteAmount) o;
			if (discreteAmount.iBasis == iBasis)
				return Long.compare(count, discreteAmount.count);
		}
		return asBigDecimal().compareTo(o.asBigDecimal());
	}

	@Override
	public void assertIBasis(long otherIBasis) {
		if (iBasis != otherIBasis)
			throw new BasisError();
	}

	@Override
	@Transient
	public boolean isPositive() {
		return count > 0;
	}

	@Override
	@Transient
	public boolean isZero() {
		return count == 0;
	}

	@Override
	@Transient
	public boolean isMin() {
		return count == Long.MIN_VALUE;
	}

	@Override
	@Transient
	public boolean isMax() {
		return count == Long.MAX_VALUE;
	}

	@Override
	@Transient
	public boolean isNegative() {
		return count < 0;
	}

	@Override
	public boolean equals(Object object) {
		boolean result = false;
		if (object instanceof DecimalAmount) {
			DecimalAmount decimalAmount = (DecimalAmount) object;

			if (decimalAmount.asBigDecimal().equals(asBigDecimal()))
				return true;
		} else if (object instanceof DiscreteAmount) {
			DiscreteAmount discreteAmount = (DiscreteAmount) object;

			if (discreteAmount.asBigDecimal().equals(asBigDecimal()))
				return true;
		} else {

			return false;
		}
		return result;

	}

	@Override
	public int hashCode() {
		int hash = 3;
		hash = 7 * hash + Long.valueOf(this.getCount()).hashCode();
		hash = 7 * hash + Double.valueOf(this.getBasis()).hashCode();
		return hash;
	}

	// JPA
	protected DiscreteAmount() {

	}

	/**
	 * The invertedBasis is 1/basis. This is done because we can then use a long integer instead of double, knowing that all bases must be integral
	 * factors of 1.<br/>
	 * Example inverted bases: quarters=4, dimes=10, nickels=20, pennies=100, satoshis=1e8
	 */
	private Long iBasis = 0L;
	protected Long count;
	private BigDecimal bd;

}
