/*
 * Copyright 2010 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.tools.datetimefmtcreator;

import com.google.gwt.i18n.client.constants.DateTimeConstantsImpl;
import com.google.gwt.i18n.rebind.LocaleUtils;
import com.google.gwt.i18n.shared.GwtLocale;
import com.google.gwt.i18n.shared.GwtLocaleFactory;

import com.ibm.icu.text.DateTimePatternGenerator;
import com.ibm.icu.util.ULocale;

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

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Map.Entry;
import java.util.regex.Pattern;

/**
 * Generate implementations of DateTimeFormatInfoImpl for all the supported
 * locales.
 */
public class DateTimeFormatCreator {

  private static class DtfiGenerator {

    private static void buildPatterns(GwtLocale locale, TreeMap<Key, String[]> properties) {
      ULocale ulocale = new ULocale(ULocale.canonicalize(locale.getAsString()));
      DateTimePatternGenerator dtpg = DateTimePatternGenerator.getInstance(ulocale);
      for (Map.Entry<String, String> entry : patterns.entrySet()) {
        properties.put(new Key(locale, "format" + entry.getKey()), new String[] {dtpg
            .getBestPattern(entry.getValue())});
      }
    }

    private static GwtLocale findEarliestAncestor(GwtLocale locale, Set<GwtLocale> set) {
      if (set == null) {
        return null;
      }
      for (GwtLocale search : locale.getInheritanceChain()) {
        if (set.contains(search)) {
          return search;
        }
      }
      return null;
    }

    private static String quote(String value) {
      return value.replaceAll("\"", "\\\\\"");
    }

    private static String[] split(String target) {
      // We add an artificial end character to avoid the odd split() behavior
      // that drops the last item if it is only whitespace.
      target = target + "~";

      // Do not split on escaped commas.
      String[] args = target.split("(?<![\\\\]),");

      // Now remove the artificial ending we added above.
      // We have to do it before we escape and trim because otherwise
      // the artificial trailing '~' would prevent the last item from being
      // properly trimmed.
      if (args.length > 0) {
        int last = args.length - 1;
        args[last] = args[last].substring(0, args[last].length() - 1);
      }

      for (int i = 0; i < args.length; i++) {
        args[i] = args[i].replaceAll("\\\\,", ",").trim();
      }
      return args;
    }

    private File propDir;

    private File src;

    public DtfiGenerator(File src) {
      this.src = src;
      String packageName = DateTimeConstantsImpl.class.getPackage().getName();
      propDir = new File(src, packageName.replaceAll("\\.", "/"));
      if (!propDir.exists()) {
        System.err.println("Can't find directory for " + packageName);
        return;
      }
    }

    public void generate() throws FileNotFoundException, IOException {
      final Pattern dtcProps = Pattern.compile("DateTimeConstantsImpl(.*)\\.properties");
      String[] propFiles = propDir.list(new FilenameFilter() {
        public boolean accept(File dir, String name) {
          return dtcProps.matcher(name).matches();
        }
      });
      TreeMap<Key, String[]> properties = new TreeMap<Key, String[]>();
      GwtLocaleFactory factory = LocaleUtils.getLocaleFactory();
      collectPropertyData(propFiles, properties, factory);
      Map<GwtLocale, Set<GwtLocale>> parents = removeInheritedValues(properties);
      generateSources(properties, parents);
    }

    private void addLocaleParent(Map<GwtLocale, Set<GwtLocale>> parents, GwtLocale keyLocale,
        GwtLocale parentLocale) {
      Set<GwtLocale> parentSet = parents.get(keyLocale);
      if (parentSet == null) {
        parentSet = new HashSet<GwtLocale>();
        parents.put(keyLocale, parentSet);
      }
      parentSet.add(parentLocale);
    }

