package com.ripple.core.coretypes;

import com.ripple.core.coretypes.uint.UInt64;
import com.ripple.core.fields.AmountField;
import com.ripple.core.fields.Field;
import com.ripple.core.fields.Type;
import com.ripple.core.serialized.BinaryParser;
import com.ripple.core.serialized.BytesSink;
import com.ripple.core.serialized.SerializedType;
import com.ripple.core.serialized.TypeTranslator;
import org.json.JSONObject;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;

/**
 * In ripple, amounts are either XRP, the native currency, or an IOU of
 * a given currency as issued by a designated account.
 */
public class Amount extends Number implements SerializedType, Comparable<Amount>

{

    private static BigDecimal TAKER_PAYS_FOR_THAT_DAMN_OFFER = new BigDecimal("1000000000000.000100");
//    public static final Amount NEUTRAL_ZERO = new Amount(Currency.NEUTRAL, AccountID.NEUTRAL);

    /**
     * Thrown when an Amount is constructed with an invalid value
     */
    public static class PrecisionError extends RuntimeException {
        public Amount illegal;

        public PrecisionError(String s) {
            super(s);
        }

        public PrecisionError(String s, Amount amount) {
            super(s);
            illegal = amount;
        }
    }

    // For rounding/multiplying/dividing
    public static final MathContext MATH_CONTEXT = new MathContext(16, RoundingMode.HALF_UP);
    // The maximum amount of digits in mantissa of an IOU amount
    public static final int MAXIMUM_IOU_PRECISION = 16;
    // The smallest quantity of an XRP is a drop, 1 millionth of an XRP
    public static final int MAXIMUM_NATIVE_SCALE = 6;
    // Defines bounds for native amounts
    public static final BigDecimal MAX_NATIVE_VALUE = parseDecimal("100,000,000,000.0");
    public static final BigDecimal MIN_NATIVE_VALUE = parseDecimal("0.000,001");

    // These are flags used when serializing to binary form
    public static final UInt64 BINARY_FLAG_IS_IOU = new UInt64("8000000000000000", 16);
    public static final UInt64 BINARY_FLAG_IS_NON_NEGATIVE_NATIVE = new UInt64("4000000000000000", 16);

    public static final Amount ONE_XRP = fromString("1.0");

    // The quantity of XRP or Issue(currency/issuer pairing)
    // When native, the value unit is XRP, not drops.
    private BigDecimal value;
    private Currency currency;
    // If the currency is XRP
    private boolean isNative;
    // Normally, in the constructor of an Amount the value is checked
    // that it's scale/precision and quantity are correctly bounded.
    // If unbounded is true, these checks are skipped.
    // This is there for historical ledgers that contain amounts that
    // would now be considered malformed (in the sense of the transaction 
    // engine result class temMALFORMED)
    private boolean unbounded = false;
    // The ZERO account is used for specifying the issuer for native 
    // amounts. In practice the issuer is never used when an
    // amount is native.
    private AccountID issuer;

    // While internally the value is stored as a BigDecimal
    // the mantissa and exponent, as per the binary
    // format can be computed.
    // The mantissa is computed lazily, then cached
    private UInt64 mantissa = null;
    // The exponent is always calculated.
    private int exponent;

    public Amount(BigDecimal value, Currency currency, AccountID issuer) {
        this(value, currency, issuer, false);
    }

    public Amount(BigDecimal value) {
        isNative = true;
        currency = Currency.XRP;
        this.setAndCheckValue(value);
    }

    public Amount(BigDecimal value, Currency currency, AccountID issuer, boolean isNative, boolean unbounded) {
        this.isNative = isNative;
        this.currency = currency;
        this.unbounded = unbounded;
        this.setAndCheckValue(value);
        // done AFTER set value which sets some default values
        this.issuer = issuer;
    }

    public Amount(Currency currency, AccountID account) {
        this(BigDecimal.ZERO, currency, account);
    }

    // Private constructors
    Amount(BigDecimal newValue, Currency currency, AccountID issuer, boolean isNative) {
        this(newValue, currency, issuer, isNative, false);
    }

