/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.ibm.icu4jni.text;

import com.ibm.icu4jni.util.LocaleData;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.text.DecimalFormatSymbols;
import java.text.FieldPosition;
import java.text.Format;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Currency;
import java.util.NoSuchElementException;

public final class NativeDecimalFormat {
    /**
     * Constants corresponding to the native type UNumberFormatSymbol, for setSymbol.
     */
    private static final int UNUM_DECIMAL_SEPARATOR_SYMBOL = 0;
    private static final int UNUM_GROUPING_SEPARATOR_SYMBOL = 1;
    private static final int UNUM_PATTERN_SEPARATOR_SYMBOL = 2;
    private static final int UNUM_PERCENT_SYMBOL = 3;
    private static final int UNUM_ZERO_DIGIT_SYMBOL = 4;
    private static final int UNUM_DIGIT_SYMBOL = 5;
    private static final int UNUM_MINUS_SIGN_SYMBOL = 6;
    private static final int UNUM_PLUS_SIGN_SYMBOL = 7;
    private static final int UNUM_CURRENCY_SYMBOL = 8;
    private static final int UNUM_INTL_CURRENCY_SYMBOL = 9;
    private static final int UNUM_MONETARY_SEPARATOR_SYMBOL = 10;
    private static final int UNUM_EXPONENTIAL_SYMBOL = 11;
    private static final int UNUM_PERMILL_SYMBOL = 12;
    private static final int UNUM_PAD_ESCAPE_SYMBOL = 13;
    private static final int UNUM_INFINITY_SYMBOL = 14;
    private static final int UNUM_NAN_SYMBOL = 15;
    private static final int UNUM_SIGNIFICANT_DIGIT_SYMBOL = 16;
    private static final int UNUM_MONETARY_GROUPING_SEPARATOR_SYMBOL = 17;
    private static final int UNUM_FORMAT_SYMBOL_COUNT = 18;

    /**
     * Constants corresponding to the native type UNumberFormatAttribute, for
     * getAttribute/setAttribute.
     */
    private static final int UNUM_PARSE_INT_ONLY = 0;
    private static final int UNUM_GROUPING_USED = 1;
    private static final int UNUM_DECIMAL_ALWAYS_SHOWN = 2;
    private static final int UNUM_MAX_INTEGER_DIGITS = 3;
    private static final int UNUM_MIN_INTEGER_DIGITS = 4;
    private static final int UNUM_INTEGER_DIGITS = 5;
    private static final int UNUM_MAX_FRACTION_DIGITS = 6;
    private static final int UNUM_MIN_FRACTION_DIGITS = 7;
    private static final int UNUM_FRACTION_DIGITS = 8;
    private static final int UNUM_MULTIPLIER = 9;
    private static final int UNUM_GROUPING_SIZE = 10;
    private static final int UNUM_ROUNDING_MODE = 11;
    private static final int UNUM_ROUNDING_INCREMENT = 12;
    private static final int UNUM_FORMAT_WIDTH = 13;
    private static final int UNUM_PADDING_POSITION = 14;
    private static final int UNUM_SECONDARY_GROUPING_SIZE = 15;
    private static final int UNUM_SIGNIFICANT_DIGITS_USED = 16;
    private static final int UNUM_MIN_SIGNIFICANT_DIGITS = 17;
    private static final int UNUM_MAX_SIGNIFICANT_DIGITS = 18;
    private static final int UNUM_LENIENT_PARSE = 19;

    /**
     * Constants corresponding to the native type UNumberFormatTextAttribute, for
     * getTextAttribute/setTextAttribute.
     */
    private static final int UNUM_POSITIVE_PREFIX = 0;
    private static final int UNUM_POSITIVE_SUFFIX = 1;
    private static final int UNUM_NEGATIVE_PREFIX = 2;
    private static final int UNUM_NEGATIVE_SUFFIX = 3;
    private static final int UNUM_PADDING_CHARACTER = 4;
    private static final int UNUM_CURRENCY_CODE = 5;
    private static final int UNUM_DEFAULT_RULESET = 6;
    private static final int UNUM_PUBLIC_RULESETS = 7;

