/*
 * Copyright 2008 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.gwt.i18n.rebind;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.Generator.RunsLocal;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.impl.ResourceLocatorImpl;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.resource.ResourceOracle;
import com.google.gwt.i18n.client.CurrencyList;
import com.google.gwt.i18n.client.impl.CurrencyDataImpl;
import com.google.gwt.i18n.shared.GwtLocale;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

import org.apache.tapestry.util.text.LocalizedProperties;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;

/**
 * Generator used to generate a localized version of CurrencyList, which contains the list of
 * currencies (with names, symbols, and other information) localized to the current locale.
 */
@RunsLocal(requiresProperties = {"locale.queryparam", "locale", "runtime.locales", "locale.cookie"})
public class CurrencyListGenerator extends Generator {

  /**
   * Immutable collection of data about a currency in a locale, built from the
   * CurrencyData and CurrencyExtra properties files.
   */
  private static class CurrencyInfo {

    private static final Pattern SPLIT_VERTICALBAR = Pattern.compile("\\|");

    private final String code;

    private final String displayName;

    private final int flags;

    private final boolean obsolete;

    private final String portableSymbol;

    private String simpleSymbol;

    private final String symbol;

    /**
     * Create an instance.
     *
     * currencyData format:
     *
     * <pre>
     *       display name|symbol|decimal digits|not-used-flag
     * </pre>
     *
     * <ul>
     * <li>If a symbol is not supplied, the currency code will be used
     * <li>If # of decimal digits is omitted, 2 is used
     * <li>If a currency is not generally used, not-used-flag=1
     * <li>Trailing empty fields can be omitted
     * <li>If null, use currencyCode as the display name
     * </ul>
     *
     * extraData format:
     *
     * <pre>
     *       portable symbol|flags|currency symbol override
     *     flags are space separated list of:
     *       At most one of the following:
     *         SymPrefix     The currency symbol goes before the number,
     *                       regardless of the normal position for this locale.
     *         SymSuffix     The currency symbol goes after the number,
     *                       regardless of the normal position for this locale.
     *
     *       At most one of the following:
     *         ForceSpace    Always add a space between the currency symbol
     *                       and the number.
     *         ForceNoSpace  Never add a space between the currency symbol
     *                       and the number.
     * </pre>
     *
     * @param currencyCode ISO4217 currency code
     * @param currencyData entry from a CurrencyData properties file
     * @param extraData entry from a CurrencyExtra properties file
     * @throws NumberFormatException
     */
    public CurrencyInfo(String currencyCode, String currencyData,
        String extraData) throws NumberFormatException {
      code = currencyCode;
      if (currencyData == null) {
        currencyData = currencyCode;
      }
      String[] currencySplit = SPLIT_VERTICALBAR.split(currencyData);
      String currencyDisplay = currencySplit[0];
      String currencySymbol = null;
      String simpleCurrencySymbol = null;
      if (currencySplit.length > 1 && currencySplit[1].length() > 0) {
        currencySymbol = currencySplit[1];
      }
      int currencyFractionDigits = 2;
      if (currencySplit.length > 2 && currencySplit[2].length() > 0) {
        currencyFractionDigits = Integer.valueOf(currencySplit[2]);
      }
      boolean currencyObsolete = false;
      if (currencySplit.length > 3 && currencySplit[3].length() > 0) {
        currencyObsolete = Integer.valueOf(currencySplit[3]) != 0;
      }
      int currencyFlags = currencyFractionDigits;
      if (currencyObsolete) {
        currencyFlags |= CurrencyDataImpl.DEPRECATED_FLAG;
      }
      String currencyPortableSymbol = "";
      if (extraData != null) {
        // CurrencyExtra contains up to 3 fields separated by |
        // 0 - portable currency symbol
        // 1 - space-separated flags regarding currency symbol
        // positioning/spacing
        // 2 - override of CLDR-derived currency symbol
        // 3 - simple currency symbol
        String[] extraSplit = SPLIT_VERTICALBAR.split(extraData);
        currencyPortableSymbol = extraSplit[0];
        if (extraSplit.length > 1) {
          if (extraSplit[1].contains("SymPrefix")) {
            currencyFlags |= CurrencyDataImpl.POS_FIXED_FLAG;
          } else if (extraSplit[1].contains("SymSuffix")) {
            currencyFlags |= CurrencyDataImpl.POS_FIXED_FLAG
                | CurrencyDataImpl.POS_SUFFIX_FLAG;
          }
          if (extraSplit[1].contains("ForceSpace")) {
            currencyFlags |= CurrencyDataImpl.SPACING_FIXED_FLAG
                | CurrencyDataImpl.SPACE_FORCED_FLAG;
          } else if (extraSplit[1].contains("ForceNoSpace")) {
            currencyFlags |= CurrencyDataImpl.SPACING_FIXED_FLAG;
          }
        }
        // If a non-empty override is supplied, use it for the currency
        // symbol.
        if (extraSplit.length > 2 && extraSplit[2].length() > 0) {
          currencySymbol = extraSplit[2];
        }
        // If a non-empty simple symbol is supplied, use it for the currency
        // symbol.
        if (extraSplit.length > 3 && extraSplit[3].length() > 0) {
          simpleCurrencySymbol = extraSplit[3];
        }
        // If we don't have a currency symbol yet, use the portable symbol if
        // supplied.
        if (currencySymbol == null && currencyPortableSymbol.length() > 0) {
          currencySymbol = currencyPortableSymbol;
        }
      }
      // If all else fails, use the currency code as the symbol.
      if (currencySymbol == null) {
        currencySymbol = currencyCode;
      }
      if (currencyPortableSymbol.length() == 0) {
        currencyPortableSymbol = currencySymbol;
      }
      if (simpleCurrencySymbol == null) {
        simpleCurrencySymbol = currencySymbol;
      }
      displayName = currencyDisplay;
      symbol = currencySymbol;
      flags = currencyFlags;
      portableSymbol = currencyPortableSymbol;
      simpleSymbol = simpleCurrencySymbol;
      obsolete = currencyObsolete;
    }

