/*
 * 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 static com.google.gwt.i18n.rebind.AnnotationUtil.getClassAnnotation;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.dev.resource.Resource;
import com.google.gwt.dev.resource.ResourceOracle;
import com.google.gwt.dev.util.StringKey;
import com.google.gwt.dev.util.collect.IdentityHashSet;
import com.google.gwt.i18n.client.LocalizableResource.DefaultLocale;
import com.google.gwt.i18n.rebind.AbstractResource.ResourceList;
import com.google.gwt.i18n.rebind.AnnotationsResource.AnnotationsError;
import com.google.gwt.i18n.shared.GwtLocale;
import com.google.gwt.i18n.shared.GwtLocaleFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

/**
 * Creates resources.
 */
public abstract class ResourceFactory {

  /**
   * A key based on JClassType and GwtLocale.
   */
  static class ClassLocale extends StringKey {
    public ClassLocale(JClassType clazz, GwtLocale locale) {
      super(clazz.getQualifiedSourceName() + "/" + locale.toString());
    }
  }

  /**
   * Separator between class name and locale in resource files. Should not
   * appear in valid localizable class names.
   */
  public static final char LOCALE_SEPARATOR = '_';

  private static List<ResourceFactory> loaders = new ArrayList<ResourceFactory>();

  /**
   * Since multiple generators share the ResourceFactory, we tie the
   * ResourceFactoryContext cache to the GeneratorContext.
   */
  private static final WeakHashMap<GeneratorContext, ResourceFactoryContext>
      resourceFactoryCtxHolder = new WeakHashMap<GeneratorContext, ResourceFactoryContext>();

  static {
    loaders.add(new LocalizedPropertiesResource.Factory());
  }

  public static synchronized ResourceList getBundle(TreeLogger logger,
      JClassType topClass, GwtLocale bundleLocale, boolean isConstants,
      GeneratorContext genCtx) {
    List<GwtLocale> locales = bundleLocale.getCompleteSearchList();
    List<JClassType> classes = new ArrayList<JClassType>();
    Set<JClassType> seenClasses = new IdentityHashSet<JClassType>();
    Map<ClassLocale, AnnotationsResource> annotations = new HashMap<ClassLocale, AnnotationsResource>();
    GwtLocaleFactory factory = LocaleUtils.getLocaleFactory();
    GwtLocale defaultLocale = factory.getDefault();
    walkInheritanceTree(logger, topClass, factory, defaultLocale, classes,
        annotations, seenClasses, isConstants);
    // TODO(jat): handle explicit subinterface with other locales -- ie:
    // public interface Foo_es_MX extends Foo { ... }
    ResourceList allResources = new ResourceList();
    ResourceFactoryContext localizableCtx = getResourceFactoryContext(genCtx);
    for (GwtLocale locale : locales) {
      for (JClassType clazz : classes) {
        ClassLocale key = new ClassLocale(clazz, locale);
        ResourceList resources;
        resources = localizableCtx.getResourceList(key);
        if (resources == null) {
          resources = new ResourceList();
          addFileResources(logger, clazz, locale, genCtx.getResourcesOracle(), resources);
          AnnotationsResource annotationsResource = annotations.get(key);
          if (annotationsResource != null) {
            resources.add(annotationsResource);
          }
          localizableCtx.putResourceList(key, resources);
        }
        allResources.addAll(resources);
      }
    }
    String className = topClass.getQualifiedSourceName();
    TreeLogger branch = logger.branch(TreeLogger.SPAM,
        "Resource search order for " + className + ", locale " + bundleLocale);
    if (logger.isLoggable(TreeLogger.SPAM)) {
      for (AbstractResource resource : allResources) {
        branch.log(TreeLogger.SPAM, resource.toString());
      }
    }
    return allResources;
  }

  public static String getResourceName(JClassType targetClass) {
    String name = targetClass.getName();
    if (targetClass.isMemberType()) {
      name = name.replace('.', '$');
    }
    return name;
  }

