/*
 * 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.core.ext.linker.impl;

import com.google.gwt.core.ext.Linker;
import com.google.gwt.core.ext.LinkerContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.ConfigurationProperty;
import com.google.gwt.core.ext.linker.EmittedArtifact;
import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility;
import com.google.gwt.core.ext.linker.LinkerOrder;
import com.google.gwt.core.ext.linker.LinkerOrder.Order;
import com.google.gwt.core.ext.linker.PublicResource;
import com.google.gwt.core.ext.linker.SelectionProperty;
import com.google.gwt.dev.cfg.BindingProperty;
import com.google.gwt.dev.cfg.ModuleDef;
import com.google.gwt.dev.cfg.Script;
import com.google.gwt.dev.jjs.InternalCompilerException;
import com.google.gwt.dev.jjs.JsOutputOption;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.js.JsLiteralInterner;
import com.google.gwt.dev.js.JsNamer.IllegalNameException;
import com.google.gwt.dev.js.JsObfuscateNamer;
import com.google.gwt.dev.js.JsParser;
import com.google.gwt.dev.js.JsParserException;
import com.google.gwt.dev.js.JsPrettyNamer;
import com.google.gwt.dev.js.JsSourceGenerationVisitor;
import com.google.gwt.dev.js.JsSymbolResolver;
import com.google.gwt.dev.js.JsUnusedFunctionRemover;
import com.google.gwt.dev.js.JsVerboseNamer;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsModVisitor;
import com.google.gwt.dev.js.ast.JsProgram;
import com.google.gwt.dev.js.ast.JsScope;
import com.google.gwt.dev.resource.ResourceOracle;
import com.google.gwt.dev.util.DefaultTextOutput;
import com.google.gwt.dev.util.OutputFileSet;
import com.google.gwt.util.tools.Utility;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * An implementation of {@link LinkerContext} that is initialized from a
 * {@link ModuleDef}.
 */
public class StandardLinkerContext extends Linker implements LinkerContext {

  /**
   * Applies the {@link JsLiteralInterner} optimization to each top-level
   * function defined within a JsProgram.
   */
  private static class TopFunctionStringInterner extends JsModVisitor {

    public static boolean exec(JsProgram program) {
      TopFunctionStringInterner v = new TopFunctionStringInterner(program);
      v.accept(program);
      return v.didChange();
    }

    private final JsProgram program;

    public TopFunctionStringInterner(JsProgram program) {
      this.program = program;
    }

    @Override
    public boolean visit(JsFunction x, JsContext ctx) {
      didChange |= JsLiteralInterner.exec(program, x.getBody(), x.getScope(), true);
      return false;
    }
  }

  public static final Comparator<ConfigurationProperty> CONFIGURATION_PROPERTY_COMPARATOR =
      new Comparator<ConfigurationProperty>() {
        @Override
        public int compare(ConfigurationProperty o1, ConfigurationProperty o2) {
          return o1.getName().compareTo(o2.getName());
        }
      };

  static final Comparator<SelectionProperty> SELECTION_PROPERTY_COMPARATOR = new Comparator<SelectionProperty>() {
    @Override
    public int compare(SelectionProperty o1, SelectionProperty o2) {
      return o1.getName().compareTo(o2.getName());
    }
  };

  private final SortedSet<ConfigurationProperty> configurationProperties;

  private final JsOutputOption outputOption;

  private final List<Class<? extends Linker>> linkerClasses;
  private Linker[] linkers;
  private final Map<Class<? extends Linker>, String> linkerShortNames = new HashMap<Class<? extends Linker>, String>();
  private final String moduleFunctionName;
  private final long moduleLastModified;
  private final String moduleName;

  private final Map<String, StandardSelectionProperty> propertiesByName = new HashMap<String, StandardSelectionProperty>();

  private ResourceOracle publicResourceOracle;

  private final SortedSet<SelectionProperty> selectionProperties;
  private final ModuleDef module;

