/*
 * 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.ext.TreeLogger;
import com.google.gwt.i18n.client.PluralRule.PluralForm;
import com.google.gwt.i18n.shared.GwtLocale;

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * AbstractResource serves the same purpose as java
 * ResourceBundle/PropertyResourceBundle.
 * <p>
 * Each <code>Resource</code> belongs to a resource tree, indicated by the
 * path attribute.
 * <p>
 * AbstractResource uses a Factory pattern rather than a single static method to
 * load itself given an abstract string path.
 * <p>
 * One advanced feature which should not be used outside the core GWT system is
 * that resources can have more than one parent, for instance pets_en_US could
 * have pets_en as one parent and animals_en_US as another. The alternative
 * parents have lower precedence than any primary parent. Each alternative
 * parent is associated with a separate resource tree.
 */
public abstract class AbstractResource {

  /**
   * Exception indicating a required resource was not found.
   */
  public static class MissingResourceException extends RuntimeException {
    private String during;
    private String key;
    private String method;
    private List<AbstractResource> searchedResources;

    public MissingResourceException(String key,
        List<AbstractResource> searchedResources) {
      super("No resource found for key '" + key + "'");
      this.key = key;
      this.searchedResources = searchedResources;
    }

    public String getDuring() {
      return during;
    }

    public String getKey() {
      return key;
    }

    public String getMethod() {
      return method;
    }

    public List<AbstractResource> getSearchedResources() {
      return searchedResources;
    }

    public void setDuring(String during) {
      this.during = during;
    }

    public void setMethod(String method) {
      this.method = method;
    }
  }

  /**
   * Definition of a single entry for a resource.
   */
  public interface ResourceEntry {

    /**
     * Retrieve a particular form for this entry.
     *
     * @param form form to retrieve (null for the default)
     * @return null if the requested form is not present
     */
    String getForm(String form);

    /**
     * Returns a list of forms associated with this entry.
     *
     * The default form (also the only form for anything other than messages
     * with plural support) is always available and not present in this list.
     */
    Collection<String> getForms();

    /**
     * Returns key for this entry (must not be null).
     */
    String getKey();
  }

  /**
   * Encapsulates an ordered set of resources to search for translations.
   */
  public static class ResourceList extends AbstractList<AbstractResource> {

    private List<AbstractResource> list = new ArrayList<AbstractResource>();

    private Map<String, PluralForm[]> pluralForms = new HashMap<String, PluralForm[]>();

    private Set<AbstractResource> set = new HashSet<AbstractResource>();

    @Override
    public boolean add(AbstractResource element) {
      if (set.contains(element)) {
        return false;
      }
      set.add(element);
      return list.add(element);
    }

    @Override
    public void add(int index, AbstractResource element) {
      if (set.contains(element)) {
        throw new IllegalArgumentException("Duplicate element");
      }
      set.add(element);
      list.add(index, element);
    }

    /**
     * Add all keys known by this ResourceList to the specified set.
     *
     * @param s set to add keys to
     */
    public void addToKeySet(Set<String> s) {
      for (AbstractResource resource : list) {
        resource.addToKeySet(s);
      }
    }

    /**
     * From the list of locales matched for any resources in this resource list,
     * choose the one that is least derived from the original search locale.
     * @param logger logger to use
     * @param locale originally requested locale
     * @return least derived matched locale
     */
    public GwtLocale findLeastDerivedLocale(TreeLogger logger,
        GwtLocale locale) {
      List<GwtLocale> searchList = locale.getCompleteSearchList();
      Map<GwtLocale, Integer> derivedIndex = new HashMap<GwtLocale, Integer>();
      for (int i = 0; i < searchList.size(); ++i) {
        derivedIndex.put(searchList.get(i), i);
      }
      GwtLocale defaultLocale = LocaleUtils.getLocaleFactory().getDefault();
      GwtLocale best = defaultLocale;
      int bestIdx = Integer.MAX_VALUE;
      for (int i = 0; i < list.size(); ++i) {
        GwtLocale matchLocale = list.get(i).getMatchLocale();
        Integer wrappedIdx = derivedIndex.get(matchLocale);
        if (wrappedIdx == null) {
          // We had an @DefaultLocale for a locale not present in this
          // permutation -- treat it as the default locale.
          wrappedIdx = derivedIndex.get(defaultLocale);
          if (wrappedIdx == null) {
            // shouldn't happen
            assert false : "No default locale in search list";
            continue;
          }
        }
        int idx = wrappedIdx;
        if (idx < bestIdx) {
          bestIdx = idx;
          best = matchLocale;
        }
      }
      return best;
    }

