/*
 *******************************************************************************
 * Copyright (C) 2007-2011, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */

package com.ibm.icu.text;

import java.text.FieldPosition;
import java.text.ParsePosition;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import com.ibm.icu.impl.UCharacterProperty;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.ULocale.Category;

/**
 * <p>
 * <code>PluralFormat</code> supports the creation of internationalized
 * messages with plural inflection. It is based on <i>plural
 * selection</i>, i.e. the caller specifies messages for each
 * plural case that can appear in the users language and the
 * <code>PluralFormat</code> selects the appropriate message based on
 * the number.
 * </p>
 * <h4>The Problem of Plural Forms in Internationalized Messages</h4>
 * <p>
 * Different languages have different ways to inflect
 * plurals. Creating internationalized messages that include plural
 * forms is only feasible when the framework is able to handle plural
 * forms of <i>all</i> languages correctly. <code>ChoiceFormat</code>
 * doesn't handle this well, because it attaches a number interval to
 * each message and selects the message whose interval contains a
 * given number. This can only handle a finite number of
 * intervals. But in some languages, like Polish, one plural case
 * applies to infinitely many intervals (e.g., paucal applies to
 * numbers ending with 2, 3, or 4 except those ending with 12, 13, or
 * 14). Thus <code>ChoiceFormat</code> is not adequate.
 * </p><p>
 * <code>PluralFormat</code> deals with this by breaking the problem
 * into two parts:
 * <ul>
 * <li>It uses <code>PluralRules</code> that can define more complex
 *     conditions for a plural case than just a single interval. These plural
 *     rules define both what plural cases exist in a language, and to
 *     which numbers these cases apply.
 * <li>It provides predefined plural rules for many locales. Thus, the programmer
 *     need not worry about the plural cases of a language. On the flip side,
 *     the localizer does not have to specify the plural cases; he can simply
 *     use the predefined keywords. The whole plural formatting of messages can
 *     be done using localized patterns from resource bundles.
 * </ul>
 * </p>
 * <h4>Usage of <code>PluralFormat</code></h4>
 * <p>
 * This discussion assumes that you use <code>PluralFormat</code> with
 * a predefined set of plural rules. You can create one using one of
 * the constructors that takes a <code>ULocale</code> object. To
 * specify the message pattern, you can either pass it to the
 * constructor or set it explicitly using the
 * <code>applyPattern()</code> method. The <code>format()</code>
 * method takes a number object and selects the message of the
 * matching plural case. This message will be returned.
 * </p>
 * <h5>Patterns and Their Interpretation</h5>
 * <p>
 * The pattern text defines the message output for each plural case of the
 * used locale. The pattern is a sequence of
 * <code><i>caseKeyword</i>{<i>message</i>}</code> clauses, separated by white
 * space characters. Each clause assigns the message <code><i>message</i></code>
 * to the plural case identified by <code><i>caseKeyword</i></code>.
 * </p><p>
 * You always have to define a message text for the default plural case
 * "<code>other</code>" which is contained in every rule set. If the plural
 * rules of the <code>PluralFormat</code> object do not contain a plural case
 * identified by <code><i>caseKeyword</i></code>, an
 * <code>IllegalArgumentException</code> is thrown.
 * If you do not specify a message text for a particular plural case, the
 * message text of the plural case "<code>other</code>" gets assigned to this
 * plural case. If you specify more than one message for the same plural case,
 * an <code>IllegalArgumentException</code> is thrown.
 * <br/>
 * Spaces between <code><i>caseKeyword</i></code> and
 * <code><i>message</i></code>  will be ignored; spaces within
 * <code><i>message</i></code> will be preserved.
 * </p><p>
 * The message text for a particular plural case may contain other message
 * format patterns. <code>PluralFormat</code> preserves these so that you
 * can use the strings produced by <code>PluralFormat</code> with other
 * formatters. If you are using <code>PluralFormat</code> inside a
 * <code>MessageFormat</code> pattern, <code>MessageFormat</code> will
 * automatically evaluate the resulting format pattern.<br/>
 * Thus, curly braces (<code>{</code>, <code>}</code>) are <i>only</i> allowed
 * in message texts to define a nested format pattern.<br/>
 * The pound sign (<code>#</code>) will be interpreted as the number placeholder
 * in the message text, if it is not contained in curly braces (to preserve
 * <code>NumberFormat</code> patterns). <code>PluralFormat</code> will
 * replace each of those pound signs by the number passed to the
 * <code>format()</code> method. It will be formatted using a
 * <code>NumberFormat</code> for the <code>PluralFormat</code>'s locale. If you
 * need special number formatting, you have to explicitly specify a
 * <code>NumberFormat</code> for the <code>PluralFormat</code> to use.
 * </p>
 * Example
 * <pre>
 * MessageFormat msgFmt = new MessageFormat("{0, plural, " +
 *     "one{{0, number, C''''est #,##0.0#  fichier}} " +
 *     "other {Ce sont # fichiers}} dans la liste.",
 *     new ULocale("fr"));
 * Object args[] = {new Long(0)};
 * System.out.println(msgFmt.format(args));
 * args = {new Long(3)};
 * System.out.println(msgFmt.format(args));
 * </pre>
 * Produces the output:<br />
 * <code>C'est 0,0 fichier dans la liste.</code><br />
 * <code>Ce sont 3 fichiers dans la liste."</code>
 * <p>
 * <strong>Note:</strong><br />
 *   Currently <code>PluralFormat</code>
 *   does not make use of quotes like <code>MessageFormat</code>.
 *   If you use plural format strings with <code>MessageFormat</code> and want
 *   to use a quote sign "<code>'</code>", you have to write "<code>''</code>".
 *   <code>MessageFormat</code> unquotes this pattern and  passes the unquoted
 *   pattern to <code>PluralFormat</code>. It's a bit trickier if you use
 *   nested formats that do quoting. In the example above, we wanted to insert
 *   "<code>'</code>" in the number format pattern. Since
 *   <code>NumberFormat</code> supports quotes, we had to insert
 *   "<code>''</code>". But since <code>MessageFormat</code> unquotes the
 *   pattern before it gets passed to <code>PluralFormat</code>, we have to
 *   double these quotes, i.e. write "<code>''''</code>".
 * </p>
 * <h4>Defining Custom Plural Rules</h4>
 * <p>If you need to use <code>PluralFormat</code> with custom rules, you can
 * create a <code>PluralRules</code> object and pass it to
 * <code>PluralFormat</code>'s constructor. If you also specify a locale in this
 * constructor, this locale will be used to format the number in the message
 * texts.
 * </p><p>
 * For more information about <code>PluralRules</code>, see
 * {@link PluralRules}.
 * </p>
 *
 * @author tschumann (Tim Schumann)
 * @stable ICU 3.8
 */