  public StandardLinkerContext(TreeLogger logger, ModuleDef module,
      ResourceOracle publicResourceOracle, JsOutputOption outputOption)
      throws UnableToCompleteException {
    logger = logger.branch(TreeLogger.DEBUG,
        "Constructing StandardLinkerContext", null);

    this.moduleFunctionName = module.getFunctionName();
    this.moduleName = module.getName();
    this.moduleLastModified = module.lastModified();
    this.publicResourceOracle = publicResourceOracle;
    this.outputOption = outputOption;
    this.module = module;

    // Sort the linkers into the order they should actually run.
    linkerClasses = new ArrayList<Class<? extends Linker>>();

    // Get all the pre-linkers first.
    for (Class<? extends Linker> linkerClass : module.getActiveLinkers()) {
      Order order = linkerClass.getAnnotation(LinkerOrder.class).value();
      if (order == null) {
        logger.log(Type.ERROR, linkerClass.getName() + " has no @LinkerOrder annotation");
        throw new UnableToCompleteException();
      }
      if (order == Order.PRE) {
        linkerClasses.add(linkerClass);
      }
    }

    // Get the primary linker.
    Class<? extends Linker> primary = module.getActivePrimaryLinker();
    if (primary == null) {
      logger.log(
          TreeLogger.ERROR,
          "Primary linker is null.  Does your module "
              + "inherit from com.google.gwt.core.Core or com.google.gwt.user.User?");
    } else {
      linkerClasses.add(module.getActivePrimaryLinker());
    }

    // Get all the post-linkers IN REVERSE ORDER.
    {
      List<Class<? extends Linker>> postLinkerClasses = new ArrayList<Class<? extends Linker>>();
      for (Class<? extends Linker> linkerClass : module.getActiveLinkers()) {
        Order order = linkerClass.getAnnotation(LinkerOrder.class).value();
        assert (order != null);
        if (order == Order.POST) {
          postLinkerClasses.add(linkerClass);
        }
      }
      Collections.reverse(postLinkerClasses);
      linkerClasses.addAll(postLinkerClasses);
    }

    resetLinkers(logger);

    for (Map.Entry<String, Class<? extends Linker>> entry : module.getLinkers().entrySet()) {
      linkerShortNames.put(entry.getValue(), entry.getKey());
    }

    /*
     * This will make all private PublicResources and GeneratedResources appear
     * in the root of the module auxiliary directory.
     */
    linkerShortNames.put(this.getClass(), "");

    // Break ModuleDef properties out into LinkerContext interfaces
    {
      SortedSet<ConfigurationProperty> mutableConfigurationProperties = new TreeSet<ConfigurationProperty>(
          CONFIGURATION_PROPERTY_COMPARATOR);
      SortedSet<SelectionProperty> mutableSelectionProperties = new TreeSet<SelectionProperty>(
          SELECTION_PROPERTY_COMPARATOR);
      for (com.google.gwt.dev.cfg.ConfigurationProperty p : module
          .getProperties().getConfigurationProperties()) {
        StandardConfigurationProperty newProp = new StandardConfigurationProperty(p);
        mutableConfigurationProperties.add(newProp);
        if (logger.isLoggable(TreeLogger.SPAM)) {
          logger.log(TreeLogger.SPAM,
              "Added configuration property " + newProp, null);
        }
      }
      for (BindingProperty p : module.getProperties().getBindingProperties()) {
        StandardSelectionProperty newProp = new StandardSelectionProperty(p);
        mutableSelectionProperties.add(newProp);
        propertiesByName.put(newProp.getName(), newProp);
        if (logger.isLoggable(TreeLogger.SPAM)) {
          logger.log(TreeLogger.SPAM, "Added selection property " + newProp,
              null);
        }
      }
      selectionProperties = Collections.unmodifiableSortedSet(mutableSelectionProperties);
      configurationProperties = Collections.unmodifiableSortedSet(mutableConfigurationProperties);
    }
  }

  public boolean allLinkersAreShardable() {
    return findUnshardableLinkers().isEmpty();
  }

  /**
   * Find all linkers that are not updated to support running generators on
   * compilations shards.
   */
  public List<Linker> findUnshardableLinkers() {
    List<Linker> problemLinkers = new ArrayList<Linker>();

    for (Linker linker : linkers) {
      if (!linker.isShardable()) {
        problemLinkers.add(linker);
      }
    }
    return problemLinkers;
  }

  /**
   * Convert all static resources in the specified module to artifacts.
   */
  public ArtifactSet getArtifactsForPublicResources(TreeLogger logger,
      ModuleDef module) {
    ArtifactSet artifacts = new ArtifactSet();
    for (String path : publicResourceOracle.getPathNames()) {
      String partialPath = path.replace(File.separatorChar, '/');
      PublicResource resource = new StandardPublicResource(partialPath,
          publicResourceOracle.getResource(path));
      artifacts.add(resource);
      if (logger.isLoggable(TreeLogger.SPAM)) {
        logger.log(TreeLogger.SPAM, "Added public resource " + resource, null);
      }
    }

    {
      int index = 0;
      for (Script script : module.getScripts()) {
        String url = script.getSrc();
        artifacts.add(new StandardScriptReference(url, index++));
        if (logger.isLoggable(TreeLogger.SPAM)) {
          logger.log(TreeLogger.SPAM, "Added script " + url, null);
        }
      }
    }

    {
      int index = 0;
      for (String style : module.getStyles()) {
        artifacts.add(new StandardStylesheetReference(style, index++));
        if (logger.isLoggable(TreeLogger.SPAM)) {
          logger.log(TreeLogger.SPAM, "Added style " + style, null);
        }
      }
    }
    return artifacts;
  }