    /**
     * The address of the ICU DecimalFormat* on the native heap.
     */
    private int addr;

    /**
     * The last pattern we gave to ICU, so we can make repeated applications cheap.
     * This helps in cases like String.format("%.2f,%.2f\n", x, y) where the DecimalFormat is
     * reused.
     */
    private String lastPattern;

    // TODO: store all these in DecimalFormat instead!
    private boolean negPrefNull;
    private boolean negSuffNull;
    private boolean posPrefNull;
    private boolean posSuffNull;

    private transient boolean parseBigDecimal;

    /**
     * Cache the BigDecimal form of the multiplier. This is null until we've
     * formatted a BigDecimal (with a multiplier that is not 1), or the user has
     * explicitly called {@link #setMultiplier(int)} with any multiplier.
     */
    private BigDecimal multiplierBigDecimal = null;

    public NativeDecimalFormat(String pattern, DecimalFormatSymbols dfs) {
        try {
            this.addr = open(pattern, dfs.getCurrencySymbol(),
                    dfs.getDecimalSeparator(), dfs.getDigit(), dfs.getExponentSeparator(),
                    dfs.getGroupingSeparator(), dfs.getInfinity(),
                    dfs.getInternationalCurrencySymbol(), dfs.getMinusSign(),
                    dfs.getMonetaryDecimalSeparator(), dfs.getNaN(), dfs.getPatternSeparator(),
                    dfs.getPercent(), dfs.getPerMill(), dfs.getZeroDigit());
            this.lastPattern = pattern;
        } catch (NullPointerException npe) {
            throw npe;
        } catch (RuntimeException re) {
            throw new IllegalArgumentException("syntax error: " + re.getMessage() + ": " + pattern);
        }
    }

    // Used so java.util.Formatter doesn't need to allocate DecimalFormatSymbols instances.
    public NativeDecimalFormat(String pattern, LocaleData data) {
        this.addr = open(pattern, data.currencySymbol,
                data.decimalSeparator, data.digit, data.exponentSeparator, data.groupingSeparator,
                data.infinity, data.internationalCurrencySymbol, data.minusSign,
                data.monetarySeparator, data.NaN, data.patternSeparator,
                data.percent, data.perMill, data.zeroDigit);
        this.lastPattern = pattern;
    }

    // Used to implement clone.
    private NativeDecimalFormat(NativeDecimalFormat other) {
        this.addr = cloneImpl(other.addr);
        this.lastPattern = other.lastPattern;
        this.negPrefNull = other.negPrefNull;
        this.negSuffNull = other.negSuffNull;
        this.posPrefNull = other.posPrefNull;
        this.posSuffNull = other.posSuffNull;
    }

    // TODO: remove this and just have DecimalFormat.hashCode do the right thing itself.
    @Override
    public int hashCode() {
        return this.getPositivePrefix().hashCode();
    }

    public synchronized void close() {
        if (addr != 0) {
            close(addr);
            addr = 0;
        }
    }

    @Override
    public Object clone() {
        return new NativeDecimalFormat(this);
    }