    @SuppressWarnings("unchecked")
    private void collectPropertyData(String[] propFiles, TreeMap<Key, String[]> properties,
        GwtLocaleFactory factory) throws FileNotFoundException, IOException {
      for (String propFile : propFiles) {
        if (!propFile.startsWith("DateTimeConstantsImpl") || !propFile.endsWith(".properties")) {
          continue;
        }
        int len = propFile.length();
        String suffix = propFile.substring(21, len - 11);
        if (suffix.startsWith("_")) {
          suffix = suffix.substring(1);
        }
        GwtLocale locale = factory.fromString(suffix).getCanonicalForm();
        File f = new File(propDir, propFile);
        FileInputStream str = null;
        try {
          str = new FileInputStream(f);
          LocalizedProperties props = new LocalizedProperties();
          props.load(str);
          Map<String, String> map = props.getPropertyMap();
          for (Map.Entry<String, String> entry : map.entrySet()) {
            String[] value = split(entry.getValue());
            if ("dateFormats".equals(entry.getKey()) || "timeFormats".equals(entry.getKey())
                || "weekendRange".equals(entry.getKey())) {
              // split these out into separate fields
              for (int i = 0; i < value.length; ++i) {
                Key key = new Key(locale, entry.getKey() + i);
                properties.put(key, new String[] {value[i]});
              }
            } else {
              Key key = new Key(locale, entry.getKey());
              properties.put(key, value);
            }
          }
          buildPatterns(locale, properties);
        } finally {
          if (str != null) {
            str.close();
          }
        }
      }
    }

    private PrintWriter createClassSource(String packageName, String className)
        throws FileNotFoundException {
      String path = packageName.replace('.', '/') + "/" + className + ".java";
      File f = new File(src, path);
      FileOutputStream ostr = new FileOutputStream(f);
      PrintWriter out = new PrintWriter(ostr);
      out.println("/*");
      out.println(" * Copyright 2010 Google Inc.");
      out.println(" * ");
      out.println(" * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not");
      out.println(" * use this file except in compliance with the License. You may obtain a copy of");
      out.println(" * the License at");
      out.println(" * ");
      out.println(" * http://www.apache.org/licenses/LICENSE-2.0");
      out.println(" *");
      out.println(" * Unless required by applicable law or agreed to in writing, software");
      out.println(" * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT");
      out.println(" * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the");
      out.println(" * License for the specific language governing permissions and limitations under");
      out.println(" * the License.");
      out.println(" */");
      out.println("package " + packageName + ";");
      out.println();
      out.println("// DO NOT EDIT - GENERATED FROM CLDR DATA");
      out.println();
      return out;
    }

    private void generateAlias(GwtLocale locale, GwtLocale parent) throws IOException {
      System.out.println("Generating alias " + locale);
      String suffix;
      if (parent.isDefault()) {
        suffix = "";
      } else {
        suffix = "_" + parent.getAsString();
      }
      String packageName = "com.google.gwt.i18n.client.impl.cldr";
      String className = "DateTimeFormatInfoImpl_" + locale.getAsString();
      PrintWriter out = null;
      try {
        out = createClassSource(packageName, className);
        out.println("/**");
        out.println(" * Locale \"" + locale + "\" is an alias for \"" + parent + "\".");
        out.println(" */");
        out.println("public class " + className + " extends DateTimeFormatInfoImpl" + suffix + " {");
        out.println("}");
      } finally {
        if (out != null) {
          out.close();
        }
      }
    }