public class PluralFormat extends UFormat {
    private static final long serialVersionUID = 1L;

    /*
     * The locale used for standard number formatting and getting the predefined
     * plural rules (if they were not defined explicitely).
     */
    private ULocale ulocale = null;

    /*
     * The plural rules used for plural selection.
     */
    private PluralRules pluralRules = null;

    /*
     * The applied pattern string.
     */
    private String pattern = null;

    /*
     * The format messages for each plural case. It is a mapping:
     *  <code>String</code>(plural case keyword) --&gt; <code>String</code>
     *  (message for this plural case).
     */
    private Map<String, String> parsedValues = null;

    /*
     * This <code>NumberFormat</code> is used for the standard formatting of
     * the number inserted into the message.
     */
    private NumberFormat numberFormat = null;

    /**
     * Creates a new <code>PluralFormat</code> for the default <code>FORMAT</code> locale.
     * This locale will be used to get the set of plural rules and for standard
     * number formatting.
     * @see Category#FORMAT
     * @stable ICU 3.8
     */
    public PluralFormat() {
        init(null, ULocale.getDefault(Category.FORMAT));
    }

    /**
     * Creates a new <code>PluralFormat</code> for a given locale.
     * @param ulocale the <code>PluralFormat</code> will be configured with
     *        rules for this locale. This locale will also be used for standard
     *        number formatting.
     * @stable ICU 3.8
     */
    public PluralFormat(ULocale ulocale) {
        init(null, ulocale);
    }