    /**
     * Note: this doesn't check that the underlying native DecimalFormat objects' configured
     * native DecimalFormatSymbols objects are equal. It is assumed that the
     * caller (DecimalFormat) will check the DecimalFormatSymbols objects
     * instead, for performance.
     *
     * This is also unreasonably expensive, calling down to JNI multiple times.
     *
     * TODO: remove this and just have DecimalFormat.equals do the right thing itself.
     */
    @Override
    public boolean equals(Object object) {
        if (object == this) {
            return true;
        }
        if (!(object instanceof NativeDecimalFormat)) {
            return false;
        }
        NativeDecimalFormat obj = (NativeDecimalFormat) object;
        if (obj.addr == this.addr) {
            return true;
        }
        return obj.toPattern().equals(this.toPattern()) &&
                obj.isDecimalSeparatorAlwaysShown() == this.isDecimalSeparatorAlwaysShown() &&
                obj.getGroupingSize() == this.getGroupingSize() &&
                obj.getMultiplier() == this.getMultiplier() &&
                obj.getNegativePrefix().equals(this.getNegativePrefix()) &&
                obj.getNegativeSuffix().equals(this.getNegativeSuffix()) &&
                obj.getPositivePrefix().equals(this.getPositivePrefix()) &&
                obj.getPositiveSuffix().equals(this.getPositiveSuffix()) &&
                obj.getMaximumIntegerDigits() == this.getMaximumIntegerDigits() &&
                obj.getMaximumFractionDigits() == this.getMaximumFractionDigits() &&
                obj.getMinimumIntegerDigits() == this.getMinimumIntegerDigits() &&
                obj.getMinimumFractionDigits() == this.getMinimumFractionDigits() &&
                obj.isGroupingUsed() == this.isGroupingUsed();
    }

    /**
     * Copies the DecimalFormatSymbols settings into our native peer in bulk.
     */
    public void setDecimalFormatSymbols(final DecimalFormatSymbols dfs) {
        setDecimalFormatSymbols(this.addr, dfs.getCurrencySymbol(), dfs.getDecimalSeparator(),
                dfs.getDigit(), dfs.getExponentSeparator(), dfs.getGroupingSeparator(),
                dfs.getInfinity(), dfs.getInternationalCurrencySymbol(), dfs.getMinusSign(),
                dfs.getMonetaryDecimalSeparator(), dfs.getNaN(), dfs.getPatternSeparator(),
                dfs.getPercent(), dfs.getPerMill(), dfs.getZeroDigit());
    }

    public void setDecimalFormatSymbols(final LocaleData localeData) {
        setDecimalFormatSymbols(this.addr, localeData.currencySymbol, localeData.decimalSeparator,
                localeData.digit, localeData.exponentSeparator, localeData.groupingSeparator,
                localeData.infinity, localeData.internationalCurrencySymbol, localeData.minusSign,
                localeData.monetarySeparator, localeData.NaN, localeData.patternSeparator,
                localeData.percent, localeData.perMill, localeData.zeroDigit);
    }

    public char[] formatBigDecimal(BigDecimal value, FieldPosition field) {
        FieldPositionIterator fpi = FieldPositionIterator.forFieldPosition(field);
        char[] result = formatDigitList(this.addr, value.toString(), fpi);
        if (fpi != null) {
            FieldPositionIterator.setFieldPosition(fpi, field);
        }
        return result;
    }

    public char[] formatBigInteger(BigInteger value, FieldPosition field) {
        FieldPositionIterator fpi = FieldPositionIterator.forFieldPosition(field);
        char[] result = formatDigitList(this.addr, value.toString(10), fpi);
        if (fpi != null) {
            FieldPositionIterator.setFieldPosition(fpi, field);
        }
        return result;
    }

    public char[] formatLong(long value, FieldPosition field) {
        FieldPositionIterator fpi = FieldPositionIterator.forFieldPosition(field);
        char[] result = formatLong(this.addr, value, fpi);
        if (fpi != null) {
            FieldPositionIterator.setFieldPosition(fpi, field);
        }
        return result;
    }

    public char[] formatDouble(double value, FieldPosition field) {
        FieldPositionIterator fpi = FieldPositionIterator.forFieldPosition(field);
        char[] result = formatDouble(this.addr, value, fpi);
        if (fpi != null) {
            FieldPositionIterator.setFieldPosition(fpi, field);
        }
        return result;
    }

    public void applyLocalizedPattern(String pattern) {
        applyPattern(this.addr, true, pattern);
        lastPattern = null;
    }

    public void applyPattern(String pattern) {
        if (lastPattern != null && pattern.equals(lastPattern)) {
            return;
        }
        applyPattern(this.addr, false, pattern);
        lastPattern = pattern;
    }

