package org.cryptocoinpartners.schema;

import java.math.BigDecimal;
import java.math.MathContext;

import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;

import org.cryptocoinpartners.util.RemainderHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Amount has a polymorphic base representation of either a BigDecimal or a special long/long format which captures discrete amounts as a count plus a
 * basis. The basis is stored inverted so it can be represented as a long instead of a double. Long/long is the preferred format for discrete amounts
 * like trading volumes and prices, while I/O prefers BigDecimal. Whenever a downcast or rounding happens, the remainders are recorded, along with a
 * RemainderHandler delegate which is to dispatch of the remainder. No calls to the RemainderHandlers are invoked until settle() is called on the
 * Amount, at which time all accumulated remainders since the last settle() are pushed to their respective RemainderHandlers.
 * 
 * @author Tim Olson
 */
@MappedSuperclass
public abstract class Amount implements Comparable<Amount>, java.io.Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 6644052682251199772L;
	public static final MathContext mc = MathContext.DECIMAL32; //  IEEE 64-bit decimal, scale 16

	/**
	 * only when absolutely necessary
	 */
	public abstract double asDouble();

	public abstract BigDecimal asBigDecimal();

	@Transient
	public abstract int getScale();

	public DiscreteAmount toBasis(double newBasis, RemainderHandler remainderHandler) {
		long newIBasis = DiscreteAmount.invertBasis(newBasis);
		return toIBasis(newIBasis, remainderHandler);
	}

	public abstract DiscreteAmount toIBasis(long newIBasis, RemainderHandler remainderHandler);

	public void assertBasis(double basis) throws BasisError {
		long otherIBasis = DiscreteAmount.invertBasis(basis);
		assertIBasis(otherIBasis);
	}

	public abstract void assertIBasis(long otherIBasis);

	@Override
	public String toString() {
		return asBigDecimal().toString();
	}

	public class BasisError extends Error {
	}

	public Amount abs() {
		return isNegative() ? negate() : this;
	}

	@Transient
	public abstract boolean isPositive();

	@Transient
	public abstract boolean isZero();

	@Transient
	public abstract boolean isMax();

	@Transient
	public abstract boolean isMin();

	@Transient
	public abstract boolean isNegative();

	@Transient
	public abstract Amount negate();

	@Transient
	public abstract Amount plus(Amount o);

	@Transient
	public abstract Amount minus(Amount o);

	@Transient
	public DecimalAmount times(BigDecimal o, RemainderHandler remainderHandler) {
		return new DecimalAmount(asBigDecimal().multiply(o, remainderHandler.getMathContext()));
	}

	@Transient
	public DecimalAmount dividedBy(BigDecimal o, RemainderHandler remainderHandler) {
		BigDecimal[] divideAndRemainder = asBigDecimal().divideAndRemainder(o, remainderHandler.getMathContext());
		DecimalAmount result = new DecimalAmount(divideAndRemainder[0]);
		remainderHandler.handleRemainder(result, divideAndRemainder[1]);
		return result;
	}

	@Transient
	public DecimalAmount divide(Amount o, RemainderHandler remainderHandler) {
		return divide(o.asBigDecimal(), remainderHandler);
	}

	@Transient
	public DecimalAmount divide(BigDecimal o, RemainderHandler remainderHandler) {
		// o.setScale(mc.getPrecision(), remainderHandler.getRoundingMode());
		// this.asBigDecimal().setScale(mc.getPrecision(), remainderHandler.getRoundingMode());

		BigDecimal division = asBigDecimal().divide(o, mc.getPrecision(), remainderHandler.getRoundingMode());
		DecimalAmount result = DecimalAmount.of(division);
		return result;
	}

	@Transient
	public Amount times(int o, RemainderHandler remainderHandler) {
		return times(new BigDecimal(o), remainderHandler);
	}

	@Transient
	public Amount dividedBy(int o, RemainderHandler remainderHandler) {
		return dividedBy(new BigDecimal(o), remainderHandler);
	}

	@Transient
	public DecimalAmount divide(int o, RemainderHandler remainderHandler) {

		BigDecimal division = asBigDecimal().divide(BigDecimal.valueOf(o), remainderHandler.getRoundingMode());
		DecimalAmount result = DecimalAmount.of(division);
		return result;
	}

	@Transient
	public Amount times(double o, RemainderHandler remainderHandler) {
		return times(new BigDecimal(o), remainderHandler);
	}

	@Transient
	public Amount dividedBy(double o, RemainderHandler remainderHandler) {
		return dividedBy(new BigDecimal(o), remainderHandler);
	}

	@Transient
	public abstract Amount times(Amount o, RemainderHandler remainderHandler);

	@Transient
	public abstract Amount dividedBy(Amount o, RemainderHandler remainderHandler);

	protected static final Logger log = LoggerFactory.getLogger(Amount.class);

	@Transient
	public abstract Amount invert();

}