  private static void addFileResources(TreeLogger logger, JClassType clazz, GwtLocale locale,
      ResourceOracle resourceOracle, ResourceList resources) {
    // TODO: handle classes in the default package?
    String targetPath = clazz.getPackage().getName() + '.'
        + getResourceName(clazz);
    String localizedPath = targetPath;
    if (!locale.isDefault()) {
      localizedPath = targetPath + LOCALE_SEPARATOR + locale.getAsString();
    }
    // Check for file-based resources.
    String partialPath = localizedPath.replace('.', '/');
    for (int i = 0; i < loaders.size(); i++) {
      ResourceFactory element = loaders.get(i);
      String ext = "." + element.getExt();
      String path = partialPath + ext;
      Resource resource = resourceOracle.getResource(path);
      if (resource == null && partialPath.contains("$")) {
        // Also look for A_B for inner classes, as $ in path names
        // can cause issues for some build tools.
        path = partialPath.replace('$', '_') + ext;
        resource = resourceOracle.getResource(path);
      }
      if (resource != null) {
        InputStream resourceStream = null;
        try {
          resourceStream = resource.openContents();
        } catch (IOException ex) {
          logger.log(TreeLogger.ERROR, "Error opening resource: " + resource.getLocation());
          throw new RuntimeException(ex);
        }
        AbstractResource found = element.load(resourceStream, locale);
        found.setPath(path);
        resources.add(found);
      }
    }
  }

  private static synchronized ResourceFactoryContext getResourceFactoryContext(
      GeneratorContext context) {
    if (context instanceof CachedGeneratorContext) {
      context = ((CachedGeneratorContext) context).getWrappedGeneratorContext();
    }
    ResourceFactoryContext resourceFactoryCtx = resourceFactoryCtxHolder.get(context);
    if (resourceFactoryCtx == null) {
      resourceFactoryCtx = new ResourceFactoryContext();
      resourceFactoryCtxHolder.put(context, resourceFactoryCtx);
    }
    return resourceFactoryCtx;
  }

  private static void walkInheritanceTree(TreeLogger logger, JClassType clazz,
      GwtLocaleFactory factory, GwtLocale defaultLocale,
      List<JClassType> classes,
      Map<ClassLocale, AnnotationsResource> annotations,
      Set<JClassType> seenClasses, boolean isConstants) {
    if (seenClasses.contains(clazz)) {
      return;
    }
    seenClasses.add(clazz);
    classes.add(clazz);
    AnnotationsResource resource;
    try {
      resource = new AnnotationsResource(logger, clazz, defaultLocale,
          isConstants);
      if (resource.notEmpty()) {
        resource.setPath(clazz.getQualifiedSourceName());
        ClassLocale key = new ClassLocale(clazz, defaultLocale);
        annotations.put(key, resource);
        String defLocaleValue = null;

        // If the class has an embedded locale in it, use that for the default
        String className = clazz.getSimpleSourceName();
        int underscore = className.indexOf('_');
        if (underscore >= 0) {
          defLocaleValue = className.substring(underscore + 1);
        }

        // If there is an annotation declaring the default locale, use that
        DefaultLocale defLocaleAnnot = getClassAnnotation(clazz,
            DefaultLocale.class);
        if (defLocaleAnnot != null) {
          defLocaleValue = defLocaleAnnot.value();
        }
        GwtLocale defLocale = LocaleUtils.getLocaleFactory().fromString(
            defLocaleValue);
        if (!defLocale.isDefault()) {
          key = new ClassLocale(clazz, defLocale);
          annotations.put(key, resource);
        }
      }
    } catch (AnnotationsError e) {
      logger.log(TreeLogger.ERROR, e.getMessage(), e);
    }
    if (clazz.getSuperclass() != null) {
      walkInheritanceTree(logger, clazz.getSuperclass(), factory,
          defaultLocale, classes, annotations, seenClasses, isConstants);
    }
    for (JClassType intf : clazz.getImplementedInterfaces()) {
      walkInheritanceTree(logger, intf, factory, defaultLocale, classes,
          annotations, seenClasses, isConstants);
    }
  }

  abstract String getExt();

  abstract AbstractResource load(InputStream m, GwtLocale locale);
}