    /**
     * Creates a new <code>PluralFormat</code> for a given set of rules.
     * The standard number formatting will be done using the default <code>FORMAT</code> locale.
     * @param rules defines the behavior of the <code>PluralFormat</code>
     *        object.
     * @see Category#FORMAT
     * @stable ICU 3.8
     */
    public PluralFormat(PluralRules rules) {
        init(rules, ULocale.getDefault(Category.FORMAT));
    }

    /**
     * Creates a new <code>PluralFormat</code> for a given set of rules.
     * The standard number formatting will be done using the given locale.
     * @param ulocale the default number formatting will be done using this
     *        locale.
     * @param rules defines the behavior of the <code>PluralFormat</code>
     *        object.
     * @stable ICU 3.8
     */
    public PluralFormat(ULocale ulocale, PluralRules rules) {
        init(rules, ulocale);
    }

    /**
     * Creates a new <code>PluralFormat</code> for a given pattern string.
     * The default <code>FORMAT</code> locale will be used to get the set of plural rules and for
     * standard number formatting.
     * @param  pattern the pattern for this <code>PluralFormat</code>.
     * @throws IllegalArgumentException if the pattern is invalid.
     * @see Category#FORMAT
     * @stable ICU 3.8
     */
    public PluralFormat(String pattern) {
        init(null, ULocale.getDefault(Category.FORMAT));
        applyPattern(pattern);
    }

    /**
     * Creates a new <code>PluralFormat</code> for a given pattern string and
     * locale.
     * The locale will be used to get the set of plural rules and for
     * standard number formatting.
     * @param ulocale the <code>PluralFormat</code> will be configured with
     *        rules for this locale. This locale will also be used for standard
     *        number formatting.
     * @param  pattern the pattern for this <code>PluralFormat</code>.
     * @throws IllegalArgumentException if the pattern is invalid.
     * @stable ICU 3.8
     */
    public PluralFormat(ULocale ulocale, String pattern) {
        init(null, ulocale);
        applyPattern(pattern);
    }

    /**
     * Creates a new <code>PluralFormat</code> for a given set of rules and a
     * pattern.
     * The standard number formatting will be done using the default <code>FORMAT</code> locale.
     * @param rules defines the behavior of the <code>PluralFormat</code>
     *        object.
     * @param  pattern the pattern for this <code>PluralFormat</code>.
     * @throws IllegalArgumentException if the pattern is invalid.
     * @see Category#FORMAT
     * @stable ICU 3.8
     */
    public PluralFormat(PluralRules rules, String pattern) {
        init(rules, ULocale.getDefault(Category.FORMAT));
        applyPattern(pattern);
    }

    /**
     * Creates a new <code>PluralFormat</code> for a given set of rules, a
     * pattern and a locale.
     * @param ulocale the <code>PluralFormat</code> will be configured with
     *        rules for this locale. This locale will also be used for standard
     *        number formatting.
     * @param rules defines the behavior of the <code>PluralFormat</code>
     *        object.
     * @param  pattern the pattern for this <code>PluralFormat</code>.
     * @throws IllegalArgumentException if the pattern is invalid.
     * @stable ICU 3.8
     */
    public PluralFormat(ULocale ulocale, PluralRules rules, String pattern) {
        init(rules, ulocale);
        applyPattern(pattern);
    }

