/*
 * 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.dev;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
import com.google.gwt.dev.CompileTaskRunner.CompileTask;
import com.google.gwt.dev.cfg.ModuleDef;
import com.google.gwt.dev.cfg.ModuleDefLoader;
import com.google.gwt.dev.cfg.PropertyCombinations;
import com.google.gwt.dev.util.Memory;
import com.google.gwt.dev.util.arg.ArgHandlerPerm;
import com.google.gwt.dev.util.arg.OptionPerm;
import com.google.gwt.dev.util.log.perf.SimpleEvent;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.List;

/**
 * Creates a ready-to-compile AST for a single permutation.
 *
 * Also collapses similar permutations and builds them together.
 */
public class PrecompileOnePerm {

  /**
   * The set of options for the precompiler.
   */
  public interface PrecompileOnePermOptions extends
      PrecompileTaskOptions, OptionPerm {
  }

  static class ArgProcessor extends PrecompileTaskArgProcessor {
    public ArgProcessor(PrecompileOnePermOptions options) {
      super(options);
      registerHandler(new ArgHandlerPerm(options));
    }

    @Override
    protected String getName() {
      return PrecompileOnePerm.class.getName();
    }
  }

  @SuppressWarnings("serial")
  static class PrecompileOnePermOptionsImpl extends
      PrecompileTaskOptionsImpl implements PrecompileOnePermOptions {

    int permToCompile = -1;

    public PrecompileOnePermOptionsImpl() {
    }

    public PrecompileOnePermOptionsImpl(PrecompileOnePermOptions other) {
      copyFrom(other);
    }

    public void copyFrom(PrecompileOnePermOptions other) {
      super.copyFrom(other);
      setPermToCompile(other.getPermToCompile());
    }

    @Override
    public int getPermToCompile() {
      return permToCompile;
    }

    @Override
    public void setPermToCompile(int permToCompile) {
      this.permToCompile = permToCompile;
    }
  }

  /**
   * Performs a command-line precompile.
   */
  public static void main(String[] args) {
    Memory.initialize();
    if (System.getProperty("gwt.jjs.dumpAst") != null) {
      System.out.println("Will dump AST to: "
          + System.getProperty("gwt.jjs.dumpAst"));
    }

    /*
     * NOTE: main always exits with a call to System.exit to terminate any
     * non-daemon threads that were started in Generators. Typically, this is to
     * shutdown AWT related threads, since the contract for their termination is
     * still implementation-dependent.
     */
    final PrecompileOnePermOptions options = new PrecompileOnePermOptionsImpl();
    boolean success = false;
    if (new ArgProcessor(options).processArgs(args)) {
      CompileTask task = new CompileTask() {
        @Override
        public boolean run(TreeLogger logger) throws UnableToCompleteException {
          return new PrecompileOnePerm(options).run(logger);
        }
      };
      if (CompileTaskRunner.runWithAppropriateLogger(options, task)) {
        // Exit w/ success code.
        success = true;
      }
    }
    System.exit(success ? 0 : 1);
  }

  /**
   * Returns a filename for the serialized AST when precompile is performed
   * separately on a per-permutation basis.
   */
  static String getPrecompileFilename(int permutationBase) {
    return Precompile.PRECOMPILE_FILENAME_PREFIX + "-" + permutationBase
        + Precompile.PRECOMPILE_FILENAME_SUFFIX;
  }

  private static boolean validateOptions(TreeLogger logger, PrecompileOnePermOptions options) {
    // Fatal Errors
    if (options.getModuleNames().size() != 1) {
      logger.log(TreeLogger.ERROR, "Expected a single module name.");
      return false;
    }
    if (options.getPermToCompile() < 0) {
      logger.log(TreeLogger.ERROR,
          "Expected argument -perm to specify the permutation to compile.");
      return false;
    }

    // Warnings
    if (!options.isEnabledGeneratingOnShards()) {
      logger.log(TreeLogger.WARN,
      "-XdisableGeneratingOnShards has no effect in PrecompileOnePerm");
    }

    return true;
  }

  private final PrecompileOnePermOptionsImpl options;

  private CompilerContext compilerContext;