    public String getDisplayName() {
      return displayName;
    }

    public String getJava() {
      StringBuilder buf = new StringBuilder();
      buf.append("new CurrencyDataImpl(\"").append(quote(code)).append("\", \"");
      buf.append(quote(symbol)).append("\", ").append(flags);
      buf.append(", \"").append(quote(portableSymbol)).append('\"');
      buf.append(", \"").append(quote(simpleSymbol)).append('\"');
      return buf.append(')').toString();
    }

    public String getJson() {
      StringBuilder buf = new StringBuilder();
      buf.append("[ \"").append(quote(code)).append("\", \"");
      buf.append(quote(symbol)).append("\", ").append(flags);
      buf.append(", \"").append(quote(portableSymbol)).append('\"');
      buf.append(", \"").append(quote(simpleSymbol)).append('\"');
      return buf.append(']').toString();
    }
  }

  private static final String CURRENCY_DATA = CurrencyDataImpl.class.getCanonicalName();

  /**
   * Prefix for properties files containing CLDR-derived currency data for each
   * locale.
   */
  private static final String CURRENCY_DATA_PREFIX =
      "com/google/gwt/i18n/client/impl/cldr/CurrencyData";

  /**
   * Prefix for properties files containing additional flags about currencies
   * each locale, which are not present in CLDR.
   */
  private static final String CURRENCY_EXTRA_PREFIX =
      "com/google/gwt/i18n/client/constants/CurrencyExtra";

  private static final String CURRENCY_LIST = CurrencyList.class.getCanonicalName();

  private static final String HASHMAP = HashMap.class.getCanonicalName();

  private static final String JAVASCRIPTOBJECT = JavaScriptObject.class.getCanonicalName();

  /**
   * Prefix for properties files containing number formatting constants for each
   * locale. We use this only to get the default currency for our current
   * locale.
   */
  private static final String NUMBER_CONSTANTS_PREFIX =
      "com/google/gwt/i18n/client/constants/NumberConstantsImpl";

  /**
   * Backslash-escape any double quotes in the supplied string.
   *
   * @param str string to quote
   * @return string with double quotes backslash-escaped.
   */
  private static String quote(String str) {
    return str.replace("\"", "\\\"");
  }