    /*
     * Initializes the <code>PluralRules</code> object.
     * Postcondition:<br/>
     *   <code>ulocale</code>    :  is <code>locale</code><br/>
     *   <code>pluralRules</code>:  if <code>rules</code> != <code>null</code>
     *                              it's set to rules, otherwise it is the
     *                              predefined plural rule set for the locale
     *                              <code>ulocale</code>.<br/>
     *   <code>parsedValues</code>: is <code>null</code><br/>
     *   <code>pattern</code>:      is <code>null</code><br/>
     *   <code>numberFormat</code>: a <code>NumberFormat</code> for the locale
     *                              <code>ulocale</code>.
     */
    private void init(PluralRules rules, ULocale locale) {
        ulocale = locale;
        pluralRules = (rules == null) ? PluralRules.forLocale(ulocale)
                                      : rules;
        parsedValues = null;
        pattern = null;
        numberFormat = NumberFormat.getInstance(ulocale);
    }

    /**
     * Sets the pattern used by this plural format.
     * The method parses the pattern and creates a map of format strings
     * for the plural rules.
     * Patterns and their interpretation are specified in the class description.
     *
     * @param pttrn the pattern for this plural format.
     * @throws IllegalArgumentException if the pattern is invalid.
     * @stable ICU 3.8
     */
    public void applyPattern(String pttrn) {
        pttrn = pttrn.trim();

        this.pattern = pttrn;
        int braceStack = 0;
        Set<String> ruleNames = pluralRules.getKeywords();
        parsedValues = new HashMap<String, String>();

        // Format string has to include keywords.
        // states:
        // 0: Reading keyword.
        // 1: Reading value for preceding keyword.
        int state = 0;
        StringBuilder token = new StringBuilder();
        String currentKeyword = null;
        boolean readSpaceAfterKeyword = false;
        for (int i = 0; i < pttrn.length(); ++i) {
            char ch = pttrn.charAt(i);
            switch (state) {
            case 0: // Reading value.
                if (token.length() == 0) {
                    readSpaceAfterKeyword = false;
                }
                if (UCharacterProperty.isRuleWhiteSpace(ch)) {
                    if (token.length() > 0) {
                        readSpaceAfterKeyword = true;
                    }
                    // Skip leading and trailing whitespaces.
                    break;
                }
                if (ch == '{') { // End of keyword definition reached.
                    currentKeyword = token.toString().toLowerCase(
                            Locale.ENGLISH);
                    if (!ruleNames.contains(currentKeyword)) {
                        parsingFailure("Malformed formatting expression. "
                                + "Unknown keyword \"" + currentKeyword
                                + "\" at position " + i + ".");
                    }
                    if (parsedValues.get(currentKeyword) != null) {
                        parsingFailure("Malformed formatting expression. "
                                + "Text for case \"" + currentKeyword
                                + "\" at position " + i + " already defined!");
                    }
                    token.delete(0, token.length());
                    braceStack++;
                    state = 1;
                    break;
                }
                if (readSpaceAfterKeyword) {
                    parsingFailure("Malformed formatting expression. " +
                            "Invalid keyword definition. Character \"" + ch +
                            "\" at position " + i + " not expected!");
                }
                token.append(ch);
                break;
            case 1: // Reading value.
                switch (ch) {
                case '{':
                    braceStack++;
                    token.append(ch);
                    break;
                case '}':
                    braceStack--;
                    if (braceStack == 0) { // End of value reached.
                        parsedValues.put(currentKeyword, token.toString());
                        token.delete(0, token.length());
                        state = 0;
                    } else if (braceStack < 0) {
                        parsingFailure("Malformed formatting expression. "
                                + "Braces do not match.");
                    } else { // braceStack > 0
                        token.append(ch);
                    }
                    break;
                default:
                    token.append(ch);
                }
                break;
            } // switch state
        } // for loop.
        if (braceStack != 0) {
            parsingFailure(
                    "Malformed formatting expression. Braces do not match.");
        }
        checkSufficientDefinition();
    }

