// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;

import java.math.BigDecimal;
import java.math.BigInteger;

/**
 * A DecimalQuantity with internal storage as a 64-bit BCD, with fallback to a byte array for numbers
 * that don't fit into the standard BCD.
 */
public final class DecimalQuantity_DualStorageBCD extends DecimalQuantity_AbstractBCD {

    /**
     * The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map to
     * one digit. For example, the number "12345" in BCD is "0x12345".
     *
     * <p>
     * Whenever bcd changes internally, {@link #compact()} must be called, except in special cases like
     * setting the digit to zero.
     */
    private byte[] bcdBytes;

    private long bcdLong = 0L;

    private boolean usingBytes = false;

    @Override
    public int maxRepresentableDigits() {
        return Integer.MAX_VALUE;
    }

    public DecimalQuantity_DualStorageBCD() {
        setBcdToZero();
        flags = 0;
    }

    public DecimalQuantity_DualStorageBCD(long input) {
        setToLong(input);
    }

    public DecimalQuantity_DualStorageBCD(int input) {
        setToInt(input);
    }

    public DecimalQuantity_DualStorageBCD(double input) {
        setToDouble(input);
    }

    public DecimalQuantity_DualStorageBCD(BigInteger input) {
        setToBigInteger(input);
    }

    public DecimalQuantity_DualStorageBCD(BigDecimal input) {
        setToBigDecimal(input);
    }

    public DecimalQuantity_DualStorageBCD(DecimalQuantity_DualStorageBCD other) {
        copyFrom(other);
    }

    public DecimalQuantity_DualStorageBCD(Number number) {
        // NOTE: Number type expansion happens both here
        // and in NumberFormat.java
        if (number instanceof Long) {
            setToLong(number.longValue());
        } else if (number instanceof Integer) {
            setToInt(number.intValue());
        } else if (number instanceof Float) {
            setToDouble(number.doubleValue());
        } else if (number instanceof Double) {
            setToDouble(number.doubleValue());
        } else if (number instanceof BigInteger) {
            setToBigInteger((BigInteger) number);
        } else if (number instanceof BigDecimal) {
            setToBigDecimal((BigDecimal) number);
        } else if (number instanceof com.ibm.icu.math.BigDecimal) {
            setToBigDecimal(((com.ibm.icu.math.BigDecimal) number).toBigDecimal());
        } else {
            throw new IllegalArgumentException(
                    "Number is of an unsupported type: " + number.getClass().getName());
        }
    }

    @Override
    public DecimalQuantity createCopy() {
        return new DecimalQuantity_DualStorageBCD(this);
    }

    @Override
    protected byte getDigitPos(int position) {
        if (usingBytes) {
            if (position < 0 || position >= precision)
                return 0;
            return bcdBytes[position];
        } else {
            if (position < 0 || position >= 16)
                return 0;
            return (byte) ((bcdLong >>> (position * 4)) & 0xf);
        }
    }

    @Override
    protected void setDigitPos(int position, byte value) {
        assert position >= 0;
        if (usingBytes) {
            ensureCapacity(position + 1);
            bcdBytes[position] = value;
        } else if (position >= 16) {
            switchStorage();
            ensureCapacity(position + 1);
            bcdBytes[position] = value;
        } else {
            int shift = position * 4;
            bcdLong = bcdLong & ~(0xfL << shift) | ((long) value << shift);
        }
    }

    @Override
    protected void shiftLeft(int numDigits) {
        if (!usingBytes && precision + numDigits > 16) {
            switchStorage();
        }
        if (usingBytes) {
            ensureCapacity(precision + numDigits);
            int i = precision + numDigits - 1;
            for (; i >= numDigits; i--) {
                bcdBytes[i] = bcdBytes[i - numDigits];
            }
            for (; i >= 0; i--) {
                bcdBytes[i] = 0;
            }
        } else {
            bcdLong <<= (numDigits * 4);
        }
        scale -= numDigits;
        precision += numDigits;
    }

    @Override
    protected void shiftRight(int numDigits) {
        if (usingBytes) {
            int i = 0;
            for (; i < precision - numDigits; i++) {
                bcdBytes[i] = bcdBytes[i + numDigits];
            }
            for (; i < precision; i++) {
                bcdBytes[i] = 0;
            }
        } else {
            bcdLong >>>= (numDigits * 4);
        }
        scale += numDigits;
        precision -= numDigits;
    }

