/*
 * 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.Linker;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.ModuleMetricsArtifact;
import com.google.gwt.core.ext.linker.PrecompilationMetricsArtifact;
import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
import com.google.gwt.core.ext.typeinfo.JClassType;
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.javac.CompilationState;
import com.google.gwt.dev.javac.CompilationUnit;
import com.google.gwt.dev.jjs.JavaToJavaScriptCompiler;
import com.google.gwt.dev.jjs.PrecompilationContext;
import com.google.gwt.dev.jjs.UnifiedAst;
import com.google.gwt.dev.util.CollapsedPropertyKey;
import com.google.gwt.dev.util.Memory;
import com.google.gwt.dev.util.collect.Lists;
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.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;

/**
 * Performs the first phase of compilation, generating the set of permutations
 * to compile, and a ready-to-compile AST.
 */
public class Precompile {
  /**
   * The file name for the max number of permutations output as plain text.
   */
  static final String PERM_COUNT_FILENAME = "permCount.txt";

  static final String PRECOMPILE_FILENAME = Precompile.PRECOMPILE_FILENAME_PREFIX
      + Precompile.PRECOMPILE_FILENAME_SUFFIX;

  /**
   * The file name for the serialized AST artifact from the Precompile step.
   * Sometimes this file is overloaded and only contains a PrecompileOptions
   * object to indicate that precompilation should run inside the CompilePerms
   * step.
   */
  static final String PRECOMPILE_FILENAME_PREFIX = "precompilation";

  static final String PRECOMPILE_FILENAME_SUFFIX = ".ser";

  /**
   * 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 PrecompileTaskOptions options = new PrecompileTaskOptionsImpl();
    boolean success = false;
    if (new PrecompileTaskArgProcessor(options).processArgs(args)) {
      CompileTask task = new CompileTask() {
        @Override
        public boolean run(TreeLogger logger) throws UnableToCompleteException {
          return new Precompile(options).run(logger);
        }
      };
      if (CompileTaskRunner.runWithAppropriateLogger(options, task)) {
        // Exit w/ success code.
        success = true;
      }
    }
    System.exit(success ? 0 : 1);
  }

  /**
   * Precompiles the given module.
   *
   * @param logger a logger to use
   * @param compilerContext shared read only compiler state
   * @return the precompilation
   * @throws UnableToCompleteException
   */
  public static Precompilation precompile(TreeLogger logger, CompilerContext compilerContext)
      throws UnableToCompleteException {
    PropertyCombinations allPermutations = new PropertyCombinations(
        compilerContext.getModule().getProperties(),
        compilerContext.getModule().getActiveLinkerNames());
    if (compilerContext.getOptions().isIncrementalCompileEnabled() && allPermutations.size() > 1) {
      logger.log(TreeLogger.ERROR,
          "Current binding properties are expanding to more than one permutation "
          + "but per-file compilation requires that each compile operate on only one permutation.");
      throw new UnableToCompleteException();
    }
    return precompile(logger, compilerContext, 0, allPermutations);
  }

  /**
   * Validates the given module can be compiled.
   *
   * @param logger a logger to use
   * @param compilerContext shared read only compiler state
   */
  public static boolean validate(TreeLogger logger, CompilerContext compilerContext) {
    try (SimpleEvent ignored = new SimpleEvent("Validate")) {
      ModuleDef module = compilerContext.getModule();
      PrecompileTaskOptions jjsOptions = compilerContext.getOptions();
      CompilationState compilationState = module.getCompilationState(logger, compilerContext);
      if (jjsOptions.isStrict() && compilationState.hasErrors()) {
        abortDueToStrictMode(logger);
      }
      String[] declEntryPts = module.getEntryPointTypeNames();
      String[] additionalRootTypes = null;
      if (declEntryPts.length == 0) {
        // No declared entry points, just validate all visible classes.
        Collection<CompilationUnit> compilationUnits = compilationState.getCompilationUnits();
        additionalRootTypes = new String[compilationUnits.size()];
        int i = 0;
        for (CompilationUnit unit : compilationUnits) {
          additionalRootTypes[i++] = unit.getTypeName();
        }
      }

      PrecompilationContext precompilationContext = PrecompilationContextCreator.create(
          compilerContext, compilationState, new PropertyCombinations(module.getProperties(),
              module.getActiveLinkerNames()), declEntryPts, additionalRootTypes, null);
      // Allow GC later.
      compilationState = null;
      JavaToJavaScriptCompiler.precompile(logger, compilerContext, precompilationContext);
      return true;

    } catch (UnableToCompleteException e) {
      // Already logged.
      return false;
    }
  }