    private void generateLocale(GwtLocale locale, GwtLocale parent, Map<String, String[]> values)
        throws IOException {
      System.out.println("Generating locale " + locale);
      boolean addOverrides = true;
      PrintWriter out = null;
      try {
        if (locale.isDefault()) {
          String packageName = "com.google.gwt.i18n.client";
          String className = "DefaultDateTimeFormatInfo";
          out = createClassSource(packageName, className);
          out.println("/**");
          out.println(" * Default implementation of DateTimeFormatInfo interface, using values from");
          out.println(" * the CLDR root locale.");
          out.println(" * <p>");
          out.println(" * Users who need to create their own DateTimeFormatInfo implementation are");
          out.println(" * encouraged to extend this class so their implementation won't break when");
          out.println(" * new methods are added.");
          out.println(" */");
          out.println("public class DefaultDateTimeFormatInfo implements DateTimeFormatInfo {");
          addOverrides = false;
        } else {
          String suffix;
          if (parent.isDefault()) {
            suffix = "";
          } else {
            suffix = "_" + parent.getAsString();
          }
          String packageName = "com.google.gwt.i18n.client.impl.cldr";
          String className = "DateTimeFormatInfoImpl_" + locale.getAsString();
          out = createClassSource(packageName, className);
          out.println("/**");
          out.println(" * Implementation of DateTimeFormatInfo for locale \"" + locale + "\".");
          out.println(" */");
          out.println("public class " + className + " extends DateTimeFormatInfoImpl" + suffix
              + " {");
        }
        Set<String> keySet = values.keySet();
        String[] keys = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keys, new Comparator<String>() {
          public int compare(String a, String b) {
            String mappedA = a;
            String mappedB = b;
            FieldMapping field = fieldMap.get(a);
            if (field != null) {
              mappedA = field.methodName;
            }
            field = fieldMap.get(b);
            if (field != null) {
              mappedB = field.methodName;
            }
            return mappedA.compareTo(mappedB);
          }
        });
        for (String key : keys) {
          String[] value = values.get(key);
          FieldMapping mapping = fieldMap.get(key);
          Class<?> type = value.length > 1 ? String[].class : String.class;
          String related = null;
          String name = key;
          if (mapping != null) {
            name = mapping.methodName;
            type = mapping.type;
            related = mapping.related;
          }
          String[] relatedValue = values.get(related);
          String relayMethod = null;
          if (Arrays.equals(value, relatedValue)) {
            relayMethod = fieldMap.get(related).methodName;
          }
          out.println();
          if (addOverrides) {
            out.println("  @Override");
          }
          out.println("  public " + type.getSimpleName() + " " + name + "() {");
          out.print("    return ");
          if (relayMethod != null) {
            out.println(relayMethod + "();");
          } else {
            if (type.isArray()) {
              out.println("new " + type.getSimpleName() + " { ");
              out.print("        ");
            }
            boolean first = true;
            for (String oneValue : value) {
              if (!first) {
                out.println(",");
                out.print("        ");
              }
              if (type == int.class || type == int[].class) {
                out.print(Integer.valueOf(oneValue) - 1);
              } else {
                out.print("\"" + quote(oneValue) + "\"");
              }
              first = false;
            }
            if (type.isArray()) {
              out.println();
              out.println("    };");
            } else {
              out.println(";");
            }
          }
          out.println("  }");
        }
        if (locale.isDefault()) {
          // TODO(jat): actually generate these from CLDR data
          out.println();
          out.println("  public String dateFormat() {");
          out.println("    return dateFormatMedium();");
          out.println("  }");
          out.println();
          out.println("  public String dateTime(String timePattern, String datePattern) {");
          out.println("    return datePattern + \" \" + timePattern;");
          out.println("  }");
          out.println();
          out.println("  public String dateTimeFull(String timePattern, String datePattern) {");
          out.println("    return dateTime(timePattern, datePattern);");
          out.println("  }");
          out.println();
          out.println("  public String dateTimeLong(String timePattern, String datePattern) {");
          out.println("    return dateTime(timePattern, datePattern);");
          out.println("  }");
          out.println();
          out.println("  public String dateTimeMedium(String timePattern, String datePattern) {");
          out.println("    return dateTime(timePattern, datePattern);");
          out.println("  }");
          out.println();
          out.println("  public String dateTimeShort(String timePattern, String datePattern) {");
          out.println("    return dateTime(timePattern, datePattern);");
          out.println("  }");
          out.println();
          out.println("  public String timeFormat() {");
          out.println("    return timeFormatMedium();");
          out.println("  }");
        }
        out.println("}");
      } finally {
        if (out != null) {
          out.close();
        }
      }
    }

    private void generateSources(TreeMap<Key, String[]> properties,
        Map<GwtLocale, Set<GwtLocale>> parents) throws IOException {
      Set<GwtLocale> locales = new HashSet<GwtLocale>();
      // process sorted locales/keys, generating each locale on change
      GwtLocale lastLocale = null;
      Map<String, String[]> thisLocale = new HashMap<String, String[]>();
      for (Entry<Key, String[]> entry : properties.entrySet()) {
        if (lastLocale != null && lastLocale != entry.getKey().locale) {
          GwtLocale parent = findEarliestAncestor(lastLocale, parents.get(lastLocale));
          generateLocale(lastLocale, parent, thisLocale);
          thisLocale.clear();
          lastLocale = null;
        }
        if (lastLocale == null) {
          lastLocale = entry.getKey().locale;
          locales.add(lastLocale);
        }
        thisLocale.put(entry.getKey().key, entry.getValue());
      }
      if (lastLocale != null) {
        GwtLocale parent = findEarliestAncestor(lastLocale, parents.get(lastLocale));
        generateLocale(lastLocale, parent, thisLocale);
      }
      Set<GwtLocale> seen = new HashSet<GwtLocale>(locales);
      for (GwtLocale locale : locales) {
        for (GwtLocale alias : locale.getAliases()) {
          if (!seen.contains(alias)) {
            seen.add(alias);
            // generateAlias(alias, locale);
          }
        }
      }
    }