    @Override
    public AbstractResource get(int index) {
      return list.get(index);
    }

    /**
     * Returns the first AnnotationsResource containing a specified key.
     *
     * @param logger
     * @param key
     * @return first AnnotationsResource containing key, or null if none
     */
    public AnnotationsResource getAnnotationsResource(TreeLogger logger,
        String key) {
      for (AbstractResource resource : list) {
        if (resource instanceof AnnotationsResource
            && resource.keySet.contains(key)) {
          return (AnnotationsResource) resource;
        }
      }
      return null;
    }

    /**
     * Get an entry from the first resource in this list containing a match.
     *
     * @param key
     * @return a ResourceEntry instance
     */
    public ResourceEntry getEntry(String key) {
      for (AbstractResource resource : list) {
        ResourceEntry e = resource.getEntry(key);
        if (e != null) {
          return e;
        }
      }
      return null;
    }

    /**
     * Returns the list of extensions available for a given key.
     *
     * @param key
     * @return collection of extensions for the given key
     */
    public Collection<String> getExtension(String key) {
      Set<String> extensions = new HashSet<String>();
      for (AbstractResource resource : list) {
        extensions.addAll(resource.getExtensions(key));
      }
      return extensions;
    }

    /**
     * Returns the list of plural forms for a given key.
     *
     * @param key
     * @return array of plural forms.
     */
    public PluralForm[] getPluralForms(String key) {
      return pluralForms.get(key);
    }

    /**
     * Returns a translation for a key, or throw an exception.
     *
     * @param key
     * @return translated string for key
     * @throws MissingResourceException
     */
    public String getRequiredString(String key)
        throws MissingResourceException {
      String val = getString(key);
      if (val == null) {
        throw new MissingResourceException(key, list);
      }
      return val;
    }

    /**
     * Returns a translation for a key/extension, or throw an exception.
     *
     * @param key
     * @param ext key extension, null if none
     * @return translated string for key
     * @throws MissingResourceException
     */
    public String getRequiredStringExt(String key, String ext)
        throws MissingResourceException {
      String val = getStringExt(key, ext);
      if (val == null) {
        throw new MissingResourceException(getExtendedKey(key, ext), list);
      }
      return val;
    }

    /**
     * Returns a translation for a key, or null if not found.
     *
     * @param key
     * @return translated string for key
     */
    public String getString(String key) {
      for (AbstractResource resource : list) {
        String s = resource.getStringExt(key, null);
        if (s != null) {
          return s;
        }
      }
      return null;
    }

    /**
     * Returns a translation for a key/extension, or null if not found.
     *
     * @param key
     * @param extension key extension, null if none
     * @return translated string for key
     */
    public String getStringExt(String key, String extension) {
      for (AbstractResource resource : list) {
        String s = resource.getStringExt(key, extension);
        if (s != null) {
          return s;
        }
      }
      return null;
    }

    @Override
    public int indexOf(Object o) {
      return list.indexOf(o);
    }

    @Override
    public Iterator<AbstractResource> iterator() {
      return list.iterator();
    }

    /**
     * Returns set of keys present across all resources.
     */
    public Set<String> keySet() {
      Set<String> keySet = new HashSet<String>();
      for (AbstractResource resource : list) {
        keySet.addAll(resource.keySet());
      }
      return keySet;
    }

    @Override
    public int lastIndexOf(Object o) {
      return list.lastIndexOf(o);
    }

    @Override
    public AbstractResource remove(int index) {
      AbstractResource element = list.remove(index);
      set.remove(element);
      return element;
    }

    /**
     * Set the plural forms associated with a given message.
     *
     * @param key
     * @param forms
     */
    public void setPluralForms(String key, PluralForm[] forms) {
      if (!pluralForms.containsKey(key)) {
        pluralForms.put(key, forms);
      }
    }

    @Override
    public int size() {
      return list.size();
    }
  }

