/*
 * Copyright 2014 Adam Mackler
 *
 * 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 org.bitcoinj.utils;

import org.bitcoinj.base.Coin;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Collections;
import java.util.Locale;

import static java.math.BigDecimal.ONE;
import static java.math.BigDecimal.ZERO;
import static java.math.RoundingMode.HALF_UP;
import static org.bitcoinj.base.Coin.SMALLEST_UNIT_EXPONENT;

/**
 * <p>This class, a concrete extension of {@link BtcFormat}, is distinguished by its
 * accommodation of multiple denominational units as follows:
 *
 * <p>When formatting Bitcoin monetary values, an instance of this class automatically adjusts
 * the denominational units in which it represents a given value so as to minimize the number
 * of consecutive zeros in the number that is displayed, and includes either a currency code or
 * symbol in the formatted value to indicate which denomination was chosen.
 *
 * <p>When parsing {@code String} representations of Bitcoin monetary values, instances of
 * this class automatically recognize units indicators consisting of currency codes and
 * symbols, including including those containing currency or metric prefixes such as
 * {@code "¢"} or {@code "c"} to indicate hundredths, and interpret each number being
 * parsed in accordance with the recognized denominational units.
 *
 * <p>A more detailed explanation, including examples, is in the documentation for the {@link
 * BtcFormat} class, and further information beyond that is in the documentation for the {@link
 * java.text.Format} class, from which this class descends.

 * @see          java.text.Format
 * @see          java.text.NumberFormat
 * @see          java.text.DecimalFormat
 * @see          DecimalFormatSymbols
 * @see          Coin
 */

public final class BtcAutoFormat extends BtcFormat {

    /**
     * Enum for specifying the style of currency indicators that are used
     * when formatting, either codes or symbols.
     */
    public enum Style {

        /* Notes:
         * 1) The odd-looking character in the replacements below, named "currency sign," is used in
         *    the patterns recognized by Java's number formatter.  A single occurrence of this
         *    character specifies a currency symbol, while two adjacent occurrences indicate an
         *    international currency code.
         * 2) The positive and negative patterns each have three parts: prefix, number, suffix.
         *    The number characters are limited to digits, zero, decimal-separator, group-separator, and
         *    scientific-notation specifier: [#0.,E]
         *    All number characters besides 'E' must be single-quoted in order to appear as
         *    literals in either the prefix or suffix.
         * These patterns are explained in the documentation for java.text.DecimalFormat.
         */

        /** Constant for the formatting style that uses a currency code, e.g., "BTC". */
        CODE {
            @Override void apply(DecimalFormat decimalFormat) {
                /* To switch to using codes from symbols, we replace each single occurrence of the
                 * currency-sign character with two such characters in a row.
                 * We also insert a space character between every occurrence of this character and
                 * an adjacent numerical digit or negative sign (that is, between the currency-sign
                 * and the signed-number). */
                decimalFormat.applyPattern(
                    negify(decimalFormat.toPattern()).replaceAll("¤","¤¤").
                                                      replaceAll("([#0.,E-])¤¤","$1 ¤¤").
                                                      replaceAll("¤¤([0#.,E-])","¤¤ $1")
                );
            }
        },

        /** Constant for the formatting style that uses a currency symbol, e.g., "฿". */
        SYMBOL {
            @Override void apply(DecimalFormat decimalFormat) {
                /* To make certain we are using symbols rather than codes, we replace
                 * each double occurrence of the currency sign character with a single. */
                decimalFormat.applyPattern(negify(decimalFormat.toPattern()).replaceAll("¤¤","¤"));
            }
        };

        /** Effect a style corresponding to an enum value on the given number formatter object. */
        abstract void apply(DecimalFormat decimalFormat);
    }

    /** Constructor */
    protected BtcAutoFormat(Locale locale, Style style, int fractionPlaces) {
        super((DecimalFormat)NumberFormat.getCurrencyInstance(locale), fractionPlaces, Collections.emptyList());
        style.apply(this.numberFormat);
    }

