/*
 * 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.BadPropertyValueException;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.SelectionProperty;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.i18n.client.LocalizableResource.Generate;
import com.google.gwt.i18n.client.LocalizableResource.Key;
import com.google.gwt.i18n.rebind.AbstractResource.ResourceList;
import com.google.gwt.i18n.rebind.AnnotationsResource.AnnotationsError;
import com.google.gwt.i18n.rebind.format.MessageCatalogFormat;
import com.google.gwt.i18n.server.KeyGenerator;
import com.google.gwt.i18n.server.MessageCatalogFactory;
import com.google.gwt.i18n.server.MessageCatalogFactory.Context;
import com.google.gwt.i18n.server.MessageCatalogFactory.Writer;
import com.google.gwt.i18n.server.MessageInterface;
import com.google.gwt.i18n.server.MessageProcessingException;
import com.google.gwt.i18n.shared.GwtLocale;
import com.google.gwt.i18n.shared.GwtLocaleFactory;
import com.google.gwt.user.rebind.AbstractGeneratorClassCreator;
import com.google.gwt.user.rebind.AbstractMethodCreator;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.MissingResourceException;

/**
 * Represents generic functionality needed for <code>Constants</code> and
 * <code>Messages</code> classes.
 */
abstract class AbstractLocalizableImplCreator extends
    AbstractGeneratorClassCreator {

  public static class MessageCatalogContextImpl
      implements Context {

    private final GeneratorContext context;
    private final TreeLogger logger;

    public MessageCatalogContextImpl(GeneratorContext context,
        TreeLogger logger) {
      this.context = context;
      this.logger = logger;
    }

    public OutputStream createBinaryFile(String catalogName) {
      try {
        final OutputStream ostr = context.tryCreateResource(logger, catalogName);
        if (ostr != null) {
          // wrap the stream so we can commit the resource on close
          return new OutputStream() {

            @Override
            public void close() throws IOException {
              try {
                context.commitResource(logger, ostr).setVisibility(
                    Visibility.Private);
              } catch (UnableToCompleteException e) {
                // error already logged, anything more to do?
              }
            }

            @Override
            public void write(byte[] b, int off, int len) throws IOException {
              ostr.write(b, off, len);
            }

            @Override
            public void write(int b) throws IOException {
              ostr.write(b);
            }
          };
        }
      } catch (UnableToCompleteException e) {
        // error already logged, anything more to do?
      }
      return null;
    }

    public PrintWriter createTextFile(String catalogName, String charSet) {
      OutputStream outStr = createBinaryFile(catalogName);
      if (outStr != null) {
        try {
          return new PrintWriter(new BufferedWriter(
              new OutputStreamWriter(outStr, "UTF-8")), false);
        } catch (UnsupportedEncodingException e) {
          error("UTF-8 not supported", e);
        }
      }
      return null;
    }

    public void error(String msg) {
      logger.log(TreeLogger.ERROR, msg);
    }

    public void error(String msg, Throwable cause) {
      logger.log(TreeLogger.ERROR, msg, cause);
    }

    public GwtLocaleFactory getLocaleFactory() {
      return LocaleUtils.getLocaleFactory();
    }

    public void warning(String msg) {
      logger.log(TreeLogger.WARN, msg);
    }

    public void warning(String msg, Throwable cause) {
      logger.log(TreeLogger.WARN, msg, cause);
    }
  }

  static String generateConstantOrMessageClass(TreeLogger logger,
      GeneratorContext context, GwtLocale locale, JClassType targetClass)
      throws UnableToCompleteException {
    TypeOracle oracle = context.getTypeOracle();
    JClassType constantsClass;
    JClassType messagesClass;
    JClassType constantsWithLookupClass;
    boolean seenError = false;
    try {
      constantsClass = oracle.getType(LocalizableGenerator.CONSTANTS_NAME);
      constantsWithLookupClass = oracle.getType(LocalizableGenerator.CONSTANTS_WITH_LOOKUP_NAME);
      messagesClass = oracle.getType(LocalizableGenerator.MESSAGES_NAME);
    } catch (NotFoundException e) {
      // Should never happen in practice.
      throw error(logger, e);
    }

    String name = targetClass.getName();
    String packageName = targetClass.getPackage().getName();

    // Make sure the interface being rebound extends either Constants or
    // Messages.
    boolean assignableToConstants = constantsClass.isAssignableFrom(targetClass);
    boolean assignableToMessages = messagesClass.isAssignableFrom(targetClass);
    if (!assignableToConstants && !assignableToMessages) {
      // Let the implementation generator handle this interface.
      return null;
    }

    // Make sure that they don't try to extend both Messages and Constants.
    if (assignableToConstants && assignableToMessages) {
      throw error(logger, name + " cannot extend both Constants and Messages");
    }

    // Make sure that the type being rebound is in fact an interface.
    if (targetClass.isInterface() == null) {
      throw error(logger, name + " must be an interface");
    }

    ResourceList resourceList = null;
    try {
      resourceList = ResourceFactory.getBundle(logger, targetClass, locale,
          assignableToConstants, context);
    } catch (MissingResourceException e) {
      throw error(logger,
          "Localization failed; there must be at least one resource accessible through"
              + " the classpath in package '" + packageName
              + "' whose base name is '"
              + ResourceFactory.getResourceName(targetClass) + "'");
    } catch (IllegalArgumentException e) {
      // A bad key can generate an illegal argument exception.
      throw error(logger, e.getMessage());
    }

    // generated implementations for interface X will be named X_, X_en,
    // X_en_CA, etc.
    GwtLocale generatedLocale = resourceList.findLeastDerivedLocale(logger,
        locale);
    String localeSuffix = String.valueOf(ResourceFactory.LOCALE_SEPARATOR);
    localeSuffix += generatedLocale.getAsString();
    // Use _ rather than "." in class name, cannot use $
    String resourceName = targetClass.getName().replace('.', '_');
    String className = resourceName + localeSuffix;
    PrintWriter pw = context.tryCreate(logger, packageName, className);
    if (pw != null) {
      ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(
          packageName, className);
      factory.addImplementedInterface(targetClass.getQualifiedSourceName());
      SourceWriter writer = factory.createSourceWriter(context, pw);
      // Now that we have all the information set up, process the class
      if (constantsWithLookupClass.isAssignableFrom(targetClass)) {
        ConstantsWithLookupImplCreator c = new ConstantsWithLookupImplCreator(
            logger, writer, targetClass, resourceList, context.getTypeOracle());
        c.emitClass(logger, generatedLocale);
      } else if (constantsClass.isAssignableFrom(targetClass)) {
        ConstantsImplCreator c = new ConstantsImplCreator(logger, writer,
            targetClass, resourceList, context.getTypeOracle());
        c.emitClass(logger, generatedLocale);
      } else {
        MessagesImplCreator messages = new MessagesImplCreator(logger, writer, targetClass,
            resourceList, context.getTypeOracle(), context.getResourcesOracle());
        messages.emitClass(logger, generatedLocale);
      }
      context.commit(logger, pw);
    }
    // Generate a translatable output file if requested.
    Generate generate = getClassAnnotation(targetClass, Generate.class);
    if (generate != null) {
      String path = generate.fileName();
      if (Generate.DEFAULT.equals(path)) {
        path = targetClass.getPackage().getName() + "."
            + targetClass.getName().replace('.', '_');
      } else if (path.endsWith(File.pathSeparator)) {
        path = path + targetClass.getName().replace('.', '_');
      }
      String[] genLocales = generate.locales();
      boolean found = false;
      if (genLocales.length != 0) {
        // verify the current locale is in the list
        for (String genLocale : genLocales) {
          if (GwtLocale.DEFAULT_LOCALE.equals(genLocale)) {
            // Locale "default" gets special handling because of property
            // fallbacks; "default" might be mapped to any real locale.
            try {
              SelectionProperty localeProp = context.getPropertyOracle()
                  .getSelectionProperty(logger, "locale");
              String defaultLocale = localeProp.getFallbackValue();
              if (defaultLocale.length() > 0) {
                genLocale = defaultLocale;
              }
            } catch (BadPropertyValueException e) {
              throw error(logger, "Could not get 'locale' property");
            }
          }
          if (genLocale.equals(locale.toString())) {
            found = true;
            break;
          }
        }
      } else {
        // Since they want all locales, this is guaranteed to be one of them.
        found = true;
      }
      if (found) {
        for (String genClassName : generate.format()) {
          MessageCatalogFormat msgWriter = null;
          MessageCatalogFactory msgCatFactory = null;
          try {
            // TODO(jat): if GWT is ever modified to take a classpath for user
            // code as an option, we would need to use the user classloader here
            Class<?> clazz = Class.forName(genClassName, false,
                MessageCatalogFormat.class.getClassLoader());
            if (MessageCatalogFormat.class.isAssignableFrom(clazz)) {
              Class<? extends MessageCatalogFormat> msgFormatClass
                  = clazz.asSubclass(MessageCatalogFormat.class);
              msgWriter = msgFormatClass.newInstance();
            } else if (MessageCatalogFactory.class.isAssignableFrom(clazz)) {
              Class<? extends MessageCatalogFactory> msgFactoryClass
                  = clazz.asSubclass(MessageCatalogFactory.class);
              msgCatFactory = msgFactoryClass.newInstance();
            } else {
              logger.log(TreeLogger.ERROR, "Class specified in @Generate must "
                  + "either be a subtype of MessageCatalogFormat or "
                  + "MessageCatalogFactory");
              seenError = true;
              continue;
            }
          } catch (InstantiationException e) {
            logger.log(TreeLogger.ERROR, "Error instantiating @Generate class "
                + genClassName, e);
            seenError = true;
            continue;
          } catch (IllegalAccessException e) {
            logger.log(TreeLogger.ERROR, "@Generate class " + genClassName
                + " illegal access", e);
            seenError = true;
            continue;
          } catch (ClassNotFoundException e) {
            logger.log(TreeLogger.ERROR, "@Generate class " + genClassName
                + " not found");
            seenError = true;
            continue;
          }
          // Make generator-specific changes to a temporary copy of the path.
          String genPath = path;
          if (genLocales.length != 1) {
            // If the user explicitly specified only one locale, do not add the
            // locale.
            genPath += '_' + locale.toString();
          }
          if (msgCatFactory != null) {
            seenError |= generateToMsgCatFactory(logger, context, locale,
                targetClass, seenError, resourceList, msgCatFactory, genPath);
          } else if (msgWriter != null) {
            seenError |= generateToLegacyMsgCatFormat(logger, context, locale,
                targetClass, seenError, resourceList, className, msgWriter,
                genPath);
          }
        }
      }
    }
    if (seenError) {
      // If one of our generators had a fatal error, don't complete normally.
      throw new UnableToCompleteException();
    }
    return packageName + "." + className;
  }

  /**
   * Write translation source files to the old-style
   * {@link MessageCatalogFormat}.
   *
   * @return true if an error occurred (already logged)
   */
  private static boolean generateToLegacyMsgCatFormat(TreeLogger logger,
      GeneratorContext context, GwtLocale locale, JClassType targetClass,
      boolean seenError, ResourceList resourceList, String className,
      MessageCatalogFormat msgWriter, String genPath)
      throws UnableToCompleteException {
    genPath += msgWriter.getExtension();
    OutputStream outStr = context.tryCreateResource(logger, genPath);
    if (outStr != null) {
      TreeLogger branch = logger.branch(TreeLogger.TRACE, "Generating "
          + genPath + " from " + className + " for locale " + locale,
          null);
      PrintWriter out = null;
      try {
        out = new PrintWriter(new BufferedWriter(
            new OutputStreamWriter(outStr, "UTF-8")), false);
      } catch (UnsupportedEncodingException e) {
        throw error(logger, "UTF-8 not supported", e);
      }
      try {
        msgWriter.write(branch, locale.toString(), resourceList, out,
            targetClass);
        out.flush();
        context.commitResource(logger, outStr).setVisibility(
            Visibility.Private);
      } catch (UnableToCompleteException e) {
        // msgWriter should have already logged an error message.
        // Keep going for now so we can find other errors.
        seenError = true;
      }
    }
    return seenError;
  }

  /**
   * Write translation source files to a {@link MessageCatalogFactory}.
   *
   * @param logger
   * @param context
   * @param locale
   * @param targetClass
   * @param seenError
   * @param resourceList
   * @param msgCatFactory
   * @param genPath
   * @return true if an error occurred (already logged)
   */
  private static boolean generateToMsgCatFactory(TreeLogger logger,
      GeneratorContext context, GwtLocale locale, JClassType targetClass, boolean seenError,
      ResourceList resourceList, MessageCatalogFactory msgCatFactory,
      String genPath) {
    // TODO(jat): maintain MessageCatalogWriter instances across
    // generator runs so they can save state.  One problem is knowing
    // when the last generator has been run.
    Writer catWriter = null;
    try {
      String catalogName = genPath + msgCatFactory.getExtension();
      Context ctx = new MessageCatalogContextImpl(
          context, logger);
      MessageInterface msgIntf = new TypeOracleMessageInterface(
          LocaleUtils.getLocaleFactory(), targetClass, resourceList);
      catWriter = msgCatFactory.getWriter(ctx, catalogName);
      if (catWriter == null) {
        if (logger.isLoggable(TreeLogger.TRACE)) {
          logger.log(TreeLogger.TRACE, "Already generated " + catalogName);
        }
        return false;
      }
      msgIntf.accept(catWriter.visitClass());
    } catch (MessageProcessingException e) {
      logger.log(TreeLogger.ERROR, e.getMessage(), e);
      seenError = true;
    } finally {
      if (catWriter != null) {
        try {
          catWriter.close();
        } catch (IOException e) {
          logger.log(TreeLogger.ERROR,
              "IO error closing catalog writer", e);
          seenError = true;
        }
      }
    }
    return seenError;
  }

  /**
   * Generator to use to create keys for messages.
   */
  private KeyGenerator keyGenerator;

  /**
   * The Dictionary/value bindings used to determine message contents.
   */
  private ResourceList resourceList;

  /**
   * True if the class being generated uses Constants-style annotations/quoting.
   */
  private boolean isConstants;

  /**
   * Constructor for <code>AbstractLocalizableImplCreator</code>.
   *
   * @param writer writer
   * @param targetClass current target
   * @param resourceList backing resource
   */
  public AbstractLocalizableImplCreator(TreeLogger logger, SourceWriter writer,
      JClassType targetClass, ResourceList resourceList, boolean isConstants) {
    super(writer, targetClass);
    this.resourceList = resourceList;
    this.isConstants = isConstants;
    try {
      keyGenerator = AnnotationsResource.getKeyGenerator(targetClass);
    } catch (AnnotationsError e) {
      logger.log(TreeLogger.WARN, "Error getting key generator for "
          + targetClass.getQualifiedSourceName(), e);
    }
  }

  /**
   * Gets the resource associated with this class.
   *
   * @return the resource
   */
  public ResourceList getResourceBundle() {
    return resourceList;
  }

  @Override
  protected String branchMessage() {
    return "Processing " + this.getTarget();
  }

  /**
   * Find the creator associated with the given method, and delegate the
   * creation of the method body to it.
   *
   * @param logger TreeLogger instance for logging
   * @param method method to be generated
   * @param locale locale to generate
   * @throws UnableToCompleteException
   */
  protected void delegateToCreator(TreeLogger logger, JMethod method,
      GwtLocale locale) throws UnableToCompleteException {
    AbstractMethodCreator methodCreator = getMethodCreator(logger, method);
    String key = getKey(logger, method);
    if (key == null) {
      logger.log(TreeLogger.ERROR, "Unable to get or compute key for method "
          + method.getName(), null);
      throw new UnableToCompleteException();
    }
    methodCreator.createMethodFor(logger, method, key, resourceList, locale);
  }

  /**
   * Returns a resource key given a method name.
   *
   * @param logger TreeLogger instance for logging
   * @param method method to get key for
   * @return the key to use for resource lookups or null if unable to get or
   *         compute the key
   */
  protected String getKey(TreeLogger logger, JMethod method) {
    Key key = method.getAnnotation(Key.class);
    if (key != null) {
      return key.value();
    }
    return AnnotationsResource.getKey(logger, keyGenerator, method, isConstants);
  }
}