    private Amount(BigDecimal value, String currency, String issuer) {
        this(value, currency);
        if (issuer != null) {
            this.issuer = AccountID.fromString(issuer);
        }
    }

    public Amount(BigDecimal value, String currency) {
        isNative = false;
        this.currency = Currency.fromString(currency);
        this.setAndCheckValue(value);
    }

    private void setAndCheckValue(BigDecimal value) {
        this.value = value.stripTrailingZeros();
        initialize();
    }

    private void initialize() {
        if (isNative()) {
            issuer = AccountID.XRP_ISSUER;
            if (!unbounded) {
                checkXRPBounds();
            }
            // Offset is unused for native amounts
            exponent = -6; // compared to drops.
        } else {
            issuer = AccountID.NEUTRAL;
            exponent = calculateExponent();

            if (value.precision() > MAXIMUM_IOU_PRECISION && !unbounded) {
                String err = "value precision of " + value.precision() + " is greater than maximum " +
                        "iou precision of " + MAXIMUM_IOU_PRECISION;
                throw new PrecisionError(err, this);
            }
        }
    }

    private Amount newValue(BigDecimal newValue) {
        return newValue(newValue, false, false);
    }

    private Amount newValue(BigDecimal newValue, boolean round, boolean unbounded) {
        if (round) {
            newValue = roundValue(newValue, isNative);
        }
        return new Amount(newValue, currency, issuer, isNative, unbounded);
    }

    private Amount newValue(BigDecimal val, boolean round) {
        return newValue(val, round, false);
    }

    /* Getters and Setters */

    public BigDecimal value() {
        return value;
    }

    public Currency currency() {
        return currency;
    }

    public AccountID issuer() {
        return issuer;
    }

    public Issue issue() {
        // TODO: store the currency and issuer as an Issue
        return new Issue(currency, issuer);
    }

    public UInt64 mantissa() {
        if (mantissa == null) {
            mantissa = calculateMantissa();
        }
        return mantissa;
    }

    public int exponent() {
        return exponent;
    }

    public boolean isNative() {
        return isNative;
    }

    public String currencyString() {
        return currency.toString();
    }

    public String issuerString() {
        if (issuer == null) {
            return "";
        }
        return issuer.toString();
    }

    /* Offset & Mantissa Helpers */

    /**
     * @return a positive value for the mantissa
     */
    private UInt64 calculateMantissa() {
        if (isNative()) {
            return new UInt64(bigIntegerDrops().abs());
        } else {
            return new UInt64(bigIntegerIOUMantissa());
        }
    }

    protected int calculateExponent() {
        return -MAXIMUM_IOU_PRECISION + value.precision() - value.scale();
    }

    public BigInteger bigIntegerIOUMantissa() {
        return exactBigIntegerScaledByPowerOfTen(-exponent).abs();
    }

    private BigInteger bigIntegerDrops() {
        return exactBigIntegerScaledByPowerOfTen(MAXIMUM_NATIVE_SCALE);
    }

    private BigInteger exactBigIntegerScaledByPowerOfTen(int n) {
        return value.scaleByPowerOfTen(n).toBigIntegerExact();
    }

    /* Equality testing */

