/*
 * Copyright 2015 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.template.soy.jbcsrc;

import com.google.common.base.Optional;
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteSink;
import com.google.common.io.CharStreams;
import com.google.template.soy.base.internal.SoyFileKind;
import com.google.template.soy.base.internal.SoyFileSupplier;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.jbcsrc.shared.CompiledTemplates;
import com.google.template.soy.jbcsrc.shared.Names;
import com.google.template.soy.soytree.SoyFileNode;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.TemplateRegistry;

import java.io.IOException;
import java.io.OutputStream;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;

/**
 * The entry point to the {@code jbcsrc} compiler.
 */
public final class BytecodeCompiler {
  private static final Logger logger = Logger.getLogger(BytecodeCompiler.class.getName());
  /**
   * Compiles all the templates in the given registry.
   *
   * @param registry All the templates to compile
   * @param developmentMode Whether or not we are in development mode.  In development mode we 
   *    compile classes lazily
   * @param reporter The error reporter
   * @return CompiledTemplates or {@code absent()} if compilation fails, in which case errors will
   *     have been reported to the error reporter.
   */
  public static Optional<CompiledTemplates> compile(
      final TemplateRegistry registry, boolean developmentMode, ErrorReporter reporter) {
    final Stopwatch stopwatch = Stopwatch.createStarted();
    ErrorReporter.Checkpoint checkpoint = reporter.checkpoint();
    checkForUnsupportedFeatures(registry, reporter);
    if (reporter.errorsSince(checkpoint)) {
      return Optional.absent();
    }
    CompiledTemplateRegistry compilerRegistry = new CompiledTemplateRegistry(registry);
    if (developmentMode) {
      CompiledTemplates templates =
          new CompiledTemplates(
              compilerRegistry.getDelegateTemplateNames(),
              new CompilingClassLoader(compilerRegistry));
      // TODO(lukes): consider spawning a thread to load all the generated classes in the background
      return Optional.of(templates);
    }
    // TODO(lukes): once most internal users have moved to precompilation eliminate this and just
    // use the 'developmentMode' path above.  This hybrid only makes sense for production services
    // that are doing runtime compilation.  Hopefully, this will become an anomaly.
    List<ClassData> classes =
        compileTemplates(
            compilerRegistry,
            reporter,
            new CompilerListener<List<ClassData>>() {
              final List<ClassData> compiledClasses = new ArrayList<>();
              int numBytes = 0;
              int numFields = 0;
              int numDetachStates = 0;

              @Override
              public void onCompile(ClassData clazz) {
                numBytes += clazz.data().length;
                numFields += clazz.numberOfFields();
                numDetachStates += clazz.numberOfDetachStates();
                compiledClasses.add(clazz);
              }

              @Override
              public List<ClassData> getResult() {
                logger.log(
                    Level.INFO,
                    "Compilation took {0}\n"
                        + "     templates: {1}\n"
                        + "       classes: {2}\n"
                        + "         bytes: {3}\n"
                        + "        fields: {4}\n"
                        + "  detachStates: {5}",
                        new Object[] {
                            stopwatch.toString(),
                            registry.getAllTemplates().size(),
                            compiledClasses.size(),
                            numBytes,
                            numFields,
                            numDetachStates
                    });
                return compiledClasses;
              }
            });
    if (reporter.errorsSince(checkpoint)) {
      return Optional.absent();
    }
    CompiledTemplates templates =
        new CompiledTemplates(
            compilerRegistry.getDelegateTemplateNames(), new MemoryClassLoader(classes));
    stopwatch.reset().start();
    templates.loadAll(compilerRegistry.getTemplateNames());
    logger.log(Level.INFO, "Loaded all classes in {0}", stopwatch);
    return Optional.of(templates);
  }

  /**
   * Compiles all the templates in the given registry to a jar file written to the given output
   * stream.
   *
   * <p>If errors are encountered, the error reporter will be updated and we will return.  The
   * contents of any data written to the sink at that point are undefined.
   *
   * @param registry All the templates to compile
   * @param reporter The error reporter
   * @param sink The output sink to write the JAR to.
   */
  public static void compileToJar(TemplateRegistry registry, ErrorReporter reporter, ByteSink sink)
      throws IOException {
    ErrorReporter.Checkpoint checkpoint = reporter.checkpoint();
    checkForUnsupportedFeatures(registry, reporter);
    if (reporter.errorsSince(checkpoint)) {
      return;
    }
    CompiledTemplateRegistry compilerRegistry = new CompiledTemplateRegistry(registry);
    if (reporter.errorsSince(checkpoint)) {
      return;
    }
    try (OutputStream stream = sink.openStream();
        JarOutputStream jarOutput = new DeterministicJarOutputStream(stream, getJarManifest())) {
      compileTemplates(
          compilerRegistry,
          reporter,
          new CompilerListener<Void>() {
            @Override
            void onCompile(ClassData clazz) throws IOException {
              jarOutput.putNextEntry(new ZipEntry(clazz.type().internalName() + ".class"));
              jarOutput.write(clazz.data());
              jarOutput.closeEntry();
            }
          });
    }
  }