  /**
   * Generate an implementation for the given type.
   *
   * @param logger error logger
   * @param context generator context
   * @param typeName target type name
   * @return generated class name
   * @throws UnableToCompleteException
   */
  @Override
  public final String generate(TreeLogger logger, GeneratorContext context,
      String typeName) throws UnableToCompleteException {
    assert CURRENCY_LIST.equals(typeName);
    TypeOracle typeOracle = context.getTypeOracle();

    PropertyOracle propertyOracle = context.getPropertyOracle();
    LocaleUtils localeUtils = LocaleUtils.getInstance(logger, propertyOracle,
        context);
    GwtLocale locale = localeUtils.getCompileLocale();
    Set<GwtLocale> runtimeLocales = localeUtils.getRuntimeLocales();

    JClassType targetClass;
    try {
      targetClass = typeOracle.getType(typeName);
    } catch (NotFoundException e) {
      logger.log(TreeLogger.ERROR, "No such type", e);
      throw new UnableToCompleteException();
    }
    if (runtimeLocales.isEmpty()) {
      return generateLocaleTree(logger, context, targetClass, locale);
    }
    CachedGeneratorContext cachedContext = new CachedGeneratorContext(context);
    return generateRuntimeSelection(logger, cachedContext, targetClass, locale,
        runtimeLocales);
  }

  /**
   * Generate an implementation class for the requested locale, including all
   * parent classes along the inheritance chain. The data will be kept at the
   * location in the inheritance chain where it was defined in properties files.
   *
   * @param logger
   * @param context
   * @param targetClass
   * @param locale
   * @return generated class name for the requested locale
   */
  private String generateLocaleTree(TreeLogger logger, GeneratorContext context,
      JClassType targetClass, GwtLocale locale) {
    String superClassName = CURRENCY_LIST;
    List<GwtLocale> searchList = locale.getCompleteSearchList();

    /**
     * Map of currency code -> CurrencyInfo for that code.
     */
    Map<String, CurrencyInfo> allCurrencyData = new HashMap<String, CurrencyInfo>();

    LocalizedProperties currencyExtra = null;
    /*
     * The searchList is guaranteed to be ordered such that subclasses always
     * precede superclasses. Therefore, we iterate backwards to ensure that
     * superclasses are always generated first.
     */
    String lastDefaultCurrencyCode = null;
    for (int i = searchList.size(); i-- > 0;) {
      GwtLocale search = searchList.get(i);
      LocalizedProperties newExtra =
          getProperties(logger, CURRENCY_EXTRA_PREFIX, search, context.getResourcesOracle());
      if (newExtra != null) {
        currencyExtra = newExtra;
      }
      Map<String, String> currencyData =
          getCurrencyData(logger, search, context.getResourcesOracle());
      Set<String> keySet = currencyData.keySet();
      String[] currencies = new String[keySet.size()];
      keySet.toArray(currencies);
      Arrays.sort(currencies);

      // Go ahead and populate the data map.
      for (String currencyCode : currencies) {
        String extraData = currencyExtra == null ? null
            : currencyExtra.getProperty(currencyCode);
        allCurrencyData.put(currencyCode, new CurrencyInfo(currencyCode,
            currencyData.get(currencyCode), extraData));
      }

      String defCurrencyCode = getDefaultCurrency(logger, search, context.getResourcesOracle());
      // If this locale specifies a particular locale, or the one that is
      // inherited has been changed in this locale, re-specify the default
      // currency so the method will be generated.
      if (defCurrencyCode == null && keySet.contains(lastDefaultCurrencyCode)) {
        defCurrencyCode = lastDefaultCurrencyCode;
      }
      if (!currencyData.isEmpty() || defCurrencyCode != null) {
        String newClass =
            generateOneLocale(logger, context, targetClass, search,
                superClassName, currencies, allCurrencyData, defCurrencyCode);
        superClassName = newClass;
        lastDefaultCurrencyCode = defCurrencyCode;
      }
    }
    return superClassName;
  }