    public AttributedCharacterIterator formatToCharacterIterator(Object object) {
        if (!(object instanceof Number)) {
            throw new IllegalArgumentException();
        }
        Number number = (Number) object;
        FieldPositionIterator fpIter = new FieldPositionIterator();
        String text;
        if (number instanceof BigInteger || number instanceof BigDecimal) {
            text = new String(formatDigitList(this.addr, number.toString(), fpIter));
        } else if (number instanceof Double || number instanceof Float) {
            double dv = number.doubleValue();
            text = new String(formatDouble(this.addr, dv, fpIter));
        } else {
            long lv = number.longValue();
            text = new String(formatLong(this.addr, lv, fpIter));
        }

        AttributedString as = new AttributedString(text);

        while (fpIter.next()) {
            Format.Field field = fpIter.field();
            as.addAttribute(field, field, fpIter.start(), fpIter.limit());
        }

        // return the CharacterIterator from AttributedString
        return as.getIterator();
    }

    private int makeScalePositive(int scale, StringBuilder val) {
        if (scale < 0) {
            scale = -scale;
            for (int i = scale; i > 0; i--) {
                val.append('0');
            }
            scale = 0;
        }
        return scale;
    }

    public String toLocalizedPattern() {
        return toPatternImpl(this.addr, true);
    }

    public String toPattern() {
        return toPatternImpl(this.addr, false);
    }

    public Number parse(String string, ParsePosition position) {
        return parse(addr, string, position, parseBigDecimal);
    }

    // start getter and setter

    public int getMaximumFractionDigits() {
        return getAttribute(this.addr, UNUM_MAX_FRACTION_DIGITS);
    }

    public int getMaximumIntegerDigits() {
        return getAttribute(this.addr, UNUM_MAX_INTEGER_DIGITS);
    }

    public int getMinimumFractionDigits() {
        return getAttribute(this.addr, UNUM_MIN_FRACTION_DIGITS);
    }

    public int getMinimumIntegerDigits() {
        return getAttribute(this.addr, UNUM_MIN_INTEGER_DIGITS);
    }

    public int getGroupingSize() {
        return getAttribute(this.addr, UNUM_GROUPING_SIZE);
    }

    public int getMultiplier() {
        return getAttribute(this.addr, UNUM_MULTIPLIER);
    }

    public String getNegativePrefix() {
        if (negPrefNull) {
            return null;
        }
        return getTextAttribute(this.addr, UNUM_NEGATIVE_PREFIX);
    }

    public String getNegativeSuffix() {
        if (negSuffNull) {
            return null;
        }
        return getTextAttribute(this.addr, UNUM_NEGATIVE_SUFFIX);
    }

    public String getPositivePrefix() {
        if (posPrefNull) {
            return null;
        }
        return getTextAttribute(this.addr, UNUM_POSITIVE_PREFIX);
    }

    public String getPositiveSuffix() {
        if (posSuffNull) {
            return null;
        }
        return getTextAttribute(this.addr, UNUM_POSITIVE_SUFFIX);
    }

    public boolean isDecimalSeparatorAlwaysShown() {
        return getAttribute(this.addr, UNUM_DECIMAL_ALWAYS_SHOWN) != 0;
    }

    public boolean isParseBigDecimal() {
        return parseBigDecimal;
    }

    public boolean isParseIntegerOnly() {
        return getAttribute(this.addr, UNUM_PARSE_INT_ONLY) != 0;
    }

    public boolean isGroupingUsed() {
        return getAttribute(this.addr, UNUM_GROUPING_USED) != 0;
    }

    public void setDecimalSeparatorAlwaysShown(boolean value) {
        int i = value ? -1 : 0;
        setAttribute(this.addr, UNUM_DECIMAL_ALWAYS_SHOWN, i);
    }

    public void setCurrency(Currency currency) {
        setSymbol(this.addr, UNUM_CURRENCY_SYMBOL, currency.getSymbol());
        setSymbol(this.addr, UNUM_INTL_CURRENCY_SYMBOL, currency.getCurrencyCode());
    }