    @Override
    protected void popFromLeft(int numDigits) {
        assert numDigits <= precision;
        if (usingBytes) {
            int i = precision - 1;
            for (; i >= precision - numDigits; i--) {
                bcdBytes[i] = 0;
            }
        } else {
            bcdLong &= (1L << ((precision - numDigits) * 4)) - 1;
        }
        precision -= numDigits;
    }

    @Override
    protected void setBcdToZero() {
        if (usingBytes) {
            bcdBytes = null;
            usingBytes = false;
        }
        bcdLong = 0L;
        scale = 0;
        precision = 0;
        isApproximate = false;
        origDouble = 0;
        origDelta = 0;
    }

    @Override
    protected void readIntToBcd(int n) {
        assert n != 0;
        // ints always fit inside the long implementation.
        long result = 0L;
        int i = 16;
        for (; n != 0; n /= 10, i--) {
            result = (result >>> 4) + (((long) n % 10) << 60);
        }
        assert !usingBytes;
        bcdLong = result >>> (i * 4);
        scale = 0;
        precision = 16 - i;
    }

    @Override
    protected void readLongToBcd(long n) {
        assert n != 0;
        if (n >= 10000000000000000L) {
            ensureCapacity();
            int i = 0;
            for (; n != 0L; n /= 10L, i++) {
                bcdBytes[i] = (byte) (n % 10);
            }
            assert usingBytes;
            scale = 0;
            precision = i;
        } else {
            long result = 0L;
            int i = 16;
            for (; n != 0L; n /= 10L, i--) {
                result = (result >>> 4) + ((n % 10) << 60);
            }
            assert i >= 0;
            assert !usingBytes;
            bcdLong = result >>> (i * 4);
            scale = 0;
            precision = 16 - i;
        }
    }

    @Override
    protected void readBigIntegerToBcd(BigInteger n) {
        assert n.signum() != 0;
        ensureCapacity(); // allocate initial byte array
        int i = 0;
        for (; n.signum() != 0; i++) {
            BigInteger[] temp = n.divideAndRemainder(BigInteger.TEN);
            ensureCapacity(i + 1);
            bcdBytes[i] = temp[1].byteValue();
            n = temp[0];
        }
        scale = 0;
        precision = i;
    }

    @Override
    protected BigDecimal bcdToBigDecimal() {
        if (usingBytes) {
            // Converting to a string here is faster than doing BigInteger/BigDecimal arithmetic.
            BigDecimal result = new BigDecimal(toNumberString());
            if (isNegative()) {
                result = result.negate();
            }
            return result;
        } else {
            long tempLong = 0L;
            for (int shift = (precision - 1); shift >= 0; shift--) {
                tempLong = tempLong * 10 + getDigitPos(shift);
            }
            BigDecimal result = BigDecimal.valueOf(tempLong);
            // Test that the new scale fits inside the BigDecimal
            long newScale = result.scale() + scale;
            if (newScale <= Integer.MIN_VALUE) {
                result = BigDecimal.ZERO;
            } else {
                result = result.scaleByPowerOfTen(scale);
            }
            if (isNegative()) {
                result = result.negate();
            }
            return result;
        }
    }

    @Override
    protected void compact() {
        if (usingBytes) {
            int delta = 0;
            for (; delta < precision && bcdBytes[delta] == 0; delta++)
                ;
            if (delta == precision) {
                // Number is zero
                setBcdToZero();
                return;
            } else {
                // Remove trailing zeros
                shiftRight(delta);
            }

            // Compute precision
            int leading = precision - 1;
            for (; leading >= 0 && bcdBytes[leading] == 0; leading--)
                ;
            precision = leading + 1;

            // Switch storage mechanism if possible
            if (precision <= 16) {
                switchStorage();
            }

        } else {
            if (bcdLong == 0L) {
                // Number is zero
                setBcdToZero();
                return;
            }

            // Compact the number (remove trailing zeros)
            int delta = Long.numberOfTrailingZeros(bcdLong) / 4;
            bcdLong >>>= delta * 4;
            scale += delta;

            // Compute precision
            precision = 16 - (Long.numberOfLeadingZeros(bcdLong) / 4);
        }
    }

    /** Ensure that a byte array of at least 40 digits is allocated. */
    private void ensureCapacity() {
        ensureCapacity(40);
    }

    private void ensureCapacity(int capacity) {
        if (capacity == 0)
            return;
        int oldCapacity = usingBytes ? bcdBytes.length : 0;
        if (!usingBytes) {
            bcdBytes = new byte[capacity];
        } else if (oldCapacity < capacity) {
            byte[] bcd1 = new byte[capacity * 2];
            System.arraycopy(bcdBytes, 0, bcd1, 0, oldCapacity);
            bcdBytes = bcd1;
        }
        usingBytes = true;
    }