  private final CompilerContext.Builder compilerContextBuilder = new CompilerContext.Builder();

  public PrecompileOnePerm(PrecompileOnePermOptions options) {
    this.options = new PrecompileOnePermOptionsImpl(options);
    compilerContext = compilerContextBuilder.options(options).build();
  }

  public boolean run(TreeLogger logger) throws UnableToCompleteException {

    if (!validateOptions(logger, options)) {
      return false;
    }

    List<String> moduleNames = options.getModuleNames();

    int permToRun = options.getPermToCompile();

    String moduleName = moduleNames.get(0);
    File compilerWorkDir = options.getCompilerWorkDir(moduleName);

    ModuleDef module = ModuleDefLoader.loadFromClassPath(logger, moduleName);
    compilerContext = compilerContextBuilder.module(module).build();
    StandardLinkerContext linkerContext = new StandardLinkerContext(
        TreeLogger.NULL, module, compilerContext.getPublicResourceOracle(), options.getOutput());

    if (!linkerContext.allLinkersAreShardable()) {
      logger.log(TreeLogger.ERROR,
          "This compilation mode requires all linkers to be shardable.");
      return false;
    }

    PrecompileTaskOptions optionsFileData = AnalyzeModule.readAnalyzeModuleOptionsFile(
        logger, compilerWorkDir);
    if (optionsFileData == null) {
      logger.log(TreeLogger.ERROR, "Couldn't find "
          + AnalyzeModule.OPTIONS_FILENAME + " in " + compilerWorkDir);
      return false;
    }
    logger.log(TreeLogger.INFO, "Precompiling only specified permutations");

    if (options.isValidateOnly()) {
      TreeLogger branch = logger.branch(TreeLogger.INFO,
          "Validating compilation " + module.getName());
      if (!Precompile.validate(branch, compilerContext)) {
        branch.log(TreeLogger.ERROR, "Validation failed");
        return false;
      }
      branch.log(TreeLogger.INFO, "Validation succeeded");
    } else {
      TreeLogger branch = logger.branch(TreeLogger.INFO, "Precompiling module "
          + module.getName());
      if (!precompilePermutation(logger, compilerWorkDir, module, branch,
          permToRun)) {
        branch.log(TreeLogger.ERROR, "Precompile failed");
        return false;
      }
    }
    return true;
  }

  private boolean precompilePermutation(TreeLogger logger,
      File compilerWorkDir, ModuleDef module, TreeLogger branch, int permId)
      throws UnableToCompleteException {

    // Only precompile specified permutations
    List<PropertyCombinations> collapsedPermutations =
      Precompile.getCollapsedPermutations(module);

    PropertyCombinations onePerm = collapsedPermutations.get(permId);
    Precompilation precompilation = Precompile.precompile(branch, compilerContext, permId, onePerm);
    if (precompilation == null) {
      branch.log(TreeLogger.ERROR, "Precompilation failed");
      return false;
    }

    boolean embedSourcesContent = compilerContext.getModule()
        .shouldEmbedSourceMapContents();

    // TODO: precompile should do this after we get the parameter passing refactored.
    if (!options.shouldSaveSource() && embedSourcesContent) {
      precompilation.removeSourceArtifacts(logger);
    }

    File precompilationFile = new File(compilerWorkDir, getPrecompileFilename(permId));
    // The Precompilation instance must be the only, top-level object written to the stream here,
    // as it will start a second OOS stream to handle generated artifacts
    try (SimpleEvent ignored = new SimpleEvent("Write Precompilation to disk");
         OutputStream out = new FileOutputStream(precompilationFile);
         ObjectOutputStream objectStream = new ObjectOutputStream(out)) {
      objectStream.writeObject(precompilation);
    } catch (IOException e) {
      logger.log(TreeLogger.ERROR, "Unable to write file: "
          + precompilationFile.getAbsolutePath(), e);
      throw new UnableToCompleteException();
    }

    if (branch.isLoggable(TreeLogger.INFO)) {
      branch.log(TreeLogger.INFO, "Precompilation succeeded for permutation "
          + permId);
    }

    return true;
  }
}