    private boolean equalValue(Amount amt) {
        return compareTo(amt) == 0;
    }
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Amount) {
            return equals((Amount) obj);
        }
        return super.equals(obj);
    }

    public boolean equals(Amount amt) {
        return equalValue(amt) &&
                currency.equals(amt.currency) &&
                (isNative() || issuer.equals(amt.issuer));
    }

    public boolean equalsExceptIssuer(Amount amt) {
        return equalValue(amt) &&
                currencyString().equals(amt.currencyString());
    }



    public int compareTo(Amount amount) {
        return value.compareTo(amount.value);
    }

    public boolean isZero() {
        return value.signum() == 0;
    }

    public boolean isNegative() {
        return value.signum() == -1;
    }

    // Maybe you want !isNegative()
    // Any amount that !isNegative() isn't necessarily positive
    // Is a zero amount strictly positive? no
    public boolean isPositive() {
        return value.signum() == 1;
    }

    /**

    Arithmetic Operations

    There's no checking if an amount is of a different currency/issuer.
    
    All operations return amounts of the same currency/issuer as the
    first operand.

    eg.

        amountOne.add(amountTwo)

        The currency/issuer of the resultant amount, is that of `amountOne`
    
    Divide and multiply are equivalent to the javascript ripple-lib
    ratio_human and product_human.

    */
    public Amount add(BigDecimal augend) {
        return newValue(value.add(augend), true);
    }

    public Amount add(Amount augend) {
        return add(augend.value);
    }

    public Amount add(Number augend) {
        return add(BigDecimal.valueOf(augend.doubleValue()));
    }

    public Amount subtract(BigDecimal subtrahend) {
        return newValue(value.subtract(subtrahend), true);
    }

    public Amount subtract(Amount subtrahend) {
        return subtract(subtrahend.value);
    }

    public Amount subtract(Number subtrahend) {
        return subtract(BigDecimal.valueOf(subtrahend.doubleValue()));
    }

    public Amount multiply(BigDecimal divisor) {
        return newValue(value.multiply(divisor, MATH_CONTEXT), true);
    }

    public Amount multiply(Amount multiplicand) {
        return multiply(multiplicand.value);
    }

    public Amount multiply(Number multiplicand) {
        return multiply(BigDecimal.valueOf(multiplicand.doubleValue()));
    }

    public Amount divide(BigDecimal divisor) {
        return newValue(value.divide(divisor, MATH_CONTEXT), true);
    }

    public Amount divide(Amount divisor) {
        return divide(divisor.value);
    }

    public Amount divide(Number divisor) {
        return divide(BigDecimal.valueOf(divisor.doubleValue()));
    }

    public Amount negate() {
        return newValue(value.negate());
    }

    public Amount abs() {
        return newValue(value.abs());
    }
    public Amount min(Amount val) {
        return (compareTo(val) <= 0 ? this : val);
    }
    public Amount max(Amount val) {
        return (compareTo(val) >= 0 ? this : val);
    }

    /* Offer related helpers */
    public BigDecimal computeQuality(Amount toExchangeThisWith) {
        return value.divide(toExchangeThisWith.value, MathContext.DECIMAL128);
    }
    /**
     * @return Amount
     * The real native unit is a drop, one million of which are an XRP.
     * We want `one` unit at XRP scale (1e6 drops), or if it's an IOU,
     * just `one`.
     */
    public Amount one() {
        if (isNative()) {
            return ONE_XRP;
        } else {
            return issue().amount(1);
        }
    }

    /* Serialized Type implementation */

    @Override
    public Object toJSON() {
        if (isNative()) {
            return toDropsString();
        } else {
            return toJSONObject();
        }
    }

    public JSONObject toJSONObject() {
        if (isNative()) {
            throw new RuntimeException("Native amounts must be serialized as a string");
        }

        JSONObject out = new JSONObject();
        out.put("currency", currencyString());
        out.put("value", valueText());
        out.put("issuer", issuerString());
        return out;
    }

    @Override
    public byte[] toBytes() {
        return translate.toBytes(this);
    }

    @Override
    public String toHex() {
        return translate.toHex(this);
    }

    @Override
    public void toBytesSink(BytesSink to) {
        UInt64 man = mantissa();

        if (isNative()) {
            if (!isNegative()) {
                man = man.or(BINARY_FLAG_IS_NON_NEGATIVE_NATIVE);
            }
            to.add(man.toByteArray());
        } else {
            int exponent = exponent();
            UInt64 packed;

            if (isZero()) {
                packed = BINARY_FLAG_IS_IOU;
            } else if (isNegative()) {
                packed = man.or(new UInt64(512 + 0 + 97 + exponent).shiftLeft(64 - 10));
            } else {
                packed = man.or(new UInt64(512 + 256 + 97 + exponent).shiftLeft(64 - 10));
            }

            to.add(packed.toByteArray());
            to.add(currency.bytes());
            to.add(issuer.bytes());
        }
    }

    @Override
    public Type type() {
        return Type.Amount;
    }

    public static class Translator extends TypeTranslator<Amount> {
        @Override
        public Amount fromString(String s) {
            // We need to use the full dotted.path here, otherwise
            // we get confused with the AmountField Amount
            return com.ripple.core.coretypes.Amount.fromString(s);
        }

        @Override
        public Amount fromParser(BinaryParser parser, Integer hint) {
            BigDecimal value;
            byte[] mantissa = parser.read(8);
            byte b1 = mantissa[0], b2 = mantissa[1];

            boolean isIOU = (b1 & 0x80) != 0;
            boolean isPositive = (b1 & 0x40) != 0;
            int sign = isPositive ? 1 : -1;

            if (isIOU) {
                mantissa[0] = 0;
                Currency curr = Currency.translate.fromParser(parser);
                AccountID issuer = AccountID.translate.fromParser(parser);
                int exponent = ((b1 & 0x3F) << 2) + ((b2 & 0xff) >> 6) - 97;
                mantissa[1] &= 0x3F;

                value = new BigDecimal(new BigInteger(sign, mantissa), -exponent);
                return new Amount(value, curr, issuer, false);
            } else {
                mantissa[0] &= 0x3F;
                value = xrpFromDropsMantissa(mantissa, sign);
                return new Amount(value);
            }
        }

        @Override
        public String toString(Amount obj) {
            return obj.stringRepr();
        }

        @Override
        public JSONObject toJSONObject(Amount obj) {
            return obj.toJSONObject();
        }

        @Override
        public Amount fromJSONObject(JSONObject jsonObject) {
            String valueString = jsonObject.getString("value");
            String issuerString = jsonObject.getString("issuer");
            String currencyString = jsonObject.getString("currency");
            return new Amount(new BigDecimal(valueString), currencyString, issuerString);
        }
    }
    static public Translator translate = new Translator();

    public static BigDecimal xrpFromDropsMantissa(byte[] mantissa, int sign) {
        return new BigDecimal(new BigInteger(sign, mantissa), 6);
    }

    /* Number overides */
    @Override
    public int intValue() {
        return value.intValueExact();
    }

    @Override
    public long longValue() {
        return value.longValueExact();
    }

    @Override
    public float floatValue() {
        return value.floatValue();
    }

    @Override
    public double doubleValue() {
        return value.doubleValue();
    }

    public BigInteger bigIntegerValue() {
        return value.toBigIntegerExact();
    }

    public Amount newIssuer(AccountID issuer) {
        return new Amount(value, currency, issuer);
    }

    public Amount copy() {
        return new Amount(value, currency, issuer, isNative, unbounded);
    }

    // Static constructors
    public static Amount fromString(String val) {
        if (val.contains("/")) {
            return fromIOUString(val);
        } else if (val.contains(".")) {
            return fromXrpString(val);
        } else {
            return fromDropString(val);
        }
    }

    public static Amount fromDropString(String val) {
        BigDecimal drops = new BigDecimal(val).scaleByPowerOfTen(-6);
        checkDropsValueWhole(val);
        return new Amount(drops);
    }

    public static Amount fromIOUString(String val) {
        String[] split = val.split("/");
        if (split.length == 1) {
            throw new RuntimeException("IOU string must be in the form number/currencyString or number/currencyString/issuerString");
        } else if (split.length == 2) {
            return new Amount(new BigDecimal(split[0]), split[1]);
        } else {
            return new Amount(new BigDecimal(split[0]), split[1], split[2]);
        }
    }

    @Deprecated
    private static Amount fromXrpString(String valueString) {
        BigDecimal val = new BigDecimal(valueString);
        return new Amount(val);
    }

    /**
     * @return A String representation as used by ripple json format
     */
    public String stringRepr() {
        if (isNative()) {
            return toDropsString();
        } else {
            return iouTextFull();
        }
    }

    public String toDropsString() {
        if (!isNative()) {
            throw new RuntimeException("Amount is not native");
        }
        return bigIntegerDrops().toString();
    }

    private String iouText() {
        return String.format("%s/%s", valueText(), currencyString());
    }

    public String iouTextFull() {
        return String.format("%s/%s/%s", valueText(), currencyString(), issuerString());
    }

    public String toTextFull() {
        if (isNative()) {
            return nativeText();
        } else {
            return iouTextFull();
        }
    }

    public String nativeText() {
        return String.format("%s/XRP", valueText());
    }

    @Override
    public String toString() {
        return toTextFull();
    }

    public String toText() {
        if (isNative()) {
            return nativeText();
        } else {
            return iouText();
        }
    }

    /**
     * @return A String containing the value as a decimal number (in XRP scale)
     */
    public String valueText() {
        return value.signum() == 0 ? "0" : value().toPlainString();
    }

    public void checkLowerDropBound(BigDecimal val) {
        if (val.scale() > 6) {
            PrecisionError bigger = getOutOfBoundsError(val,
                                    "smaller than min native value",
                                    MIN_NATIVE_VALUE);
            bigger.illegal = this;
            throw bigger;
        }
    }

    public void checkUpperBound(BigDecimal val) {
        if (val.compareTo(MAX_NATIVE_VALUE) == 1) {
            PrecisionError bigger = getOutOfBoundsError(val,
                                    "bigger than max native value ",
                                    MAX_NATIVE_VALUE);
            bigger.illegal = this;
            throw bigger;
        }
    }

    private static PrecisionError getOutOfBoundsError(BigDecimal abs, String sized, BigDecimal bound) {
        return new PrecisionError(abs.toPlainString() + " absolute XRP is " + sized + bound);
    }

    public void checkXRPBounds() {
        BigDecimal v = value.abs();
        if (v.compareTo(TAKER_PAYS_FOR_THAT_DAMN_OFFER) == 0) {
            return;
        }
        checkLowerDropBound(v);
        checkUpperBound(v);
    }


    private static int significantDigits(BigDecimal input) {
        input = input.stripTrailingZeros();
        return input.scale() < 0
                ? input.precision() - input.scale()
                : input.precision();
    }

    public int significantDigits() {
        return significantDigits(value);
    }

    public static void checkDropsValueWhole(String drops) {
        boolean contains = drops.contains(".");
        if (contains) {
            throw new RuntimeException("Drops string contains floating point is decimal");
        }
    }

    public static BigDecimal roundValue(BigDecimal value, boolean nativeSrc) {
        int i = value.precision() - value.scale();
        return value.setScale(nativeSrc ? MAXIMUM_NATIVE_SCALE :
                MAXIMUM_IOU_PRECISION - i,
                MATH_CONTEXT.getRoundingMode());
    }

    private static BigDecimal parseDecimal(String s) {
        return new BigDecimal(s.replace(",", "")); //# .scaleByPowerOfTen(6);
    }

    private static AmountField amountField(final Field f) {
        return new AmountField() {
            @Override
            public Field getField() {
                return f;
            }
        };
    }

    static public AmountField Amount = amountField(Field.Amount);
    static public AmountField Balance = amountField(Field.Balance);
    static public AmountField LimitAmount = amountField(Field.LimitAmount);
    static public AmountField DeliveredAmount = amountField(Field.DeliveredAmount);
    static public AmountField TakerPays = amountField(Field.TakerPays);
    static public AmountField TakerGets = amountField(Field.TakerGets);
    static public AmountField LowLimit = amountField(Field.LowLimit);
    static public AmountField HighLimit = amountField(Field.HighLimit);
    static public AmountField Fee = amountField(Field.Fee);
    static public AmountField SendMax = amountField(Field.SendMax);
    static public AmountField MinimumOffer = amountField(Field.MinimumOffer);
    static public AmountField RippleEscrow = amountField(Field.RippleEscrow);
    static public AmountField taker_gets_funded = amountField(Field.taker_gets_funded);
    static public AmountField taker_pays_funded = amountField(Field.taker_pays_funded);

}