    /**
     * Check if a given entry within a locale is inherited from a parent.
     * 
     * @param properties
     * @param parents
     * @param key
     * @param value
     * @return true if the value is the same as the first parent defining that
     *         value
     */
    private boolean isInherited(TreeMap<Key, String[]> properties,
        Map<GwtLocale, Set<GwtLocale>> parents, Key key, String[] value) {
      GwtLocale keyLocale = key.locale;
      if (keyLocale.isDefault()) {
        // never delete entries from default
        return false;
      }
      List<GwtLocale> list = keyLocale.getInheritanceChain();
      String[] parent = null;
      for (int i = 1; i < list.size(); ++i) {
        Key parentKey = new Key(list.get(i), key.key);
        parent = properties.get(parentKey);
        if (parent != null) {
          GwtLocale parentLocale = parentKey.locale;
          addLocaleParent(parents, keyLocale, parentLocale);
          break;
        }
      }
      return Arrays.equals(value, parent);
    }

    /**
     * Remove inherited values and return a map of inherited-from locales for
     * each locale.
     * 
     * @param properties
     * @return inheritance map
     */
    private Map<GwtLocale, Set<GwtLocale>> removeInheritedValues(TreeMap<Key, String[]> properties) {
      // remove entries identical to a parent locale
      Map<GwtLocale, Set<GwtLocale>> parents = new HashMap<GwtLocale, Set<GwtLocale>>();
      Set<Entry<Key, String[]>> entrySet = properties.entrySet();
      Iterator<Entry<Key, String[]>> it = entrySet.iterator();
      while (it.hasNext()) {
        Entry<Key, String[]> entry = it.next();
        if (isInherited(properties, parents, entry.getKey(), entry.getValue())) {
          it.remove();
        }
      }
      return parents;
    }
  }

  private static class FieldMapping {
    public final String methodName;
    public final Class<?> type;
    public final String related;

    public FieldMapping(String methodName, Class<?> type) {
      this(methodName, type, null);
    }

    public FieldMapping(String methodName, Class<?> type, String related) {
      this.methodName = methodName;
      this.type = type;
      this.related = related;
    }
  }

  private static class Key implements Comparable<Key> {
    public final GwtLocale locale;
    public final String key;

    public Key(GwtLocale locale, String key) {
      this.locale = locale;
      this.key = key;
    }

    public int compareTo(Key other) {
      int c = locale.compareTo(other.locale);
      if (c == 0) {
        c = key.compareTo(other.key);
      }
      return c;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null || getClass() != obj.getClass()) {
        return false;
      }
      Key other = (Key) obj;
      return locale.equals(other.locale) && key.equals(other.key);
    }

    @Override
    public int hashCode() {
      return locale.hashCode() * 31 + key.hashCode();
    }

    @Override
    public String toString() {
      return locale.toString() + "/" + key;
    }
  }

  private static final Map<String, FieldMapping> fieldMap = new HashMap<String, FieldMapping>();

  private static Map<String, String> patterns = new HashMap<String, String>();

  static {
    fieldMap.put("ampms", new FieldMapping("ampms", String[].class));
    fieldMap.put("dateFormats0", new FieldMapping("dateFormatFull", String.class));
    fieldMap.put("dateFormats1", new FieldMapping("dateFormatLong", String.class));
    fieldMap.put("dateFormats2", new FieldMapping("dateFormatMedium", String.class));
    fieldMap.put("dateFormats3", new FieldMapping("dateFormatShort", String.class));
    fieldMap.put("timeFormats0", new FieldMapping("timeFormatFull", String.class));
    fieldMap.put("timeFormats1", new FieldMapping("timeFormatLong", String.class));
    fieldMap.put("timeFormats2", new FieldMapping("timeFormatMedium", String.class));
    fieldMap.put("timeFormats3", new FieldMapping("timeFormatShort", String.class));
    fieldMap.put("eraNames", new FieldMapping("erasFull", String[].class));
    fieldMap.put("eras", new FieldMapping("erasShort", String[].class));
    fieldMap.put("quarters", new FieldMapping("quartersFull", String[].class));
    fieldMap.put("shortQuarters", new FieldMapping("quartersShort", String[].class));
    fieldMap.put("firstDayOfTheWeek", new FieldMapping("firstDayOfTheWeek", Integer.class));
    fieldMap.put("months", new FieldMapping("monthsFull", String[].class));
    fieldMap.put("standaloneMonths", new FieldMapping("monthsFullStandalone", String[].class,
        "months"));
    fieldMap.put("narrowMonths", new FieldMapping("monthsNarrow", String[].class));
    fieldMap.put("standaloneNarrowMonths", new FieldMapping("monthsNarrowStandalone",
        String[].class, "narrowMonths"));
    fieldMap.put("shortMonths", new FieldMapping("monthsShort", String[].class));
    fieldMap.put("standaloneShortMonths", new FieldMapping("monthsShortStandalone", String[].class,
        "shortMonths"));
    fieldMap.put("weekendRange0", new FieldMapping("weekendStart", int.class));
    fieldMap.put("weekendRange1", new FieldMapping("weekendEnd", int.class));
    fieldMap.put("firstDayOfTheWeek", new FieldMapping("firstDayOfTheWeek", int.class));
    fieldMap.put("weekdays", new FieldMapping("weekdaysFull", String[].class));
    fieldMap.put("standaloneWeekdays", new FieldMapping("weekdaysFullStandalone", String[].class,
        "weekdays"));
    fieldMap.put("shortWeekdays", new FieldMapping("weekdaysShort", String[].class));
    fieldMap.put("standaloneShortWeekdays", new FieldMapping("weekdaysShortStandalone",
        String[].class, "shortWeekdays"));
    fieldMap.put("narrowWeekdays", new FieldMapping("weekdaysNarrow", String[].class));
    fieldMap.put("standaloneNarrowWeekdays", new FieldMapping("weekdaysNarrowStandalone",
        String[].class, "narrowWeekdays"));

    // patterns to use with DateTimePatternGenerator
    patterns.put("Day", "d");
    patterns.put("Hour12Minute", "hmm");
    patterns.put("Hour12MinuteSecond", "hmmss");
    patterns.put("Hour24Minute", "Hmm");
    patterns.put("Hour24MinuteSecond", "Hmmss");
    patterns.put("MinuteSecond", "mss");
    patterns.put("MonthAbbrev", "MMM");
    patterns.put("MonthAbbrevDay", "MMMd");
    patterns.put("MonthFull", "MMMM");
    patterns.put("MonthFullDay", "MMMMd");
    patterns.put("MonthFullWeekdayDay", "MMMMEEEEd");
    patterns.put("MonthNumDay", "Md");
    patterns.put("Year", "y");
    patterns.put("YearMonthAbbrev", "yMMM");
    patterns.put("YearMonthAbbrevDay", "yMMMd");
    patterns.put("YearMonthFull", "yMMMM");
    patterns.put("YearMonthFullDay", "yMMMMd");
    patterns.put("YearMonthNum", "yM");
    patterns.put("YearMonthNumDay", "yMd");
    patterns.put("YearMonthWeekdayDay", "yMMMEEEd");
    patterns.put("YearQuarterFull", "yQQQQ");
    patterns.put("YearQuarterShort", "yQ");
  }

  /**
   * @param args
   * @throws IOException
   */
  public static void main(String[] args) throws IOException {
    if (args.length != 1) {
      System.err.println("Usage: " + DateTimeFormatCreator.class.getSimpleName() + " gwt-root-dir");
      return;
    }
    File gwt = new File(args[0]);
    File src = new File(gwt, "user/src");
    if (!gwt.exists() || !src.exists()) {
      System.err.println(args[0] + " doesn't appear to be a GWT root directory");
      return;
    }
    new DtfiGenerator(src).generate();
  }
}