  /**
   * Create a list of all possible permutations configured for this module after
   * collapsing soft permutations.
   */
  static List<PropertyCombinations> getCollapsedPermutations(ModuleDef module) {
    PropertyCombinations allPermutations =
        new PropertyCombinations(module.getProperties(), module.getActiveLinkerNames());
    List<PropertyCombinations> collapsedPermutations = allPermutations.collapseProperties();
    return collapsedPermutations;
  }

  static Precompilation precompile(TreeLogger logger, CompilerContext compilerContext,
      int permutationBase, PropertyCombinations allPermutations) {
    return precompile(logger, compilerContext, permutationBase,
        allPermutations, ManagementFactory.getRuntimeMXBean().getStartTime());
  }

  static Precompilation precompile(TreeLogger logger, CompilerContext compilerContext,
      int permutationBase, PropertyCombinations propertyCombinations, long startTimeMilliseconds) {

    // This initializes the Java2D library in a thread so that the main program
    // doesn't block when the library is accessed for the first time.
    new GraphicsInitThread().start();

    try (SimpleEvent ignored = new SimpleEvent("Precompile")) {
      ModuleDef module = compilerContext.getModule();
      PrecompileTaskOptions jjsOptions = compilerContext.getOptions();
      if (jjsOptions.isIncrementalCompileEnabled()) {
        compilerContext.getMinimalRebuildCache().recordDiskSourceResources(module);
        compilerContext.getMinimalRebuildCache().recordBuildResources(module);
      }
      CompilationState compilationState = module.getCompilationState(logger, compilerContext);
      if (jjsOptions.isStrict() && compilationState.hasErrors()) {
        abortDueToStrictMode(logger);
      }

      List<String> initialTypeOracleTypes = new ArrayList<String>();
      if (jjsOptions.isCompilerMetricsEnabled()) {
        for (JClassType type : compilationState.getTypeOracle().getTypes()) {
          initialTypeOracleTypes.add(type.getPackage().getName() + "." + type.getName());
        }
      }

      // Track information about the module load including initial type
      // oracle build for diagnostic purposes.
      long moduleLoadFinished = System.currentTimeMillis();

      String[] declEntryPts = module.getEntryPointTypeNames();
      if (declEntryPts.length == 0) {
        logger.log(TreeLogger.ERROR, "Module has no entry points defined", null);
        throw new UnableToCompleteException();
      }

      PrecompilationMetricsArtifact precompilationMetrics =
          jjsOptions.isCompilerMetricsEnabled()
              ? new PrecompilationMetricsArtifact(permutationBase) : null;

      PrecompilationContext precompilationContext = PrecompilationContextCreator.create(
          compilerContext, compilationState, propertyCombinations, declEntryPts, null,
          precompilationMetrics);
      // Allow GC later.
      compilationState = null;

      UnifiedAst unifiedAst =
          JavaToJavaScriptCompiler.precompile(logger, compilerContext, precompilationContext);

      if (jjsOptions.isCompilerMetricsEnabled()) {
        ModuleMetricsArtifact moduleMetrics = new ModuleMetricsArtifact();
        moduleMetrics.setSourceFiles(module.getAllSourceFiles());
        // The initial type list has to be gathered before the call to
        // precompile().
        moduleMetrics.setInitialTypes(initialTypeOracleTypes);
        // The elapsed time in ModuleMetricsArtifact represents time
        // which could be done once for all permutations.
        moduleMetrics.setElapsedMilliseconds(moduleLoadFinished - startTimeMilliseconds);
        unifiedAst.setModuleMetrics(moduleMetrics);
      }

      // Merge all identical permutations together.
      List<Permutation> permutations =
          new ArrayList<Permutation>(Arrays.asList(precompilationContext.getPermutations()));

      mergeCollapsedPermutations(permutations);

      // Sort the permutations by an ordered key to ensure determinism.
      SortedMap<RebindAnswersPermutationKey, Permutation> merged =
          new TreeMap<RebindAnswersPermutationKey, Permutation>();
      SortedSet<String> liveRebindRequests = unifiedAst.getRebindRequests();
      for (Permutation permutation : permutations) {
        // Construct a key for the live rebind answers.
        RebindAnswersPermutationKey key =
            new RebindAnswersPermutationKey(permutation, liveRebindRequests);
        if (merged.containsKey(key)) {
          Permutation existing = merged.get(key);
          existing.mergeFrom(permutation, liveRebindRequests);
        } else {
          merged.put(key, permutation);
        }
      }

      permutations.clear();
      permutations.addAll(merged.values());

      if (jjsOptions.isCompilerMetricsEnabled()) {
        int[] ids = new int[propertyCombinations.size()];
        for (int i = 0; i < propertyCombinations.size(); i++) {
          ids[i] = permutationBase + i;
        }
        precompilationMetrics.setPermutationIds(ids);
        // TODO(zundel): Right now this double counts module load and
        // precompile time. It correctly counts the amount of time spent
        // in this process. The elapsed time in ModuleMetricsArtifact
        // represents time which could be done once for all permutations.
        precompilationMetrics.setElapsedMilliseconds(System.currentTimeMillis()
            - startTimeMilliseconds);
        unifiedAst.setPrecompilationMetrics(precompilationMetrics);
      }
      return new Precompilation(unifiedAst, permutations, permutationBase,
          precompilationContext.getGeneratorArtifacts());
    } catch (UnableToCompleteException e) {
      // We intentionally don't pass in the exception here since the real
      // cause has been logged.
      return null;
    }
  }

