/*
 * Copyright 2012 Google Inc.
 *
 * 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.google.template.soy.i18ndirectives;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.data.restricted.NumberData;
import com.google.template.soy.data.restricted.StringData;
import com.google.template.soy.internal.targetexpr.TargetExpr;
import com.google.template.soy.jssrc.restricted.JsExpr;
import com.google.template.soy.jssrc.restricted.SoyLibraryAssistedJsSrcPrintDirective;
import com.google.template.soy.pysrc.restricted.PyExpr;
import com.google.template.soy.pysrc.restricted.PyExprUtils;
import com.google.template.soy.pysrc.restricted.PyFunctionExprBuilder;
import com.google.template.soy.pysrc.restricted.SoyPySrcPrintDirective;
import com.google.template.soy.shared.restricted.ApiCallScopeBindingAnnotations.LocaleString;
import com.google.template.soy.shared.restricted.SoyJavaPrintDirective;

import com.ibm.icu.text.CompactDecimalFormat;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.util.ULocale;

import java.util.List;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Provider;

/**
 * A directive that formats an input number based on Locale of the current SoyMsgBundle.
 * It may take two optional arguments. The first is a lower-case string describing the type of
 * format to apply, which can be one of 'decimal', 'currency', 'percent', 'scientific',
 * 'compact_short', or 'compact_long'. If this argument is not provided, the default 'decimal'
 * will be used. The second argument is the "numbers" keyword passed to the ICU4J's locale. For
 * instance, it can be "native" so that we show native characters in languages like arabic (this
 * argument is ignored for templates running in JavaScript).
 *
 * <p>Usage examples:
 * {@code
       {$value|formatNum}
       {$value|formatNum:'decimal'}
       {$value|formatNum:'decimal','native'}
   }
 *
 */
