/*
 * 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.codegen.server.CodeGenUtils;
import com.google.gwt.core.client.GWT;
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.i18n.client.impl.LocaleInfoImpl;
import com.google.gwt.i18n.server.GwtLocaleImpl;
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.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Generator used to generate an implementation of the LocaleInfoImpl class, which is used by the
 * LocaleInfo class.
 */
@RunsLocal(requiresProperties = {"locale.queryparam", "locale", "runtime.locales", "locale.cookie"})
public class LocaleInfoGenerator extends Generator {

  /**
   * Properties file containing machine-generated locale display names, in their
   * native locales (if possible).
   */
  private static final String GENERATED_LOCALE_NATIVE_DISPLAY_NAMES = "com/google/gwt/i18n/client/impl/cldr/LocaleNativeDisplayNames-generated.properties";

  /**
   * Properties file containing hand-made corrections to the machine-generated
   * locale display names above.
   */
  private static final String MANUAL_LOCALE_NATIVE_DISPLAY_NAMES = "com/google/gwt/i18n/client/impl/cldr/LocaleNativeDisplayNames-manual.properties";

  /**
   * Properties file containing hand-made overrides of locale display names, in
   * their native locales (if possible).
   */
  private static final String OVERRIDE_LOCALE_NATIVE_DISPLAY_NAMES = "com/google/gwt/i18n/client/impl/cldr/LocaleNativeDisplayNames-override.properties";

  /**
   * Set of canonical language codes which are RTL.
   */
  private static final Set<String> RTL_LOCALES = new HashSet<String>();

  static {
    // TODO(jat): get this from CLDR data.
    RTL_LOCALES.add("ar");
    RTL_LOCALES.add("fa");
    RTL_LOCALES.add("he");
    RTL_LOCALES.add("ps");
    RTL_LOCALES.add("ur");
  }

  /**
   * 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, final GeneratorContext context,
      String typeName) throws UnableToCompleteException {
    TypeOracle typeOracle = context.getTypeOracle();
    // Get the current locale and interface type.
    PropertyOracle propertyOracle = context.getPropertyOracle();
    LocaleUtils localeUtils = LocaleUtils.getInstance(logger, propertyOracle,
        context);

    JClassType targetClass;
    try {
      targetClass = typeOracle.getType(typeName);
    } catch (NotFoundException e) {
      logger.log(TreeLogger.ERROR, "No such type " + typeName, e);
      throw new UnableToCompleteException();
    }
    assert (LocaleInfoImpl.class.getName().equals(
        targetClass.getQualifiedSourceName()));

    String packageName = targetClass.getPackage().getName();
    String superClassName = targetClass.getName().replace('.', '_') + "_shared";
    Set<GwtLocale> localeSet = localeUtils.getAllLocales();
    GwtLocaleImpl[] allLocales = localeSet.toArray(
        new GwtLocaleImpl[localeSet.size()]);
    // sort for deterministic output
    Arrays.sort(allLocales);
    PrintWriter pw = context.tryCreate(logger, packageName, superClassName);
    if (pw != null) {
      LocalizedProperties displayNames = new LocalizedProperties();
      LocalizedProperties displayNamesManual = new LocalizedProperties();
      LocalizedProperties displayNamesOverride = new LocalizedProperties();
      try {
        InputStream str = ResourceLocatorImpl.tryFindResourceAsStream(logger,
            context.getResourcesOracle(), GENERATED_LOCALE_NATIVE_DISPLAY_NAMES);
        if (str != null) {
          displayNames.load(str, "UTF-8");
        }
        str = ResourceLocatorImpl.tryFindResourceAsStream(logger, context.getResourcesOracle(),
            MANUAL_LOCALE_NATIVE_DISPLAY_NAMES);
        if (str != null) {
          displayNamesManual.load(str, "UTF-8");
        }
        str = ResourceLocatorImpl.tryFindResourceAsStream(logger, context.getResourcesOracle(),
            OVERRIDE_LOCALE_NATIVE_DISPLAY_NAMES);
        if (str != null) {
          displayNamesOverride.load(str, "UTF-8");
        }
      } catch (UnsupportedEncodingException e) {
        // UTF-8 should always be defined
        logger.log(TreeLogger.ERROR, "UTF-8 encoding is not defined", e);
        throw new UnableToCompleteException();
      } catch (IOException e) {
        logger.log(TreeLogger.ERROR, "Exception reading locale display names",
            e);
        throw new UnableToCompleteException();
      }

      ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(
          packageName, superClassName);
      factory.setSuperclass(targetClass.getQualifiedSourceName());
      factory.addImport(GWT.class.getCanonicalName());
      factory.addImport(JavaScriptObject.class.getCanonicalName());
      factory.addImport(HashMap.class.getCanonicalName());
      SourceWriter writer = factory.createSourceWriter(context, pw);
      writer.println("private static native String getLocaleNativeDisplayName(");
      writer.println("    JavaScriptObject nativeDisplayNamesNative,String localeName) /*-{");
      writer.println("  return nativeDisplayNamesNative[localeName];");
      writer.println("}-*/;");
      writer.println();
      writer.println("HashMap<String,String> nativeDisplayNamesJava;");
      writer.println("private JavaScriptObject nativeDisplayNamesNative;");
      writer.println();
      writer.println("@Override");
      writer.println("public String[] getAvailableLocaleNames() {");
      writer.println("  return new String[] {");
      boolean hasAnyRtl = false;
      for (GwtLocaleImpl possibleLocale : allLocales) {
        writer.println("    \""
            + possibleLocale.toString().replaceAll("\"", "\\\"") + "\",");
        if (RTL_LOCALES.contains(
            possibleLocale.getCanonicalForm().getLanguage())) {
          hasAnyRtl = true;
        }
      }
      writer.println("  };");
      writer.println("}");
      writer.println();
      writer.println("@Override");
      writer.println("public String getLocaleNativeDisplayName(String localeName) {");
      writer.println("  if (GWT.isScript()) {");
      writer.println("    if (nativeDisplayNamesNative == null) {");
      writer.println("      nativeDisplayNamesNative = loadNativeDisplayNamesNative();");
      writer.println("    }");
      writer.println("    return getLocaleNativeDisplayName(nativeDisplayNamesNative, localeName);");
      writer.println("  } else {");
      writer.println("    if (nativeDisplayNamesJava == null) {");
      writer.println("      nativeDisplayNamesJava = new HashMap<String, String>();");
      {
        for (GwtLocaleImpl possibleLocale : allLocales) {
          String localeName = possibleLocale.toString();
          String displayName = displayNamesOverride.getProperty(localeName);
          if (displayName == null) {
            displayName = displayNamesManual.getProperty(localeName);
          }
          if (displayName == null) {
            displayName = displayNames.getProperty(localeName);
          }
          if (displayName != null && displayName.length() != 0) {
            writer.println("      nativeDisplayNamesJava.put("
                + CodeGenUtils.asStringLiteral(localeName) + ", "
                + CodeGenUtils.asStringLiteral(displayName) + ");");
          }
        }
      }