  /**
   * Implementation of ResourceEntry that supports multiple forms per entry.
   */
  protected static class MultipleFormEntry implements ResourceEntry {

    private final String key;
    private final Map<String, String> values = new HashMap<String, String>();
    private final Set<String> forms = new HashSet<String>();

    public MultipleFormEntry(String key) {
      this.key = key;
    }

    public void addForm(String form, String value) {
      values.put(form, value);
      if (form != null) {
        forms.add(form);
      }
    }

    public String getForm(String form) {
      return values.get(form);
    }

    public Collection<String> getForms() {
      return forms;
    }

    public String getKey() {
      return key;
    }
  }

  /**
   * A simple resource entry with no alternate forms, only a key and a value.
   */
  protected static class SimpleEntry implements ResourceEntry {

    private final String key;
    private final String value;

    public SimpleEntry(String key, String value) {
      this.key = key;
      this.value = value;
    }

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

    public String getForm(String form) {
      return form != null ? null : value;
    }

    public Collection<String> getForms() {
      return Collections.emptyList();
    }

    public String getKey() {
      return key;
    }

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

  /**
   * Error messages concerning missing keys should include the defined keys if
   * the number of keys is below this threshold.
   */
  public static final int REPORT_KEYS_THRESHOLD = 30;

  protected static String getExtendedKey(String key, String extension) {
    if (extension != null) {
      key += '[' + extension + ']';
    }
    return key;
  }

  protected GwtLocale matchLocale;

  private Set<String> keySet;

  private String path;

  public AbstractResource(GwtLocale matchLocale) {
    this.matchLocale = matchLocale;
  }

  /**
   * Returns an entry in this resource.
   *
   * @param key
   * @return ResourceEntry instance
   */
  public ResourceEntry getEntry(String key) {
    String value = getString(key);
    return value == null ? null : new SimpleEntry(key, value);
  }

  /**
   * @param key
   */
  public Collection<String> getExtensions(String key) {
    return new ArrayList<String>();
  }

  /**
   * Get a string and fail if not present.
   *
   * @param key
   * @return the requested string
   */
  public final String getRequiredString(String key) {
    return getRequiredStringExt(key, null);
  }

  /**
   * Get a string (with optional extension) and fail if not present.
   *
   * @param key
   * @param extension
   * @return the requested string
   */
  public final String getRequiredStringExt(String key, String extension) {
    String s = getStringExt(key, extension);
    if (s == null) {
      ArrayList<AbstractResource> list = new ArrayList<AbstractResource>();
      list.add(this);
      throw new MissingResourceException(key, list);
    }
    return s;
  }

  /**
   * Get a key.
   *
   * @param key key to lookup
   * @return the string for the given key or null if not found
   * @see java.util.ResourceBundle#getString(java.lang.String)
   */
  public final String getString(String key) {
    return getStringExt(key, null);
  }

  /**
   * Get a key with an extension. Identical to getString() if extension is null.
   *
   * @param key to lookup
   * @param extension extension of the key, nullable
   * @return string or null
   */
  public abstract String getStringExt(String key, String extension);

  /**
   * Keys associated with this resource.
   *
   * @return keys
   */
  public Set<String> keySet() {
    if (keySet == null) {
      keySet = new HashSet<String>();
      addToKeySet(keySet);
    }
    return keySet;
  }

  /**
   * Returns true if this resource has any keys.
   */
  public boolean notEmpty() {
    return !keySet.isEmpty();
  }

  @Override
  public String toString() {
    return "resource for " + path;
  }

  /**
   * A multi-line representation of this object.
   *
   * @return verbose string
   */
  public String toVerboseString() {
    StringBuffer b = new StringBuffer();
    toVerboseStringAux(0, b);
    return b.toString();
  }

  abstract void addToKeySet(Set<String> s);

  GwtLocale getMatchLocale() {
    return matchLocale;
  }

  String getPath() {
    return path;
  }

  void setPath(String path) {
    this.path = path;
  }

  private void newLine(int indent, StringBuffer buf) {
    buf.append("\n");
    for (int i = 0; i < indent; i++) {
      buf.append("\t");
    }
  }

  private void toVerboseStringAux(int indent, StringBuffer buf) {
    newLine(indent, buf);
    buf.append(toString());
  }
}