  /**
   * Generate the implementation for a single locale, overriding from its parent
   * only data that has changed in this locale.
   *
   * @param logger
   * @param context
   * @param targetClass
   * @param locale
   * @param superClassName
   * @param currencies the set of currencies defined in this locale
   * @param allCurrencyData map of currency code -> unparsed CurrencyInfo for
   *        that code
   * @param defCurrencyCode default currency code for this locale
   * @return fully-qualified class name generated
   */
  private String generateOneLocale(TreeLogger logger, GeneratorContext context,
      JClassType targetClass, GwtLocale locale, String superClassName,
      String[] currencies, Map<String, CurrencyInfo> allCurrencyData,
      String defCurrencyCode) {
    String packageName = targetClass.getPackage().getName();
    String className = targetClass.getName().replace('.', '_') + "_"
        + locale.getAsString();
    PrintWriter pw = context.tryCreate(logger, packageName, className);
    if (pw != null) {
      ClassSourceFileComposerFactory factory =
          new ClassSourceFileComposerFactory(packageName, className);
      factory.setSuperclass(superClassName);
      factory.addImport(CURRENCY_DATA);
      factory.addImport(JAVASCRIPTOBJECT);
      factory.addImport(HASHMAP);
      SourceWriter writer = factory.createSourceWriter(context, pw);
      if (defCurrencyCode != null) {
        CurrencyInfo currencyInfo = allCurrencyData.get(defCurrencyCode);
        if (currencyInfo == null) {
          // Synthesize a null info if the specified default wasn't found.
          currencyInfo = new CurrencyInfo(defCurrencyCode, null, null);
          allCurrencyData.put(defCurrencyCode, currencyInfo);
        }
        writer.println();
        writer.println("@Override");
        writer.println("protected CurrencyData getDefaultJava() {");
        writer.println("  return " + currencyInfo.getJava() + ";");
        writer.println("}");
        writer.println();
        writer.println("@Override");
        writer.println("protected native CurrencyData getDefaultNative() /*-{");
        writer.println("  return " + currencyInfo.getJson() + ";");
        writer.println("}-*/;");
      }
      if (currencies.length > 0) {
        writeCurrencyMethodJava(writer, currencies, allCurrencyData);
        writeCurrencyMethodNative(writer, currencies, allCurrencyData);
        writeNamesMethodJava(writer, currencies, allCurrencyData);
        writeNamesMethodNative(writer, currencies, allCurrencyData);
      }
      writer.commit(logger);
    }
    return packageName + "." + className;
  }