  @Override
  public SortedSet<ConfigurationProperty> getConfigurationProperties() {
    return configurationProperties;
  }

  @Override
  public String getDescription() {
    return "Root Linker";
  }

  /**
   * Return the full path for an artifact produced by <code>linkertype</code>
   * that has the specified partial path. The full path will be the linker's
   * short name, as defined in the module file, followed by the given partial
   * path.
   */
  public String getExtraPathForLinker(Class<? extends Linker> linkerType,
      String partialPath) {
    assert linkerShortNames.containsKey(linkerType) : linkerType.getName()
        + " unknown";
    return linkerShortNames.get(linkerType) + '/' + partialPath;
  }

  @Override
  public String getModuleFunctionName() {
    return moduleFunctionName;
  }

  @Override
  public long getModuleLastModified() {
    return moduleLastModified;
  }

  @Override
  public String getModuleName() {
    return moduleName;
  }

  @Override
  public SortedSet<SelectionProperty> getProperties() {
    return selectionProperties;
  }

  public StandardSelectionProperty getProperty(String name) {
    return propertiesByName.get(name);
  }

  public ArtifactSet invokeFinalLink(TreeLogger logger, ArtifactSet artifacts)
      throws UnableToCompleteException {
    for (Linker linker : linkers) {
      if (linker.isShardable()) {
        TreeLogger linkerLogger = logger.branch(TreeLogger.TRACE,
            "Invoking Linker " + linker.getDescription(), null);
        artifacts = linker.link(linkerLogger, this, artifacts, false);
      }
    }
    return artifacts;
  }

  /**
   * Run linkers that have not been updated for the shardable API.
   */
  public ArtifactSet invokeLegacyLinkers(TreeLogger logger,
      ArtifactSet artifacts) throws UnableToCompleteException {
    ArtifactSet workingArtifacts = new ArtifactSet(artifacts);

    for (Linker linker : linkers) {
      if (!linker.isShardable()) {
        TreeLogger linkerLogger = logger.branch(TreeLogger.TRACE,
            "Invoking Linker " + linker.getDescription(), null);
        workingArtifacts.freeze();
        try {
          workingArtifacts = linker.link(linkerLogger, this, workingArtifacts);
        } catch (Throwable e) {
          linkerLogger.log(TreeLogger.ERROR, "Failed to link", e);
          throw new UnableToCompleteException();
        }
      }
    }
    return workingArtifacts;
  }

  /**
   * Invoke the shardable linkers on one permutation result. Those linkers run
   * with the precompile artifacts as input.
   */
  public ArtifactSet invokeLinkForOnePermutation(TreeLogger logger,
      StandardCompilationResult permResult, ArtifactSet permArtifacts)
      throws UnableToCompleteException {
    ArtifactSet workingArtifacts = new ArtifactSet(permArtifacts);
    workingArtifacts.add(permResult);

    for (Linker linker : linkers) {
      if (linker.isShardable()) {
        TreeLogger linkerLogger = logger.branch(TreeLogger.TRACE,
            "Invoking Linker " + linker.getDescription(), null);
        try {
          workingArtifacts.freeze();
          workingArtifacts = linker.link(logger, this, workingArtifacts, true);
        } catch (Throwable e) {
          linkerLogger.log(TreeLogger.ERROR, "Failed to link", e);
          throw new UnableToCompleteException();
        }
      }
    }

    /*
     * Reset linkers so that they don't accidentally carry any state across
     * permutations
     */
    resetLinkers(logger);

    workingArtifacts.freeze();
    return workingArtifacts;
  }

  public ArtifactSet invokeRelink(TreeLogger logger,
      ArtifactSet newlyGeneratedArtifacts) throws UnableToCompleteException {
    ArtifactSet workingArtifacts = new ArtifactSet(newlyGeneratedArtifacts);

    for (Linker linker : linkers) {
      TreeLogger linkerLogger = logger.branch(TreeLogger.TRACE,
          "Invoking relink on Linker " + linker.getDescription(), null);
      workingArtifacts.freeze();
      try {
        workingArtifacts = linker.relink(linkerLogger, this, workingArtifacts);
      } catch (Throwable e) {
        linkerLogger.log(TreeLogger.ERROR, "Failed to relink", e);
        throw new UnableToCompleteException();
      }
    }
    return workingArtifacts;
  }

  @Override
  public boolean isOutputCompact() {
    return outputOption.shouldMinimize();
  }

  @Override
  public ArtifactSet link(TreeLogger logger, LinkerContext context,
      ArtifactSet artifacts) {
    throw new UnsupportedOperationException();
  }