  /**
   * Writes the source files out to a {@code -src.jar}.  This places the soy files at the same
   * classpath relative location as their generated classes.  Ultimately this can be used by
   * debuggers for source level debugging.
   *
   * <p>It is a little weird that the relative locations of the generated classes are not identical
   * to the input source files.  This is due to the disconnect between java packages and soy
   * namespaces.  We should consider using the soy namespace directly as a java package in the
   * future.
   *
   * @param registry  All the templates in the current compilation unit
   * @param files The source files by file path
   * @param sink The source to write the jar file
   */
  public static void writeSrcJar(
      TemplateRegistry registry,
      ImmutableMap<String, SoyFileSupplier> files,
      ByteSink sink) throws IOException {
    Set<SoyFileNode> seenFiles = new HashSet<>();
    try (OutputStream stream = sink.openStream();
        JarOutputStream jarOutput = new DeterministicJarOutputStream(stream, getJarManifest())) {
      for (TemplateNode template : registry.getAllTemplates()) {
        SoyFileNode file = template.getParent();
        if (file.getSoyFileKind() == SoyFileKind.SRC && seenFiles.add(file)) {
          String namespace = file.getNamespace();
          String fileName = file.getFileName();
          jarOutput.putNextEntry(new ZipEntry(Names.javaFileName(namespace, fileName)));
          copyFileToOutput(files.get(file.getFilePath()), jarOutput);
          jarOutput.closeEntry();
        }
      }
    }
  }

  private static final class DeterministicJarOutputStream extends JarOutputStream {
    DeterministicJarOutputStream(OutputStream outputStream, Manifest manifest) throws IOException {
      super(outputStream, manifest);
    }

    @Override
    public void putNextEntry(ZipEntry ze) throws IOException {
      ze.setTime(0); // set an explicit timestamp to zero so we generate deterministic outputs
      super.putNextEntry(ze);
    }
  }

  /** Copies the file to the output stream */
  private static void copyFileToOutput(SoyFileSupplier from, OutputStream to)
      throws IOException {
    // 'from' contains a Reader which allows streaming reads of characters and 'to' is an 
    // OutputStream which allows for streaming writes of bytes.  This disconnect means we need to do
    // some character encoding.  The classic way to do this is to use OutputStreamWriter to wrap the
    // outputStream and apply an encoder.  This introduces some wierdness because OutputStreamWriter
    // can hold on to a few bytes to deal with unmatched surrogate pairs.  So we would need to
    // close/flush it inorder to not corrupt the files.  This is undesirable since the output is
    // actually a JarOutputStream and we are writing multiple files (we would over flush).  So 
    // instead we do the naive thing and read the whole file as a string, convert the whole string
    // to a byte array and then write the whole byte array.
    //
    // The real fix is to avoid the Reader and add methods to SoyFileSupplier to give us a 
    // ByteSource then we can avoid the error prone decode/encode dance.
    String file;
    try (Reader contents = from.open()) {
      file = CharStreams.toString(contents);
    }
    to.write(file.getBytes(StandardCharsets.UTF_8));
  }

  /** Returns a simple jar manifest. */
  private static Manifest getJarManifest() {
    Manifest mf = new Manifest();
    mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
    mf.getMainAttributes().put(new Attributes.Name("Created-By"), "soy");
    return mf;
  }

  private static void checkForUnsupportedFeatures(TemplateRegistry registry,
      ErrorReporter errorReporter) {
    UnsupportedFeatureReporter reporter = new UnsupportedFeatureReporter(errorReporter);
    for (TemplateNode node : registry.getAllTemplates()) {
      reporter.check(node);
    }
  }

  private abstract static class CompilerListener<T> {
    abstract void onCompile(ClassData newClass) throws Exception;

    T getResult() {
      return null;
    }
  }

  private static <T> T compileTemplates(
      CompiledTemplateRegistry registry,
      ErrorReporter errorReporter,
      CompilerListener<T> listener) {
    for (String name : registry.getTemplateNames()) {
      CompiledTemplateMetadata classInfo = registry.getTemplateInfoByTemplateName(name);
      if (classInfo.node().getParent().getSoyFileKind() != SoyFileKind.SRC) {
        continue; // only generate classes for sources
      }
      try {
        TemplateCompiler templateCompiler = new TemplateCompiler(registry, classInfo);
        for (ClassData clazz : templateCompiler.compile()) {
          if (Flags.DEBUG) {
            clazz.checkClass();
          }
          listener.onCompile(clazz);
        }
      // Report unexpected errors and keep going to try to collect more.
      } catch (UnexpectedCompilerFailureException e) {
        errorReporter.report(
            e.getOriginalLocation(),
            SoyErrorKind.of(
                "Unexpected error while compiling template: ''{0}''\nSoy Stack:\n{1}"
                    + "\nCompiler Stack:{2}"),
            name,
            e.printSoyStack(),
            Throwables.getStackTraceAsString(e));

      } catch (Throwable t) {
        errorReporter.report(
            classInfo.node().getSourceLocation(),
            SoyErrorKind.of("Unexpected error while compiling template: ''{0}''\n{1}"),
            name,
            Throwables.getStackTraceAsString(t));
      }
    }
    return listener.getResult();
  }

  private BytecodeCompiler() {}
}