  /**
   * Generate a class which can select between alternate implementations at
   * runtime based on the runtime locale.
   *
   * @param logger TreeLogger instance for log messages
   * @param context GeneratorContext for generating source files
   * @param targetClass class to generate
   * @param compileLocale the compile-time locale we are generating for
   * @param locales set of all locales to generate
   * @return fully-qualified class name that was generated
   */
  private String generateRuntimeSelection(TreeLogger logger,
      GeneratorContext context, JClassType targetClass, GwtLocale compileLocale,
      Set<GwtLocale> locales) {
    String packageName = targetClass.getPackage().getName();
    String className =
        targetClass.getName().replace('.', '_') + "_"
            + compileLocale.getAsString() + "_runtimeSelection";
    PrintWriter pw = context.tryCreate(logger, packageName, className);
    if (pw != null) {
      ClassSourceFileComposerFactory factory =
          new ClassSourceFileComposerFactory(packageName, className);
      factory.setSuperclass(targetClass.getQualifiedSourceName());
      factory.addImport(CURRENCY_DATA);
      factory.addImport(JAVASCRIPTOBJECT);
      factory.addImport(HASHMAP);
      factory.addImport("com.google.gwt.i18n.client.LocaleInfo");
      SourceWriter writer = factory.createSourceWriter(context, pw);
      writer.println("private CurrencyList instance;");
      writer.println();
      writer.println("@Override");
      writer.println("protected CurrencyData getDefaultJava() {");
      writer.println("  ensureInstance();");
      writer.println("  return instance.getDefaultJava();");
      writer.println("}");
      writer.println();
      writer.println("@Override");
      writer.println("protected CurrencyData getDefaultNative() {");
      writer.println("  ensureInstance();");
      writer.println("  return instance.getDefaultNative();");
      writer.println("}");
      writer.println();
      writer.println("@Override");
      writer.println("protected HashMap<String, CurrencyData> loadCurrencyMapJava() {");
      writer.println("  ensureInstance();");
      writer.println("  return instance.loadCurrencyMapJava();");
      writer.println("}");
      writer.println();
      writer.println("@Override");
      writer.println("protected JavaScriptObject loadCurrencyMapNative() {");
      writer.println("  ensureInstance();");
      writer.println("  return instance.loadCurrencyMapNative();");
      writer.println("}");
      writer.println();
      writer.println("@Override");
      writer.println("protected HashMap<String, String> loadNamesMapJava() {");
      writer.println("  ensureInstance();");
      writer.println("  return instance.loadNamesMapJava();");
      writer.println("}");
      writer.println();
      writer.println("@Override");
      writer.println("protected JavaScriptObject loadNamesMapNative() {");
      writer.println("  ensureInstance();");
      writer.println("  return instance.loadNamesMapNative();");
      writer.println("}");
      writer.println();
      writer.println("private void ensureInstance() {");
      writer.indent();
      writer.println("if (instance != null) {");
      writer.println("  return;");
      writer.println("}");
      boolean fetchedLocale = false;
      Map<String, Set<GwtLocale>> localeMap = new TreeMap<String,
          Set<GwtLocale>>();
      String compileLocaleClass =
          processChildLocale(logger, context, targetClass, localeMap,
              compileLocale);
      if (compileLocaleClass == null) {
        // already gave warning, just use default implementation
        return null;
      }
      for (GwtLocale runtimeLocale : locales) {
        processChildLocale(logger, context, targetClass, localeMap,
            runtimeLocale);
      }
      for (Entry<String, Set<GwtLocale>> entry : localeMap.entrySet()) {
        if (!fetchedLocale) {
          writer.println("String runtimeLocale = LocaleInfo.getCurrentLocale().getLocaleName();");
          fetchedLocale = true;
        }
        boolean firstLocale = true;
        String generatedClass = entry.getKey();
        if (compileLocaleClass.equals(generatedClass)) {
          // The catch-all will handle this
          continue;
        }
        writer.print("if (");
        for (GwtLocale locale : entry.getValue()) {
          if (firstLocale) {
            firstLocale = false;
          } else {
            writer.println();
            writer.print("    || ");
          }
          writer.print("\"" + locale.toString() + "\".equals(runtimeLocale)");
        }
        writer.println(") {");
        writer.println("  instance = new " + generatedClass + "();");
        writer.println("  return;");
        writer.println("}");
      }
      writer.println("instance = new " + compileLocaleClass + "();");
      writer.outdent();
      writer.println("}");
      writer.commit(logger);
    }
    return packageName + "." + className;
  }

  /**
   * Return a map of currency data for the requested locale, or null if there is
   * not one (not that inheritance is not handled here).
   * <p/>
   * The keys are ISO4217 currency codes. The format of the map values is:
   *
   * <pre>
   * display name|symbol|decimal digits|not-used-flag
   * </pre>
   *
   * If a symbol is not supplied, the currency code will be used If # of decimal
   * digits is omitted, 2 is used If a currency is not generally used,
   * not-used-flag=1 Trailing empty fields can be omitted
   */
  @SuppressWarnings("unchecked")
  private Map<String, String> getCurrencyData(TreeLogger logger, GwtLocale locale,
      ResourceOracle resourceOracle) {
    LocalizedProperties currencyData =
        getProperties(logger, CURRENCY_DATA_PREFIX, locale, resourceOracle);
    if (currencyData == null) {
      return Collections.emptyMap();
    }
    return currencyData.getPropertyMap();
  }

  /**
   * Returns the default currency code for the requested locale.
   */
  private String getDefaultCurrency(TreeLogger logger, GwtLocale locale,
      ResourceOracle resourceOracle) {
    String defCurrencyCode = null;
    LocalizedProperties numberConstants =
        getProperties(logger, NUMBER_CONSTANTS_PREFIX, locale, resourceOracle);
    if (numberConstants != null) {
      defCurrencyCode = numberConstants.getProperty("defCurrencyCode");
    }
    if (defCurrencyCode == null && locale.isDefault()) {
      defCurrencyCode = "USD";
    }
    return defCurrencyCode;
  }