  private static void abortDueToStrictMode(TreeLogger logger) throws UnableToCompleteException {
    logger.log(TreeLogger.ERROR, "Aborting compile due to errors in some input files");
    throw new UnableToCompleteException();
  }

  /**
   * This merges Permutations that can be considered equivalent by considering
   * their collapsed properties. The list passed into this method may have
   * elements removed from it.
   */
  private static void mergeCollapsedPermutations(List<Permutation> permutations) {
    if (permutations.size() < 2) {
      return;
    }

    // See the doc for CollapsedPropertyKey
    SortedMap<CollapsedPropertyKey, List<Permutation>> mergedByCollapsedProperties =
        new TreeMap<CollapsedPropertyKey, List<Permutation>>();

    // This loop creates the equivalence sets
    for (Iterator<Permutation> it = permutations.iterator(); it.hasNext();) {
      Permutation entry = it.next();
      CollapsedPropertyKey key = new CollapsedPropertyKey(entry);

      List<Permutation> equivalenceSet = mergedByCollapsedProperties.get(key);
      if (equivalenceSet == null) {
        equivalenceSet = Lists.create();
      } else {
        // Mutate list
        it.remove();
        equivalenceSet = Lists.add(equivalenceSet, entry);
      }
      mergedByCollapsedProperties.put(key, equivalenceSet);
    }

    // This loop merges the Permutations together
    for (Map.Entry<CollapsedPropertyKey, List<Permutation>> entry : mergedByCollapsedProperties
        .entrySet()) {
      Permutation mergeInto = entry.getKey().getPermutation();

      /*
       * Merge the deferred-binding properties once we no longer need the
       * PropertyOracle data from the extra permutations.
       */
      for (Permutation mergeFrom : entry.getValue()) {
        mergeInto.mergeRebindsFromCollapsed(mergeFrom);
      }
    }

    // Renumber the Permutations
    for (int i = 0, j = permutations.size(); i < j; i++) {
      permutations.set(i, new Permutation(i, permutations.get(i)));
    }
  }

  private final PrecompileTaskOptionsImpl options;

  private CompilerContext compilerContext;

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

  public Precompile(PrecompileTaskOptions options) {
    this.options = new PrecompileTaskOptionsImpl(options);
    compilerContext = compilerContextBuilder.options(options).build();
  }