    /**
     * Calculate the appropriate denomination for the given Bitcoin monetary value.  This
     * method takes a BigInteger representing a quantity of satoshis, and returns the
     * number of places that value's decimal point is to be moved when formatting said value
     * in order that the resulting number represents the correct quantity of denominational
     * units.
     *
     * <p>As a side-effect, this sets the units indicators of the underlying NumberFormat object.
     * Only invoke this from a synchronized method, and be sure to put the DecimalFormatSymbols
     * back to its proper state, otherwise immutability, equals() and hashCode() fail.
     */
    @Override
    protected int scale(BigInteger satoshis, int fractionPlaces) {
        /* The algorithm is as follows.  TODO: is there a way to optimize step 4?
           1. Can we use coin denomination w/ no rounding?  If yes, do it.
           2. Else, can we use millicoin denomination w/ no rounding? If yes, do it.
           3. Else, can we use micro denomination w/ no rounding?  If yes, do it.
           4. Otherwise we must round:
             (a) round to nearest coin + decimals
             (b) round to nearest millicoin + decimals
             (c) round to nearest microcoin + decimals
             Subtract each of (a), (b) and (c) from the true value, and choose the
             denomination that gives smallest absolute difference.  It case of tie, use the
             smaller denomination.
        */
        int places;
        int coinOffset = Math.max(SMALLEST_UNIT_EXPONENT - fractionPlaces, 0);
        BigDecimal inCoins = new BigDecimal(satoshis).movePointLeft(coinOffset);
        if (inCoins.remainder(ONE).compareTo(ZERO) == 0) {
            places = COIN_SCALE;
        } else {
            BigDecimal inMillis = inCoins.movePointRight(MILLICOIN_SCALE);
            if (inMillis.remainder(ONE).compareTo(ZERO) == 0) {
                places = MILLICOIN_SCALE;
            } else {
                BigDecimal inMicros = inCoins.movePointRight(MICROCOIN_SCALE);
                if (inMicros.remainder(ONE).compareTo(ZERO) == 0) {
                    places = MICROCOIN_SCALE;
                } else {
                    // no way to avoid rounding: so what denomination gives smallest error?
                    BigDecimal a = inCoins.subtract(inCoins.setScale(0, HALF_UP)).
                                   movePointRight(coinOffset).abs();
                    BigDecimal b = inMillis.subtract(inMillis.setScale(0, HALF_UP)).
                                   movePointRight(coinOffset - MILLICOIN_SCALE).abs();
                    BigDecimal c = inMicros.subtract(inMicros.setScale(0, HALF_UP)).
                                   movePointRight(coinOffset - MICROCOIN_SCALE).abs();
                    if (a.compareTo(b) < 0)
                        if (a.compareTo(c) < 0) places = COIN_SCALE;
                        else places = MICROCOIN_SCALE;
                    else if (b.compareTo(c) < 0) places = MILLICOIN_SCALE;
                    else places = MICROCOIN_SCALE;
                }
            }
        }
        prefixUnitsIndicator(numberFormat, places);
        return places;
    }

    /** Returns the {@code int} value indicating coin denomination.  This is what causes
     *  the number in a parsed value that lacks a units indicator to be interpreted as a quantity
     *  of bitcoins. */
    @Override
    protected int scale() { return COIN_SCALE; }

    /** Return the number of decimal places in the fraction part of numbers formatted by this
     *  instance.  This is the maximum number of fraction places that will be displayed;
     *  the actual number used is limited to a precision of satoshis. */
    public int fractionPlaces() { return minimumFractionDigits; }

    /** Return true if the other instance is equivalent to this one.
      * Formatters for different locales will never be equal, even
      * if they behave identically. */
    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof BtcAutoFormat)) return false;
        return super.equals(o);
    }

    /**
     * Return a brief description of this formatter. The exact details of the representation
     * are unspecified and subject to change, but will include some representation of the
     * pattern and the number of fractional decimal places.
     */
    @Override
    public String toString() { return "Auto-format " + pattern(); }

}