  /**
   * Load a properties file for a given locale. Note that locale inheritance is
   * the responsibility of the caller.
   * @param logger with which to log
   * @param prefix classpath prefix of properties file
   * @param locale locale to load
   * @param resourceOracle with which to locate resources
   *
   * @return LocalizedProperties instance containing properties file or null if
   *         not found.
   */
  private LocalizedProperties getProperties(TreeLogger logger, String prefix,
      GwtLocale locale, ResourceOracle resourceOracle) {
    String propFile = prefix;
    if (!locale.isDefault()) {
      propFile += "_" + locale.getAsString();
    }
    propFile += ".properties";
    InputStream str = null;
    LocalizedProperties props = new LocalizedProperties();
    try {
      str = ResourceLocatorImpl.tryFindResourceAsStream(logger, resourceOracle, propFile);
      if (str != null) {
        props.load(str, "UTF-8");
        return props;
      }
    } catch (UnsupportedEncodingException e) {
      // UTF-8 should always be defined
      return null;
    } catch (IOException e) {
      return null;
    } finally {
      if (str != null) {
        try {
          str.close();
        } catch (IOException e) {
        }
      }
    }
    return null;
  }

  /**
   * Generate an implementation for a runtime locale, to be referenced from the
   * generated runtime selection code.
   *
   * @param logger
   * @param context
   * @param targetClass
   * @param localeMap
   * @param locale
   * @return class name of the generated class, or null if failed
   */
  private String processChildLocale(TreeLogger logger, GeneratorContext context,
      JClassType targetClass, Map<String, Set<GwtLocale>> localeMap,
      GwtLocale locale) {
    String generatedClass = generateLocaleTree(logger, context, targetClass,
        locale);
    if (generatedClass == null) {
      logger.log(TreeLogger.ERROR, "Failed to generate "
          + targetClass.getQualifiedSourceName() + " in locale "
          + locale.toString());
      // skip failed locale
      return null;
    }
    Set<GwtLocale> locales = localeMap.get(generatedClass);
    if (locales == null) {
      locales = new HashSet<GwtLocale>();
      localeMap.put(generatedClass, locales);
    }
    locales.add(locale);
    return generatedClass;
  }

  /**
   * Writes a loadCurrencyMapJava method for the current locale, based on its
   * currency data and its superclass (if any). As currencies are included in
   * this method, their names are added to {@code nameMap} for later use.
   *
   * If no new currency data is added for this locale over its superclass, the
   * method is omitted entirely.
   *
   * @param writer SourceWriter instance to use for writing the class
   * @param currencies array of valid currency names in the order they should be
   *          listed
   * @param allCurrencyData map of currency codes to currency data for the
   *          current locale, including all inherited currencies data
   */
  private void writeCurrencyMethodJava(SourceWriter writer,
      String[] currencies, Map<String, CurrencyInfo> allCurrencyData) {
    boolean needHeader = true;
    for (String currencyCode : currencies) {
      // TODO(jat): only emit new data where it differs from superclass!
      CurrencyInfo currencyInfo = allCurrencyData.get(currencyCode);
      if (needHeader) {
        needHeader = false;
        writer.println();
        writer.println("@Override");
        writer.println("protected HashMap<String, CurrencyData> loadCurrencyMapJava() {");
        writer.indent();
        writer.println("HashMap<String, CurrencyData> result = super.loadCurrencyMapJava();");
      }
      writer.println("// " + currencyInfo.getDisplayName());
      writer.println("result.put(\"" + quote(currencyCode) + "\", "
          + currencyInfo.getJava() + ");");
    }
    if (!needHeader) {
      writer.println("return result;");
      writer.outdent();
      writer.println("}");
    }
  }