      writer.println("    }");
      writer.println("    return nativeDisplayNamesJava.get(localeName);");
      writer.println("  }");
      writer.println("}");
      writer.println();
      writer.println("@Override");
      writer.println("public boolean hasAnyRTL() {");
      writer.println("  return " + hasAnyRtl + ";");
      writer.println("}");
      writer.println();
      writer.println("private native JavaScriptObject loadNativeDisplayNamesNative() /*-{");
      writer.println("  return {");
      {
        boolean needComma = false;
        for (GwtLocaleImpl possibleLocale : allLocales) {
          String localeName = possibleLocale.toString();
          String displayName = displayNamesOverride.getProperty(localeName);
          if (displayName == null) {
            displayName = displayNamesManual.getProperty(localeName);
          }
          if (displayName == null) {
            displayName = displayNames.getProperty(localeName);
          }
          if (displayName != null && displayName.length() != 0) {
            if (needComma) {
              writer.println(",");
            }
            writer.print("    " + CodeGenUtils.asStringLiteral(localeName) + ": "
                + CodeGenUtils.asStringLiteral(displayName));
            needComma = true;
          }
        }
        if (needComma) {
          writer.println();
        }
      }
      writer.println("  };");
      writer.println("}-*/;");
      writer.commit(logger);
    }
    GwtLocale locale = localeUtils.getCompileLocale();
    String className = targetClass.getName().replace('.', '_') + "_"
        + locale.getAsString();
    Set<GwtLocale> runtimeLocales = localeUtils.getRuntimeLocales();
    if (!runtimeLocales.isEmpty()) {
      className += "_runtimeSelection";
    }

    pw = context.tryCreate(logger, packageName, className);
    if (pw != null) {
      ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(
          packageName, className);
      factory.setSuperclass(superClassName);
      factory.addImport("com.google.gwt.core.client.GWT");
      factory.addImport("com.google.gwt.i18n.client.LocaleInfo");
      factory.addImport("com.google.gwt.i18n.client.constants.NumberConstants");
      factory.addImport("com.google.gwt.i18n.client.constants.NumberConstantsImpl");
      factory.addImport("com.google.gwt.i18n.client.DateTimeFormatInfo");
      factory.addImport("com.google.gwt.i18n.client.impl.cldr.DateTimeFormatInfoImpl");
      SourceWriter writer = factory.createSourceWriter(context, pw);
      writer.println("@Override");
      writer.println("public String getLocaleName() {");
      if (runtimeLocales.isEmpty()) {
        writer.println("  return \"" + locale + "\";");
      } else {
        writer.println("  String rtLocale = getRuntimeLocale();");
        writer.println("  return rtLocale != null ? rtLocale : \"" + locale
            + "\";");
      }
      writer.println("}");
      writer.println();
      String queryParam = localeUtils.getQueryParam();
      if (queryParam != null) {
        writer.println("@Override");
        writer.println("public String getLocaleQueryParam() {");
        writer.println("  return " + CodeGenUtils.asStringLiteral(queryParam) + ";");
        writer.println("}");
        writer.println();
      }
      String cookie = localeUtils.getCookie();
      if (cookie != null) {
        writer.println("@Override");
        writer.println("public String getLocaleCookieName() {");
        writer.println("  return " + CodeGenUtils.asStringLiteral(cookie) + ";");
        writer.println("}");
        writer.println();
      }
      writer.println("@Override");
      writer.println("public DateTimeFormatInfo getDateTimeFormatInfo() {");
      LocalizableGenerator localizableGenerator = new LocalizableGenerator();
      // Avoid warnings for trying to create the same type multiple times
      GeneratorContext subContext = new CachedGeneratorContext(context);
      generateConstantsLookup(logger, subContext, writer, localizableGenerator,
          runtimeLocales, localeUtils, locale,
          "com.google.gwt.i18n.client.impl.cldr.DateTimeFormatInfoImpl");
      writer.println("}");
      writer.println();
      writer.println("@Override");
      writer.println("public NumberConstants getNumberConstants() {");
      generateConstantsLookup(logger, subContext, writer, localizableGenerator,
          runtimeLocales, localeUtils, locale,
          "com.google.gwt.i18n.client.constants.NumberConstantsImpl");
      writer.println("}");
      writer.commit(logger);
    }
    return packageName + "." + className;
  }

  /**
   * @param logger
   * @param context
   * @param writer
   * @param localizableGenerator
   * @param runtimeLocales
   * @param localeUtils
   * @param locale
   * @throws UnableToCompleteException
   */
  private void generateConstantsLookup(TreeLogger logger,
      GeneratorContext context, SourceWriter writer,
      LocalizableGenerator localizableGenerator, Set<GwtLocale> runtimeLocales,
      LocaleUtils localeUtils, GwtLocale locale, String typeName)
      throws UnableToCompleteException {
    writer.indent();
    boolean fetchedRuntimeLocale = false;
    Map<String, Set<GwtLocale>> localeMap = new HashMap<String, Set<GwtLocale>>();
    generateOneLocale(logger, context, localizableGenerator, typeName,
        localeUtils, localeMap, locale);
    for (GwtLocale runtimeLocale : runtimeLocales) {
      generateOneLocale(logger, context, localizableGenerator, typeName,
          localeUtils, localeMap, runtimeLocale);
    }
    if (localeMap.size() > 1) {
      for (Entry<String, Set<GwtLocale>> entry : localeMap.entrySet()) {
        if (!fetchedRuntimeLocale) {
          writer.println("String runtimeLocale = getLocaleName();");
          fetchedRuntimeLocale = true;
        }
        writer.print("if (");
        boolean firstLocale = true;
        String generatedClass = entry.getKey();
        for (GwtLocale runtimeLocale : entry.getValue()) {
          if (firstLocale) {
            firstLocale = false;
          } else {
            writer.println();
            writer.print("    || ");
          }
          writer.print("\"" + runtimeLocale.toString()
              + "\".equals(runtimeLocale)");
        }
        writer.println(") {");
        writer.println("  return new " + generatedClass + "();");
        writer.println("}");
      }
      // TODO: if we get here, there was an unexpected runtime locale --
      //    should we have an assert or throw an exception?  Currently it
      //    just falls through to the default implementation.
    }
    writer.println("return GWT.create(" + typeName + ".class);");
    writer.outdent();
  }

  /**
   * @param logger
   * @param context
   * @param localizableGenerator
   * @param typeName
   * @param localeUtils
   * @param localeMap
   * @param locale
   * @throws UnableToCompleteException
   */
  private void generateOneLocale(TreeLogger logger, GeneratorContext context,
      LocalizableGenerator localizableGenerator, String typeName,
      LocaleUtils localeUtils, Map<String, Set<GwtLocale>> localeMap, GwtLocale locale)
      throws UnableToCompleteException {
    String generatedClass = localizableGenerator.generate(logger, context,
        typeName, localeUtils, locale);
    if (generatedClass == null) {
      logger.log(TreeLogger.ERROR, "Failed to generate " + typeName
          + " in locale " + locale.toString());
      // skip failed locale
      return;
    }
    Set<GwtLocale> locales = localeMap.get(generatedClass);
    if (locales == null) {
      locales = new HashSet<GwtLocale>();
      localeMap.put(generatedClass, locales);
    }
    locales.add(locale);
  }
}