class FormatNumDirective implements SoyJavaPrintDirective, SoyLibraryAssistedJsSrcPrintDirective,
      SoyPySrcPrintDirective {


  // Map of format arguments to the Closure Format enum.
  private static final ImmutableMap<String, String> JS_ARGS_TO_ENUM =
      ImmutableMap.<String, String>builder()
          .put("'decimal'", "goog.i18n.NumberFormat.Format.DECIMAL")
          .put("'currency'", "goog.i18n.NumberFormat.Format.CURRENCY")
          .put("'percent'", "goog.i18n.NumberFormat.Format.PERCENT")
          .put("'scientific'", "goog.i18n.NumberFormat.Format.SCIENTIFIC")
          .put("'compact_short'", "goog.i18n.NumberFormat.Format.COMPACT_SHORT")
          .put("'compact_long'", "goog.i18n.NumberFormat.Format.COMPACT_LONG")
          .build();

  // This directive can be called with no arguments, with one argument setting the format type,
  // or with two arguments setting the format type and the 'numbers' keyword for the ICU4J
  // formatter.
  private static final ImmutableSet<Integer> VALID_ARGS_SIZES = ImmutableSet.of(0, 1, 2);

  private static final ImmutableSet<String> REQUIRED_JS_LIBS =
      ImmutableSet.of("goog.i18n.NumberFormat");

  private static final String DEFAULT_FORMAT = "decimal";


  /**
   * Provide the current Locale string.
   *
   * <p>Note that this Locale value is only used in the Java environment. Closure does not provide a
   * clear mechanism to override the NumberFormat defined when the NumberFormat module loads. This
   * is probably not a significant loss of functionality, since the primary reason to inject the
   * LocaleString is because the Java VM's default Locale may not be the same as the desired Locale
   * for the page, while in the JavaScript environment, the value of goog.LOCALE should reliably
   * indicate which Locale Soy should use. Similarly, the Python backend relies on implementation
   * specific runtime locale support.
   */
  private final Provider<String> localeStringProvider;

  @Inject
  FormatNumDirective(@LocaleString Provider<String> localeStringProvider) {
    this.localeStringProvider = localeStringProvider;
  }

  @Override public String getName() {
    return "|formatNum";
  }

  @Override public Set<Integer> getValidArgsSizes() {
    return VALID_ARGS_SIZES;
  }

  @Override public boolean shouldCancelAutoescape() {
    return false;
  }

  @Override public SoyValue applyForJava(SoyValue value, List<SoyValue> args) {
    ULocale uLocale = I18nUtils.parseULocale(localeStringProvider.get())
        .setKeywordValue("numbers", "local");
    if (args.size() > 1) {
      // A keyword for ULocale was passed (like 'native', for instance, to use native characters).
      uLocale = uLocale.setKeywordValue("numbers", args.get(1).stringValue());
    }

    NumberFormat numberFormat;
    String formatType = args.isEmpty() ? DEFAULT_FORMAT : args.get(0).stringValue();
    if ("decimal".equals(formatType)) {
      numberFormat = NumberFormat.getInstance(uLocale);
    } else if ("percent".equals(formatType)) {
      numberFormat = NumberFormat.getPercentInstance(uLocale);
    } else if ("currency".equals(formatType)) {
      numberFormat = NumberFormat.getCurrencyInstance(uLocale);
    } else if ("scientific".equals(formatType)) {
      numberFormat = NumberFormat.getScientificInstance(uLocale);
    } else if ("compact_short".equals(formatType)) {
      CompactDecimalFormat compactNumberFormat =
          CompactDecimalFormat.getInstance(uLocale, CompactStyle.SHORT);
      compactNumberFormat.setMaximumSignificantDigits(3);
      numberFormat = compactNumberFormat;
    } else if ("compact_long".equals(formatType)) {
      CompactDecimalFormat compactNumberFormat =
          CompactDecimalFormat.getInstance(uLocale, CompactStyle.LONG);
      compactNumberFormat.setMaximumSignificantDigits(3);
      numberFormat = compactNumberFormat;
    } else {
      throw new IllegalArgumentException(
          "First argument to formatNum must be "
              + "constant, and one of: 'decimal', 'currency', 'percent', 'scientific', "
              + "'compact_short', or 'compact_long'.");
    }

    return StringData.forValue(numberFormat.format(((NumberData) value).toFloat()));
  }

  @Override public JsExpr applyForJsSrc(JsExpr value, List<JsExpr> args) {
    String numberFormatType = parseFormat(args);

    StringBuilder expr = new StringBuilder();
    expr.append("(new goog.i18n.NumberFormat(" + JS_ARGS_TO_ENUM.get(numberFormatType) + "))");
    if ("'compact_short'".equals(numberFormatType) || "'compact_long'".equals(numberFormatType)) {
      expr.append(".setSignificantDigits(3)");
    }
    expr.append(".format(" + value.getText() + ")");

    return new JsExpr(expr.toString(), Integer.MAX_VALUE);
  }

  @Override public PyExpr applyForPySrc(PyExpr value, List<PyExpr> args) {
    String numberFormatType = parseFormat(args);

    PyFunctionExprBuilder builder =
        new PyFunctionExprBuilder(PyExprUtils.TRANSLATOR_NAME + ".format_num")
            .addArg(value)
            .addArg(new PyExpr(numberFormatType, Integer.MAX_VALUE));

    return builder.asPyStringExpr();
  }

  @Override public ImmutableSet<String> getRequiredJsLibNames() {
    return REQUIRED_JS_LIBS;
  }

  /**
   * Validates that the provided format matches a supported format, and returns the value, if not,
   * this throws an exception.
   * @param args The list of provided arguments.
   * @return String The number format type.
   */
  private static String parseFormat(List<? extends TargetExpr> args) {
    String numberFormatType = !args.isEmpty() ? args.get(0).getText() : "'" + DEFAULT_FORMAT + "'";

    if (!JS_ARGS_TO_ENUM.containsKey(numberFormatType)) {
      String validKeys = Joiner.on("', '").join(JS_ARGS_TO_ENUM.keySet());
      throw new IllegalArgumentException(
          "First argument to formatNum must be " + "constant, and one of: '" + validKeys + "'.");
    }

    return numberFormatType;
  }
}