  /**
   * Writes a loadCurrencyMapNative method for the current locale, based on its
   * currency data and its superclass (if any). As currencies are included in
   * this method, their names are added to {@code nameMap} for later use.
   *
   * If no new currency data is added for this locale over its superclass, the
   * method is omitted entirely.
   *
   * @param writer SourceWriter instance to use for writing the class
   * @param currencies array of valid currency names in the order they should be
   *          listed
   * @param allCurrencyData map of currency codes to currency data for the
   *          current locale, including all inherited currencies data
   */
  private void writeCurrencyMethodNative(SourceWriter writer,
      String[] currencies, Map<String, CurrencyInfo> allCurrencyData) {
    boolean needHeader = true;
    for (String currencyCode : currencies) {
      // TODO(jat): only emit new data where it differs from superclass!
      CurrencyInfo currencyInfo = allCurrencyData.get(currencyCode);
      if (needHeader) {
        needHeader = false;
        writer.println();
        writer.println("@Override");
        writer.println("protected JavaScriptObject loadCurrencyMapNative() {");
        writer.indent();
        writer.println("return overrideMap(super.loadCurrencyMapNative(), loadMyCurrencyMapOverridesNative());");
        writer.outdent();
        writer.println("}");
        writer.println();
        writer.println("private native JavaScriptObject loadMyCurrencyMapOverridesNative() /*-{");
        writer.indent();
        writer.println("return {");
        writer.indent();
      }
      writer.println("// " + currencyInfo.getDisplayName());
      writer.println("\"" + quote(currencyCode) + "\": "
          + currencyInfo.getJson() + ",");
    }
    if (!needHeader) {
      writer.outdent();
      writer.println("};");
      writer.outdent();
      writer.println("}-*/;");
    }
  }

  /**
   * Writes a loadNamesMapJava method for the current locale, based on its the
   * supplied names map and its superclass (if any).
   *
   * If no new names are added for this locale over its superclass, the method
   * is omitted entirely.
   *
   * @param writer SourceWriter instance to use for writing the class
   * @param currencies array of valid currency names in the order they should be
   *          listed
   * @param allCurrencyData map of currency codes to currency data for the
   *          current locale, including all inherited currencies data
   */
  private void writeNamesMethodJava(SourceWriter writer, String[] currencies,
      Map<String, CurrencyInfo> allCurrencyData) {
    boolean needHeader = true;
    for (String currencyCode : currencies) {
      // TODO(jat): only emit new data where it differs from superclass!
      CurrencyInfo currencyInfo = allCurrencyData.get(currencyCode);
      String displayName = currencyInfo.getDisplayName();
      if (displayName != null && !currencyCode.equals(displayName)) {
        if (needHeader) {
          needHeader = false;
          writer.println();
          writer.println("@Override");
          writer.println("protected HashMap<String, String> loadNamesMapJava() {");
          writer.indent();
          writer.println("HashMap<String, String> result = super.loadNamesMapJava();");
        }
        writer.println("result.put(\"" + quote(currencyCode) + "\", \""
            + quote(displayName) + "\");");
      }
    }
    if (!needHeader) {
      writer.println("return result;");
      writer.outdent();
      writer.println("}");
    }
  }

  /**
   * Writes a loadNamesMapNative method for the current locale, based on its the
   * supplied names map and its superclass (if any).
   *
   * If no new names are added for this locale over its superclass, the method
   * is omitted entirely.
   *
   * @param writer SourceWriter instance to use for writing the class
   * @param currencies array of valid currency names in the order they should be
   *          listed
   * @param allCurrencyData map of currency codes to currency data for the
   *          current locale, including all inherited currencies data
   */
  private void writeNamesMethodNative(SourceWriter writer, String[] currencies,
      Map<String, CurrencyInfo> allCurrencyData) {
    boolean needHeader = true;
    for (String currencyCode : currencies) {
      // TODO(jat): only emit new data where it differs from superclass!
      CurrencyInfo currencyInfo = allCurrencyData.get(currencyCode);
      String displayName = currencyInfo.getDisplayName();
      if (displayName != null && !currencyCode.equals(displayName)) {
        if (needHeader) {
          needHeader = false;
          writer.println();
          writer.println("@Override");
          writer.println("protected JavaScriptObject loadNamesMapNative() {");
          writer.indent();
          writer.println("return overrideMap(super.loadNamesMapNative(), loadMyNamesMapOverridesNative());");
          writer.outdent();
          writer.println("}");
          writer.println();
          writer.println("private native JavaScriptObject loadMyNamesMapOverridesNative() /*-{");
          writer.indent();
          writer.println("return {");
          writer.indent();
        }
        writer.println("\"" + quote(currencyCode) + "\": \""
            + quote(displayName) + "\",");
      }
    }
    if (!needHeader) {
      writer.outdent();
      writer.println("};");
      writer.outdent();
      writer.println("}-*/;");
    }
  }
}
