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

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
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.jjs.JavaToJavaScriptCompiler;
import com.google.gwt.dev.jjs.PermutationResult;
import com.google.gwt.dev.jjs.UnifiedAst;
import com.google.gwt.dev.util.FileBackedObject;
import com.google.gwt.dev.util.MemoryBackedObject;
import com.google.gwt.dev.util.PerfCounter;
import com.google.gwt.dev.util.PersistenceBackedObject;
import com.google.gwt.dev.util.Util;
import com.google.gwt.dev.util.arg.ArgHandlerLocalWorkers;
import com.google.gwt.dev.util.arg.OptionLocalWorkers;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.util.tools.ArgHandlerString;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * Performs the second phase of compilation, converting the Precompile's AST
 * into JavaScript outputs.
 */
public class CompilePerms {

  /**
   * Options for CompilePerms.
   */
  public interface CompilePermsOptions extends PrecompileTaskOptions,
      OptionLocalWorkers, OptionPerms {
  }

  /**
   * Handles options for which permutations to compile.
   */
  public interface OptionPerms {
    /**
     * Gets the ordered set of permutations to compile. Returns a zero-length
     * array if all permutations should be compiled.
     */
    int[] getPermsToCompile();

    /**
     * Adds another permutation to compile.
     */
    void setPermsToCompile(int[] permsToCompile);
  }

  /**
   * Argument handler for specifying the which perms to run.
   */
  protected static final class ArgHandlerPerms extends ArgHandlerString {
    private final OptionPerms option;

    public ArgHandlerPerms(OptionPerms option) {
      this.option = option;
    }

    @Override
    public String getPurpose() {
      return "Comma-delimited list of 0-based permutations to compile";
    }

    @Override
    public String getTag() {
      return "-perms";
    }

    @Override
    public String[] getTagArgs() {
      return new String[]{"permlist"};
    }

    @Override
    public boolean setString(String str) {
      String[] split = str.split(",");
      if (split.length < 1) {
        System.err.println(getTag()
            + " requires a comma-delimited list of integers");
        return false;
      }

      SortedSet<Integer> permSet = new TreeSet<Integer>();
      for (String item : split) {
        try {
          int value = Integer.parseInt(item);
          if (value < 0) {
            System.err.println(getTag() + " error: negative value '" + value
                + "' is not allowed");
            return false;
          }
          permSet.add(value);
        } catch (NumberFormatException e) {
          System.err.println(getTag()
              + " requires a comma-delimited list of integers; '" + item
              + "' is not an integer");
          return false;
        }
      }
      int[] permsToCompile = new int[permSet.size()];
      int i = 0;
      for (int perm : permSet) {
        permsToCompile[i++] = perm;
      }
      option.setPermsToCompile(permsToCompile);
      return true;
    }
  }

  static class ArgProcessor extends PrecompileTaskArgProcessor {
    public ArgProcessor(CompilePermsOptions options) {
      super(options);
      registerHandler(new ArgHandlerPerms(options));
      registerHandler(new ArgHandlerLocalWorkers(options));
    }

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

  /**
   * Concrete class to implement compiler perm options.
   */
  static class CompilePermsOptionsImpl extends PrecompileTaskOptionsImpl implements
      CompilePermsOptions {

    private int localWorkers;
    private int[] permsToCompile;

    public CompilePermsOptionsImpl() {
    }

    public CompilePermsOptionsImpl(CompilePermsOptions other) {
      copyFrom(other);
    }

    public void copyFrom(CompilePermsOptions other) {
      super.copyFrom(other);
      setPermsToCompile(other.getPermsToCompile());
      setLocalWorkers(other.getLocalWorkers());
    }

    @Override
    public int getLocalWorkers() {
      return localWorkers;
    }

    @Override
    public int[] getPermsToCompile() {
      return (permsToCompile == null) ? null : permsToCompile.clone();
    }

    @Override
    public void setLocalWorkers(int localWorkers) {
      this.localWorkers = localWorkers;
    }

    @Override
    public void setPermsToCompile(int[] permsToCompile) {
      this.permsToCompile = (permsToCompile == null) ? null
          : permsToCompile.clone();
    }
  }