  @Override
  public String optimizeJavaScript(TreeLogger logger, String program)
      throws UnableToCompleteException {
    logger = logger.branch(TreeLogger.DEBUG, "Attempting to optimize JS", null);
    Reader r = new StringReader(program);
    JsProgram jsProgram = new JsProgram();
    JsScope topScope = jsProgram.getScope();
    topScope.declareUnobfuscatableName(getModuleFunctionName());

    try {
      SourceInfo sourceInfo = jsProgram.createSourceInfo(1,
          "StandardLinkerContext.optimizeJavaScript");
      JsParser.parseInto(sourceInfo, topScope, jsProgram.getGlobalBlock(), r);
    } catch (IOException e) {
      throw new RuntimeException("Unexpected error reading in-memory stream", e);
    } catch (JsParserException e) {
      logger.log(TreeLogger.ERROR, "Unable to parse JavaScript", e);
      throw new UnableToCompleteException();
    }

    JsSymbolResolver.exec(jsProgram);
    JsUnusedFunctionRemover.exec(jsProgram);

    try {
      switch (outputOption) {
        case OBFUSCATED:
          /*
           * We can't apply the regular JsLiteralInterner to the JsProgram that
           * we've just created. In the normal case, the JsLiteralInterner adds an
           * additional statement to the program's global JsBlock, however we
           * don't know exactly what the form and structure of our JsProgram are,
           * so we'll limit the scope of the modifications to each top-level
           * function within the program.
           */
          TopFunctionStringInterner.exec(jsProgram);
          JsObfuscateNamer.exec(jsProgram, null);
          break;
        case PRETTY:
          // We don't intern strings in pretty mode to improve readability
          JsPrettyNamer.exec(jsProgram, null);
          break;
        case DETAILED:
          TopFunctionStringInterner.exec(jsProgram);
          JsVerboseNamer.exec(jsProgram, null);
          break;
        default:
          throw new InternalCompilerException("Unknown output mode");
      }
    } catch (IllegalNameException e) {
      logger.log(TreeLogger.ERROR, e.getMessage(), e);
      throw new UnableToCompleteException();
    }

    DefaultTextOutput out = new DefaultTextOutput(outputOption.shouldMinimize());
    JsSourceGenerationVisitor v = new JsSourceGenerationVisitor(out);
    v.accept(jsProgram);
    return out.toString();
  }

  /**
   * Emit EmittedArtifacts artifacts onto <code>out</code>. Does not close
   * <code>out</code>.
   *
   * @param logger where to log progress
   * @param artifacts the artifacts to emit
   * @param visibility the level of visibility of artifacts to output
   * @param out where to emit the artifact contents
   */
  public void produceOutput(TreeLogger logger, ArtifactSet artifacts,
      Visibility visibility, OutputFileSet out)
      throws UnableToCompleteException {
    logger = logger.branch(TreeLogger.TRACE, "Linking " + visibility
        + " artifacts into " + out.getPathDescription(), null);

    for (EmittedArtifact artifact : artifacts.find(EmittedArtifact.class)) {
      TreeLogger artifactLogger = logger.branch(TreeLogger.DEBUG,
          "Emitting resource " + artifact.getPartialPath(), null);

      if (!artifact.getVisibility().matches(visibility)) {
        continue;
      }

      String partialPath = artifact.getPartialPath();
      if (artifact.getVisibility() != Visibility.Public) {
        // Any non-public linker will have their artifacts stored in a directory
        // named after the linker.
        partialPath = getExtraPathForLinker(artifact.getLinker(), partialPath);
        if (partialPath.startsWith("/")) {
          partialPath = partialPath.substring(1);
        }
      }
      OutputStream artifactStream = null;
      try {
        artifactStream = new BufferedOutputStream(out.openForWrite(partialPath,
            artifact.getLastModified()));
        artifact.writeTo(artifactLogger, artifactStream);
      } catch (IOException e) {
        artifactLogger.log(TreeLogger.ERROR,
            "Fatal error emitting artifact: " + artifact.getPartialPath(), e);
        // Do not fail for Private artifacts, just log the error
        if (visibility != Visibility.Private) {
          throw new UnableToCompleteException();
        }
      } finally {
        Utility.close(artifactStream);
      }
    }
  }

  public ModuleDef getModule() {
    return module;
  }

  /**
   * (Re)instantiate all linkers.
   */
  private void resetLinkers(TreeLogger logger) throws UnableToCompleteException {
    linkers = new Linker[linkerClasses.size()];
    int i = 0;
    for (Class<? extends Linker> linkerClass : linkerClasses) {
      try {
        linkers[i++] = linkerClass.newInstance();
      } catch (InstantiationException e) {
        logger.log(TreeLogger.ERROR, "Unable to create Linker", e);
        throw new UnableToCompleteException();
      } catch (IllegalAccessException e) {
        logger.log(TreeLogger.ERROR, "Unable to create Linker", e);
        throw new UnableToCompleteException();
      }
    }
  }
}