  public boolean run(TreeLogger logger) throws UnableToCompleteException {
    for (String moduleName : options.getModuleNames()) {
      File compilerWorkDir = options.createCompilerWorkDir(logger, moduleName);

      File precompilationFile = new File(compilerWorkDir, PRECOMPILE_FILENAME);

      ModuleDef module = ModuleDefLoader.loadFromClassPath(logger, moduleName);
      compilerContext = compilerContextBuilder.module(module).build();

      StandardLinkerContext linkerContext = new StandardLinkerContext(
          TreeLogger.NULL, module, compilerContext.getPublicResourceOracle(), options.getOutput());

      boolean generateOnShards = true;

      if (!options.isEnabledGeneratingOnShards()) {
        logger.log(TreeLogger.INFO, "Precompiling on the start node");
        generateOnShards = false;
      } else if (!linkerContext.allLinkersAreShardable()) {
        TreeLogger legacyLinkersLogger =
            logger.branch(TreeLogger.INFO,
                "Precompiling on the start node, because some linkers are not updated");
        if (legacyLinkersLogger.isLoggable(TreeLogger.INFO)) {
          for (Linker linker : linkerContext.findUnshardableLinkers()) {
            legacyLinkersLogger.log(TreeLogger.INFO, "Linker"
                + linker.getClass().getCanonicalName() + " is not updated");
          }
        }
        generateOnShards = false;
      } else if (options.isValidateOnly()) {
        // Don't bother running on shards for just a validation run
        generateOnShards = false;
      }

      if (generateOnShards) {
        /*
         * Pre-precompile. Count the permutations and plan to do a real
         * precompile in the CompilePerms shards.
         */
        TreeLogger branch =
            logger.branch(TreeLogger.INFO, "Precompiling (minimal) module " + module.getName());
        try (SimpleEvent ignored = new SimpleEvent("Write PrecompileOptions to disk");
             OutputStream stream = new FileOutputStream(precompilationFile);
             ObjectOutputStream objectStream = new ObjectOutputStream(stream)) {
          objectStream.writeObject(options);
        } catch (IOException e1) {
          logger.log(TreeLogger.ERROR, "Unable to write file: "
              + precompilationFile.getAbsolutePath(), e1);
          throw new UnableToCompleteException();
        }
        int numPermutations =
            new PropertyCombinations(module.getProperties(), module.getActiveLinkerNames())
                .collapseProperties().size();
        try {
          Files.writeString(new File(compilerWorkDir, PERM_COUNT_FILENAME).toPath(),
              String.valueOf(numPermutations));
        } catch (IOException e) {
          logger.log(TreeLogger.ERROR, "Unable to write permutation count to "
              + PERM_COUNT_FILENAME, e);
          throw new UnableToCompleteException();
        }
        if (branch.isLoggable(TreeLogger.INFO)) {
          branch.log(TreeLogger.INFO,
              "Precompilation (minimal) succeeded, number of permutations: " + numPermutations);
        }
      } else {
        if (options.isValidateOnly()) {
          TreeLogger branch =
              logger.branch(TreeLogger.INFO, "Validating compilation " + module.getName());
          if (!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());

          Precompilation precompilation = precompile(branch, compilerContext);
          if (precompilation == null) {
            branch.log(TreeLogger.ERROR, "Precompilation failed");
            return false;
          }
          // TODO: move to precompile() after params are refactored
          if (!options.shouldSaveSource() && !module.shouldEmbedSourceMapContents()) {
            precompilation.removeSourceArtifacts(logger);
          }
          try (SimpleEvent ignored = new SimpleEvent("Write Precompilation to disk");
               OutputStream stream = new FileOutputStream(precompilationFile);
               ObjectOutputStream objectStream = new ObjectOutputStream(stream)) {
            objectStream.writeObject(precompilation);
          } catch (IOException e1) {
            logger.log(TreeLogger.ERROR, "Unable to write file: "
                + precompilationFile.getAbsolutePath(), e1);
            throw new UnableToCompleteException();
          }

          int permsPrecompiled = precompilation.getPermutations().length;
          try {
            Files.writeString(new File(compilerWorkDir, PRECOMPILE_FILENAME).toPath(),
                String.valueOf(permsPrecompiled));
          } catch (IOException e) {
            branch.log(TreeLogger.ERROR, "Unable to write precompilation count to "
                + PRECOMPILE_FILENAME, e);
            throw new UnableToCompleteException();
          }
          if (branch.isLoggable(TreeLogger.INFO)) {
            branch.log(TreeLogger.INFO, "Precompilation succeeded, number of permutations: "
                + permsPrecompiled);
          }
        }
      }
    }
    return true;
  }
}