  /**
   * Compile a single permutation.
   *
   * @throws UnableToCompleteException if the permutation compile fails
   */
  public static PermutationResult compile(TreeLogger logger, CompilerContext compilerContext,
      Permutation permutation, UnifiedAst unifiedAst) throws UnableToCompleteException {
    return JavaToJavaScriptCompiler.compilePermutation(unifiedAst, logger, compilerContext,
        permutation);
  }

  /**
   * Compile multiple permutations.
   */
  public static void compile(TreeLogger logger, CompilerContext compilerContext,
      Precompilation precompilation, Permutation[] perms, int localWorkers,
      List<PersistenceBackedObject<PermutationResult>> resultFiles)
      throws UnableToCompleteException {
    final TreeLogger branch = logger.branch(TreeLogger.INFO,
        "Compiling " + perms.length + " permutation" + (perms.length > 1 ? "s" : ""));
    PermutationWorkerFactory.compilePermutations(
        branch, compilerContext, precompilation, perms, localWorkers, resultFiles);
    logger.log(TreeLogger.INFO, "Compile of permutations succeeded");
  }

  public static void main(String[] args) {
    int exitCode = -1;
    /*
     * 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 CompilePermsOptions options = new CompilePermsOptionsImpl();
    if (new ArgProcessor(options).processArgs(args)) {
      CompileTask task = new CompileTask() {
        @Override
        public boolean run(TreeLogger logger) throws UnableToCompleteException {
          return new CompilePerms(options).run(logger);
        }
      };
      if (CompileTaskRunner.runWithAppropriateLogger(options, task)) {
        // Exit w/ success code.
        exitCode = 0;
      }
    }
    PerfCounter.print();
    System.exit(exitCode);
  }

  public static List<PersistenceBackedObject<PermutationResult>> makeResultFiles(
      File compilerWorkDir, Permutation[] perms, PrecompileTaskOptions options) {
    List<PersistenceBackedObject<PermutationResult>> toReturn = Lists.newArrayList();
    for (int i = 0; i < perms.length; ++i) {
      if (options.isIncrementalCompileEnabled()) {
        toReturn.add(new MemoryBackedObject<PermutationResult>(PermutationResult.class));
      } else {
        File f = makePermFilename(compilerWorkDir, perms[i].getId());
        toReturn.add(new FileBackedObject<PermutationResult>(PermutationResult.class, f));
      }
    }
    return toReturn;
  }

  /**
   * Return the filename corresponding to the given permutation number,
   * one-based.
   */
  static File makePermFilename(File compilerWorkDir, int permNumber) {
    return new File(compilerWorkDir, "permutation-" + permNumber + ".js");
  }

  static PrecompilationResult readPrecompilationFile(TreeLogger logger,
      File precompilationFile) {
    PrecompilationResult precompileResults = null;
    try {
      precompileResults = Util.readFileAsObject(precompilationFile,
          PrecompilationResult.class);
    } catch (IOException e) {
      logger.log(TreeLogger.ERROR, "Failed to read "
          + precompilationFile + "\nHas Precompile been run?");
    } catch (ClassNotFoundException e) {
      logger.log(TreeLogger.ERROR, "Failed to read "
          + precompilationFile, e);
    }
    return precompileResults;
  }

  /**
   * Choose the subset of requested permutations that correspond to the
   * indicated precompilation.
   */
  static Permutation[] selectPermutationsForPrecompilation(
      int[] permsToRun, Precompilation precompilation) {
    if (permsToRun == null) {
      // Special case: compile everything.
      return precompilation.getPermutations();
    }
    ArrayList<Permutation> subPermsList = new ArrayList<Permutation>();
    for (int id : permsToRun) {
      for (Permutation perm : precompilation.getPermutations()) {
        if (perm.getId() == id) {
          subPermsList.add(perm);
        }
      }
    }
    return subPermsList.toArray(new Permutation[subPermsList.size()]);
  }

  private CompilerContext compilerContext;
  private CompilerContext.Builder compilerContextBuilder;
  private final CompilePermsOptionsImpl options;

  public CompilePerms(CompilePermsOptions options) {
    this.options = new CompilePermsOptionsImpl(options);
    compilerContextBuilder = new CompilerContext.Builder();
  }

  public boolean run(TreeLogger logger) throws UnableToCompleteException {
    for (String moduleName : options.getModuleNames()) {
      /*
       * NOTE: as a special case, null means "compile everything".
       */
      int[] permsToRun = options.getPermsToCompile();

      File compilerWorkDir = options.getCompilerWorkDir(moduleName);
      File precompilationFile = new File(compilerWorkDir,
          Precompile.PRECOMPILE_FILENAME);

      PrecompilationResult precompileResults = readPrecompilationFile(logger,
          precompilationFile);

      if (precompileResults instanceof PrecompileTaskOptions) {
        PrecompileTaskOptions precompilationOptions = (PrecompileTaskOptions) precompileResults;
        if (!precompileAndCompile(logger, moduleName, compilerWorkDir,
            precompilationOptions)) {
          return false;
        }
      } else {
        ModuleDef module = ModuleDefLoader.loadFromClassPath(logger, moduleName);
        compilerContext = compilerContextBuilder.options(options).module(module).build();
        Precompilation precompilation = (Precompilation) precompileResults;
        // Choose which permutations go with this permutation
        Permutation[] subPerms = selectPermutationsForPrecompilation(
            permsToRun, precompilation);

        List<PersistenceBackedObject<PermutationResult>> resultFiles = makeResultFiles(
            compilerWorkDir, subPerms, options);
        compile(logger, compilerContext, precompilation, subPerms,
            options.getLocalWorkers(), resultFiles);
      }
    }

    return true;
  }

  /**
   * Run both a precompile and a compile with the given precompilation options.
   */
  private boolean precompileAndCompile(TreeLogger logger, String moduleName,
      File compilerWorkDir, PrecompileTaskOptions precompilationOptions)
      throws UnableToCompleteException {
    precompilationOptions.setGenDir(null);

    ModuleDef module = ModuleDefLoader.loadFromClassPath(logger, moduleName);
    compilerContext = compilerContextBuilder.options(precompilationOptions).module(module).build();
    PropertyCombinations allPermutations = new PropertyCombinations(
        module.getProperties(), module.getActiveLinkerNames());
    List<PropertyCombinations> collapsedPermutations = allPermutations.collapseProperties();
    int[] perms = options.getPermsToCompile();
    if (perms == null) {
      perms = new int[collapsedPermutations.size()];
      for (int i = 0; i < perms.length; ++i) {
        perms[i] = i;
      }
    }

    logger = logger.branch(TreeLogger.INFO, "Compiling " + perms.length
        + " permutation" + (perms.length > 1 ? "s" : ""));
    for (int permId : perms) {
      /*
       * TODO(spoon,scottb): move Precompile out of the loop to run only once
       * per shard. Then figure out a way to avoid copying the generated
       * artifacts into every perm result on a shard.
       */
      PropertyCombinations onePerm = collapsedPermutations.get(permId);

      Precompilation precompilation =
          Precompile.precompile(logger, compilerContext, permId, onePerm);
      if (precompilation == null) {
        return false;
      }

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

      // TODO: move to precompile() after params are refactored
      if (!options.shouldSaveSource() && !embedSourcesContent) {
        precompilation.removeSourceArtifacts(logger);
      }

      // Choose which permutations go with this precompilation
      Permutation[] subPerms = selectPermutationsForPrecompilation(
          new int[]{permId}, precompilation);
      assert subPerms.length == 1;

      PermutationResult permResult =
          compile(logger, compilerContext, subPerms[0], precompilation.getUnifiedAst());
      Link.linkOnePermutationToJar(logger, compilerContext.getModule(),
          compilerContext.getPublicResourceOracle(), precompilation.getGeneratedArtifacts(),
          permResult, makePermFilename(compilerWorkDir, permId), compilerContext.getOptions());
    }

    logger.log(TreeLogger.INFO, "Compile of permutations succeeded");
    return true;
  }
}