    public void setGroupingSize(int value) {
        setAttribute(this.addr, UNUM_GROUPING_SIZE, value);
    }

    public void setGroupingUsed(boolean value) {
        int i = value ? -1 : 0;
        setAttribute(this.addr, UNUM_GROUPING_USED, i);
    }

    public void setMaximumFractionDigits(int value) {
        setAttribute(this.addr, UNUM_MAX_FRACTION_DIGITS, value);
    }

    public void setMaximumIntegerDigits(int value) {
        setAttribute(this.addr, UNUM_MAX_INTEGER_DIGITS, value);
    }

    public void setMinimumFractionDigits(int value) {
        setAttribute(this.addr, UNUM_MIN_FRACTION_DIGITS, value);
    }

    public void setMinimumIntegerDigits(int value) {
        setAttribute(this.addr, UNUM_MIN_INTEGER_DIGITS, value);
    }

    public void setMultiplier(int value) {
        setAttribute(this.addr, UNUM_MULTIPLIER, value);
        // Update the cached BigDecimal for multiplier.
        multiplierBigDecimal = BigDecimal.valueOf(value);
    }

    public void setNegativePrefix(String value) {
        negPrefNull = value == null;
        if (!negPrefNull) {
            setTextAttribute(this.addr, UNUM_NEGATIVE_PREFIX, value);
        }
    }

    public void setNegativeSuffix(String value) {
        negSuffNull = value == null;
        if (!negSuffNull) {
            setTextAttribute(this.addr, UNUM_NEGATIVE_SUFFIX, value);
        }
    }

    public void setPositivePrefix(String value) {
        posPrefNull = value == null;
        if (!posPrefNull) {
            setTextAttribute(this.addr, UNUM_POSITIVE_PREFIX, value);
        }
    }

    public void setPositiveSuffix(String value) {
        posSuffNull = value == null;
        if (!posSuffNull) {
            setTextAttribute(this.addr, UNUM_POSITIVE_SUFFIX, value);
        }
    }

    public void setParseBigDecimal(boolean value) {
        parseBigDecimal = value;
    }

    public void setParseIntegerOnly(boolean value) {
        int i = value ? -1 : 0;
        setAttribute(this.addr, UNUM_PARSE_INT_ONLY, i);
    }

    private static void applyPattern(int addr, boolean localized, String pattern) {
        try {
            applyPatternImpl(addr, localized, pattern);
        } catch (NullPointerException npe) {
            throw npe;
        } catch (RuntimeException re) {
            throw new IllegalArgumentException("syntax error: " + re.getMessage() + ": " + pattern);
        }
    }

    public void setRoundingMode(RoundingMode roundingMode, double roundingIncrement) {
        final int nativeRoundingMode;
        switch (roundingMode) {
        case CEILING: nativeRoundingMode = 0; break;
        case FLOOR: nativeRoundingMode = 1; break;
        case DOWN: nativeRoundingMode = 2; break;
        case UP: nativeRoundingMode = 3; break;
        case HALF_EVEN: nativeRoundingMode = 4; break;
        case HALF_DOWN: nativeRoundingMode = 5; break;
        case HALF_UP: nativeRoundingMode = 6; break;
        default: throw new AssertionError();
        }
        setRoundingMode(addr, nativeRoundingMode, roundingIncrement);
    }

    // Utility to get information about field positions from native (ICU) code.
    private static class FieldPositionIterator {
        private int[] data;
        private int pos = -3; // so first call to next() leaves pos at 0

        private FieldPositionIterator() {
        }

        public static FieldPositionIterator forFieldPosition(FieldPosition fp) {
            if (fp != null && fp.getField() != -1) {
                return new FieldPositionIterator();
            }
            return null;
        }