    /**
     * Returns the pattern for this PluralFormat.
     *
     * @return the pattern string
     * @stable ICU 4.2
     */
    public String toPattern() {
        return pattern;
    }

    /**
     * Formats a plural message for a given number.
     *
     * @param number a number for which the plural message should be formatted.
     *        If no pattern has been applied to this
     *        <code>PluralFormat</code> object yet, the formatted number will
     *        be returned.
     * @return the string containing the formatted plural message.
     * @stable ICU 4.0
     */
    public final String format(double number) {
        // If no pattern was applied, return the formatted number.
        if (parsedValues == null) {
            return numberFormat.format(number);
        }

        // Get appropriate format pattern.
        String selectedRule = pluralRules.select(number);
        String selectedPattern = parsedValues.get(selectedRule);
        if (selectedPattern == null) { // Fallback to others.
            selectedPattern = parsedValues.get(PluralRules.KEYWORD_OTHER);
        }
        // Get formatted number and insert it into String.
        // Will replace all '#' which are not inside curly braces by the
        // formatted number.
        return insertFormattedNumber(number, selectedPattern);
    }

    /**
     * Formats a plural message for a given number and appends the formatted
     * message to the given <code>StringBuffer</code>.
     * @param number a number object (instance of <code>Number</code> for which
     *        the plural message should be formatted. If no pattern has been
     *        applied to this <code>PluralFormat</code> object yet, the
     *        formatted number will be returned.
     *        Note: If this object is not an instance of <code>Number</code>,
     *              the <code>toAppendTo</code> will not be modified.
     * @param toAppendTo the formatted message will be appended to this
     *        <code>StringBuffer</code>.
     * @param pos will be ignored by this method.
     * @return the string buffer passed in as toAppendTo, with formatted text
     *         appended.
     * @throws IllegalArgumentException if number is not an instance of Number
     * @stable ICU 3.8
     */
    public StringBuffer format(Object number, StringBuffer toAppendTo,
            FieldPosition pos) {
        if (number instanceof Number) {
            toAppendTo.append(format(((Number) number).doubleValue()));
            return toAppendTo;
        }
        throw new IllegalArgumentException("'" + number +
                                           "' is not a Number");
    }

    /**
     * This method is not yet supported by <code>PluralFormat</code>.
     * @param text the string to be parsed.
     * @param parsePosition defines the position where parsing is to begin,
     * and upon return, the position where parsing left off.  If the position
     * has not changed upon return, then parsing failed.
     * @return nothing because this method is not yet implemented.
     * @throws UnsupportedOperationException will always be thrown by this method.
     * @stable ICU 3.8
     */
    public Number parse(String text, ParsePosition parsePosition) {
        throw new UnsupportedOperationException();
    }

    /**
     * This method is not yet supported by <code>PluralFormat</code>.
     * @param source the string to be parsed.
     * @param pos defines the position where parsing is to begin,
     * and upon return, the position where parsing left off.  If the position
     * has not changed upon return, then parsing failed.
     * @return nothing because this method is not yet implemented.
     * @throws UnsupportedOperationException will always be thrown by this method.
     * @stable ICU 3.8
     */
    public Object parseObject(String source, ParsePosition pos) {
        throw new UnsupportedOperationException();
    }

    /**
     * Sets the locale used by this <code>PluraFormat</code> object.
     * Note: Calling this method resets this <code>PluraFormat</code> object,
     *     i.e., a pattern that was applied previously will be removed,
     *     and the NumberFormat is set to the default number format for
     *     the locale.  The resulting format behaves the same as one
     *     constructed from {@link #PluralFormat(ULocale)}.
     * @param ulocale the <code>ULocale</code> used to configure the
     *     formatter. If <code>ulocale</code> is <code>null</code>, the
     *     default <code>FORMAT</code> locale will be used.
     * @see Category#FORMAT
     * @stable ICU 3.8
     */
    public void setLocale(ULocale ulocale) {
        if (ulocale == null) {
            ulocale = ULocale.getDefault(Category.FORMAT);
        }
        init(null, ulocale);
    }