    /** Switches the internal storage mechanism between the 64-bit long and the byte array. */
    private void switchStorage() {
        if (usingBytes) {
            // Change from bytes to long
            bcdLong = 0L;
            for (int i = precision - 1; i >= 0; i--) {
                bcdLong <<= 4;
                bcdLong |= bcdBytes[i];
            }
            bcdBytes = null;
            usingBytes = false;
        } else {
            // Change from long to bytes
            ensureCapacity();
            for (int i = 0; i < precision; i++) {
                bcdBytes[i] = (byte) (bcdLong & 0xf);
                bcdLong >>>= 4;
            }
            assert usingBytes;
        }
    }

    @Override
    protected void copyBcdFrom(DecimalQuantity _other) {
        DecimalQuantity_DualStorageBCD other = (DecimalQuantity_DualStorageBCD) _other;
        setBcdToZero();
        if (other.usingBytes) {
            ensureCapacity(other.precision);
            System.arraycopy(other.bcdBytes, 0, bcdBytes, 0, other.precision);
        } else {
            bcdLong = other.bcdLong;
        }
    }

    /**
     * Checks whether the bytes stored in this instance are all valid. For internal unit testing only.
     *
     * @return An error message if this instance is invalid, or null if this instance is healthy.
     * @internal
     * @deprecated This API is for ICU internal use only.
     */
    @Deprecated
    public String checkHealth() {
        if (usingBytes) {
            if (bcdLong != 0)
                return "Value in bcdLong but we are in byte mode";
            if (precision == 0)
                return "Zero precision but we are in byte mode";
            if (precision > bcdBytes.length)
                return "Precision exceeds length of byte array";
            if (getDigitPos(precision - 1) == 0)
                return "Most significant digit is zero in byte mode";
            if (getDigitPos(0) == 0)
                return "Least significant digit is zero in long mode";
            for (int i = 0; i < precision; i++) {
                if (getDigitPos(i) >= 10)
                    return "Digit exceeding 10 in byte array";
                if (getDigitPos(i) < 0)
                    return "Digit below 0 in byte array";
            }
            for (int i = precision; i < bcdBytes.length; i++) {
                if (getDigitPos(i) != 0)
                    return "Nonzero digits outside of range in byte array";
            }
        } else {
            if (bcdBytes != null) {
                for (int i = 0; i < bcdBytes.length; i++) {
                    if (bcdBytes[i] != 0)
                        return "Nonzero digits in byte array but we are in long mode";
                }
            }
            if (precision == 0 && bcdLong != 0)
                return "Value in bcdLong even though precision is zero";
            if (precision > 16)
                return "Precision exceeds length of long";
            if (precision != 0 && getDigitPos(precision - 1) == 0)
                return "Most significant digit is zero in long mode";
            if (precision != 0 && getDigitPos(0) == 0)
                return "Least significant digit is zero in long mode";
            for (int i = 0; i < precision; i++) {
                if (getDigitPos(i) >= 10)
                    return "Digit exceeding 10 in long";
                if (getDigitPos(i) < 0)
                    return "Digit below 0 in long (?!)";
            }
            for (int i = precision; i < 16; i++) {
                if (getDigitPos(i) != 0)
                    return "Nonzero digits outside of range in long";
            }
        }

        return null;
    }

    /**
     * Checks whether this {@link DecimalQuantity_DualStorageBCD} is using its internal byte array
     * storage mechanism.
     *
     * @return true if an internal byte array is being used; false if a long is being used.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public boolean isUsingBytes() {
        return usingBytes;
    }

    @Override
    public String toString() {
        return String.format("<DecimalQuantity %d:%d %s %s%s>",
                lReqPos,
                rReqPos,
                (usingBytes ? "bytes" : "long"),
                (isNegative() ? "-" : ""),
                toNumberString());
    }

    private String toNumberString() {
        StringBuilder sb = new StringBuilder();
        if (usingBytes) {
            if (precision == 0) {
                sb.append('0');
            }
            for (int i = precision - 1; i >= 0; i--) {
                sb.append(bcdBytes[i]);
            }
        } else {
            sb.append(Long.toHexString(bcdLong));
        }
        sb.append("E");
        sb.append(scale);
        return sb.toString();
    }
}