        private static int getNativeFieldPositionId(FieldPosition fp) {
            // NOTE: -1, 0, and 1 were the only valid original java field values
            // for NumberFormat.  They take precedence.  This assumes any other
            // value is a mistake and the actual value is in the attribute.
            // Clients can construct FieldPosition combining any attribute with any field
            // value, which is just wrong, but there you go.

            int id = fp.getField();
            if (id < -1 || id > 1) {
                id = -1;
            }
            if (id == -1) {
                Format.Field attr = fp.getFieldAttribute();
                if (attr != null) {
                    for (int i = 0; i < fields.length; ++i) {
                        if (fields[i].equals(attr)) {
                            id = i;
                            break;
                        }
                    }
                }
            }
            return id;
        }

        private static void setFieldPosition(FieldPositionIterator fpi, FieldPosition fp) {
            if (fpi != null && fp != null) {
                int field = getNativeFieldPositionId(fp);
                if (field != -1) {
                    while (fpi.next()) {
                        if (fpi.fieldId() == field) {
                            fp.setBeginIndex(fpi.start());
                            fp.setEndIndex(fpi.limit());
                            break;
                        }
                    }
                }
            }
        }

        public boolean next() {
            // if pos == data.length, we've already returned false once
            if (data == null || pos == data.length) {
                throw new NoSuchElementException();
            }
            pos += 3;
            return pos < data.length;
        }

        private void checkValid() {
            if (data == null || pos < 0 || pos == data.length) {
                throw new NoSuchElementException();
            }
        }

        public int fieldId() {
            return data[pos];
        }

        public Format.Field field() {
            checkValid();
            return fields[data[pos]];
        }

        public int start() {
            checkValid();
            return data[pos + 1];
        }

        public int limit() {
            checkValid();
            return data[pos + 2];
        }

        private static Format.Field fields[] = {
            // The old java field values were 0 for integer and 1 for fraction.
            // The new java field attributes are all objects.  ICU assigns the values
            // starting from 0 in the following order; note that integer and
            // fraction positions match the old field values.
            NumberFormat.Field.INTEGER,
            NumberFormat.Field.FRACTION,
            NumberFormat.Field.DECIMAL_SEPARATOR,
            NumberFormat.Field.EXPONENT_SYMBOL,
            NumberFormat.Field.EXPONENT_SIGN,
            NumberFormat.Field.EXPONENT,
            NumberFormat.Field.GROUPING_SEPARATOR,
            NumberFormat.Field.CURRENCY,
            NumberFormat.Field.PERCENT,
            NumberFormat.Field.PERMILLE,
            NumberFormat.Field.SIGN,
        };

        // called by native
        private void setData(int[] data) {
            this.data = data;
            this.pos = -3;
        }
    }

    private static native void applyPatternImpl(int addr, boolean localized, String pattern);
    private static native int cloneImpl(int addr);
    private static native void close(int addr);
    private static native char[] formatLong(int addr, long value, FieldPositionIterator iter);
    private static native char[] formatDouble(int addr, double value, FieldPositionIterator iter);
    private static native char[] formatDigitList(int addr, String value, FieldPositionIterator iter);
    private static native int getAttribute(int addr, int symbol);
    private static native String getTextAttribute(int addr, int symbol);
    private static native int open(String pattern, String currencySymbol,
            char decimalSeparator, char digit, String exponentSeparator, char groupingSeparator,
            String infinity, String internationalCurrencySymbol, char minusSign,
            char monetaryDecimalSeparator, String nan, char patternSeparator, char percent,
            char perMill, char zeroDigit);
    private static native Number parse(int addr, String string, ParsePosition position, boolean parseBigDecimal);
    private static native void setDecimalFormatSymbols(int addr, String currencySymbol,
            char decimalSeparator, char digit, String exponentSeparator, char groupingSeparator,
            String infinity, String internationalCurrencySymbol, char minusSign,
            char monetaryDecimalSeparator, String nan, char patternSeparator, char percent,
            char perMill, char zeroDigit);
    private static native void setSymbol(int addr, int symbol, String str);
    private static native void setAttribute(int addr, int symbol, int i);
    private static native void setRoundingMode(int addr, int roundingMode, double roundingIncrement);
    private static native void setTextAttribute(int addr, int symbol, String str);
    private static native String toPatternImpl(int addr, boolean localized);
}