    /**
     * Sets the number format used by this formatter.  You only need to
     * call this if you want a different number format than the default
     * formatter for the locale.
     * @param format the number format to use.
     * @stable ICU 3.8
     */
    public void setNumberFormat(NumberFormat format) {
        numberFormat = format;
    }

    /*
     * Checks if the applied pattern provided enough information,
     * i.e., if the attribute <code>parsedValues</code> stores enough
     * information for plural formatting.
     * Will be called at the end of pattern parsing.
     * @throws IllegalArgumentException if there's not sufficient information
     *     provided.
     */
    private void checkSufficientDefinition() {
        // Check that at least the default rule is defined.
        if (parsedValues.get(PluralRules.KEYWORD_OTHER) == null) {
            parsingFailure("Malformed formatting expression.\n"
                    + "Value for case \"" + PluralRules.KEYWORD_OTHER
                    + "\" was not defined.");
        }
    }

    /*
     * Helper method that resets the <code>PluralFormat</code> object and throws
     * an <code>IllegalArgumentException</code> with a given error text.
     * @param errorText the error text of the exception message.
     * @throws IllegalArgumentException will always be thrown by this method.
     */
    private void parsingFailure(String errorText) {
        // Set PluralFormat to a valid state.
        init(null, ULocale.getDefault(Category.FORMAT));
        throw new IllegalArgumentException(errorText);
    }

    /*
     * Helper method that is called during formatting.
     * It replaces the character '#' by the number used for plural selection in
     * a message text. Only '#' are replaced, that are not written inside curly
     * braces. This allows the use of nested number formats.
     * The number will be formatted using the attribute
     * <code>numberformat</code>.
     * @param number the number used for plural selection.
     * @param message is the text in which '#' will be replaced.
     * @return the text with inserted numbers.
     */
    private String insertFormattedNumber(double number, String message) {
        if (message == null) {
            return "";
        }
        String formattedNumber = numberFormat.format(number);
        StringBuilder result = new StringBuilder();
        int braceStack = 0;
        int startIndex = 0;
        for (int i = 0; i < message.length(); ++i) {
            switch (message.charAt(i)) {
            case '{':
                ++braceStack;
                break;
            case '}':
                --braceStack;
                break;
            case '#':
                if (braceStack == 0) {
                    result.append(message.substring(startIndex,i));
                    startIndex = i + 1;
                    result.append(formattedNumber);
                }
                break;
            }
        }
        if (startIndex < message.length()) {
            result.append(message.substring(startIndex, message.length()));
        }
        return result.toString();
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    public boolean equals(Object rhs) {
        return rhs instanceof PluralFormat && equals((PluralFormat) rhs);
    }

    /**
     * Returns true if this equals the provided PluralFormat.
     * @param rhs the PluralFormat to compare against
     * @return true if this equals rhs
     * @stable ICU 3.8
     */
    public boolean equals(PluralFormat rhs) {
      return pluralRules.equals(rhs.pluralRules) &&
          parsedValues.equals(rhs.parsedValues) &&
          numberFormat.equals(rhs.numberFormat);
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    public int hashCode() {
        return pluralRules.hashCode() ^ parsedValues.hashCode();
    }

    /**
     * For debugging purposes only
     * @return a text representation of the format data.
     * @stable ICU 3.8
     */
    public String toString() {
        StringBuilder buf = new StringBuilder();
        buf.append("locale=" + ulocale);
        buf.append(", rules='" + pluralRules + "'");
        buf.append(", pattern='" + pattern + "'");
        buf.append(", parsedValues='" + parsedValues + "'");
        buf.append(", format='" + numberFormat + "'");
        return buf.toString();
    }
}
