/*
 * 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.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.AbstractLinker;
import com.google.gwt.core.ext.linker.Artifact;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.CompilationResult;
import com.google.gwt.core.ext.linker.ConfigurationProperty;
import com.google.gwt.core.ext.linker.EmittedArtifact;
import com.google.gwt.core.ext.linker.LinkerUtils;
import com.google.gwt.core.ext.linker.SelectionProperty;
import com.google.gwt.core.ext.linker.SoftPermutation;
import com.google.gwt.core.ext.linker.StatementRanges;
import com.google.gwt.core.linker.SymbolMapsLinker;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;

/**
 * A base class for Linkers that use an external script to boostrap the GWT module. This
 * implementation injects JavaScript Snippets into a JS program defined in an external file.<br />
 *
 * Created nocache.js files must provide an implementation of both __gwt_isKnownPropertyValue() and
 * __gwt_getMetaProperty() to support the permutation selection process. These functions must be
 * available within the nocache.js scope and must be made available to the permutation js scope.
 */
public abstract class SelectionScriptLinker extends AbstractLinker {

  public static final String USE_SOURCE_MAPS_PROPERTY = "compiler.useSourceMaps";

  /**
   * File name for computeScriptBase.js.
   */
  protected static final String COMPUTE_SCRIPT_BASE_JS = "com/google/gwt/core/ext/linker/impl/computeScriptBaseOld.js";

  /**
   * The extension added to demand-loaded fragment files.
   */
  protected static final String FRAGMENT_EXTENSION = ".cache.js";

  /**
   * A subdirectory to hold all the generated fragments.
   */
  protected static final String FRAGMENT_SUBDIR = "deferredjs";

  /**
   * Utility class to handle insertion of permutations code.
   */
  protected static PermutationsUtil permutationsUtil = new PermutationsUtil();

  /**
   * File name for processMetas.js.
   */
  protected static final String PROCESS_METAS_JS = "com/google/gwt/core/ext/linker/impl/processMetasOld.js";

  /**
   * TODO(bobv): Move this class into c.g.g.core.linker when HostedModeLinker
   * goes away?
   */

  /**
   * A configuration property indicating how large each script tag should be.
   */
  private static final String CHUNK_SIZE_PROPERTY = "iframe.linker.script.chunk.size";

  /**
   * A configuration property that can be used to have the linker load from somewhere other than
   * {@link #FRAGMENT_SUBDIR}.
   */
  private static final String PROP_FRAGMENT_SUBDIR_OVERRIDE = "iframe.linker.deferredjs.subdir";

  /**
   * Split a JavaScript string into multiple chunks, at statement boundaries. This method is made
   * default access for testing.
   *
   * @param ranges               Describes where the statements are located within the JavaScript
   *                             code. If <code>null</code>, then return <code>js</code> unchanged.
   * @param js                   The JavaScript code to be split up.
   * @param charsPerChunk        The number of characters to be put in each script tag.
   * @param scriptChunkSeparator The string to insert between chunks.
   * @param context
   */
  public static String splitPrimaryJavaScript(StatementRanges ranges, String js,
      int charsPerChunk, String scriptChunkSeparator, LinkerContext context) {
    boolean useSourceMaps = false;
    for (SelectionProperty prop : context.getProperties()) {
      if (USE_SOURCE_MAPS_PROPERTY.equals(prop.getName())) {
        String str = prop.tryGetValue();
        useSourceMaps = str == null ? false : Boolean.parseBoolean(str);
        break;
      }
    }

    // TODO(cromwellian) enable chunking with sourcemaps
    if (charsPerChunk < 0 || ranges == null || useSourceMaps) {
      return js;
    }

    StringBuilder sb = new StringBuilder((int) (js.length() * 1.05));
    int bytesInCurrentChunk = 0;

    for (int i = 0; i < ranges.numStatements(); i++) {
      int start = ranges.start(i);
      int end = ranges.end(i);
      int length = end - start;
      if (bytesInCurrentChunk > 0 && bytesInCurrentChunk + length > charsPerChunk) {
        if (lastChar(sb) != '\n') {
          sb.append('\n');
        }
        sb.append(scriptChunkSeparator);
        bytesInCurrentChunk = 0;
      }
      if (bytesInCurrentChunk > 0) {
        char lastChar = lastChar(sb);
        if (lastChar != '\n' && lastChar != ';' && lastChar != '}') {
          /*
           * Make sure this statement has a separator from the last one.
           */
          sb.append(";");
        }
      }
      sb.append(js, start, end);
      bytesInCurrentChunk += length;
    }
    return sb.toString();
  }

  protected static void replaceAll(StringBuffer buf, String search,
      String replace) {
    int len = search.length();
    for (int pos = buf.indexOf(search); pos >= 0; pos = buf.indexOf(search,
        pos + 1)) {
      buf.replace(pos, pos + len, replace);
    }
  }

  private static char lastChar(StringBuilder sb) {
    return sb.charAt(sb.length() - 1);
  }

  /**
   * This method is left in place for existing subclasses of SelectionScriptLinker that have not
   * been upgraded for the sharding API.
   */
  @Override
  public ArtifactSet link(TreeLogger logger, LinkerContext context,
      ArtifactSet artifacts) throws UnableToCompleteException {
    ArtifactSet toReturn = link(logger, context, artifacts, true);
    toReturn = link(logger, context, toReturn, false);
    return toReturn;
  }

  @Override
  public ArtifactSet link(TreeLogger logger, LinkerContext context,
      ArtifactSet artifacts, boolean onePermutation)
      throws UnableToCompleteException {
    if (onePermutation) {
      ArtifactSet toReturn = new ArtifactSet(artifacts);
      ArtifactSet writableArtifacts = new ArtifactSet(artifacts);

      /*
       * Support having multiple compilation results because this method is also
       * called from the legacy link method.
       */
      for (CompilationResult compilation : toReturn.find(CompilationResult.class)) {
        // pass a writable set so that other stages can use this set for temporary storage
        toReturn.addAll(doEmitCompilation(logger, context, compilation, writableArtifacts));
        maybeAddHostedModeFile(logger, context, toReturn, compilation);
      }
      /*
       * Find edit artifacts added during linking and add them. This is done as a way of returning
       * arbitrary extra outputs from within the linker methods without having to modify
       * their return signatures to pass extra return data around.
       */
      for (SymbolMapsLinker.ScriptFragmentEditsArtifact ea : writableArtifacts.find(
          SymbolMapsLinker.ScriptFragmentEditsArtifact.class)) {
        toReturn.add(ea);
      }
      return toReturn;
    } else {
      permutationsUtil.setupPermutationsMap(artifacts);
      ArtifactSet toReturn = new ArtifactSet(artifacts);
      EmittedArtifact art = emitSelectionScript(logger, context, artifacts);
      if (art != null) {
        toReturn.add(art);
      }
      maybeOutputPropertyMap(logger, context, toReturn);
      maybeAddHostedModeFile(logger, context, toReturn, null);
      return toReturn;
    }
  }

  @Override
  public boolean supportsDevModeInJunit(LinkerContext context) {
    return !"".equals(getHostedFilename());
  }

  /**
   * Extract via {@link #CHUNK_SIZE_PROPERTY} the number of characters to be included in each
   * chunk.
   */
  protected int charsPerChunk(LinkerContext context, TreeLogger logger) {
    SortedSet<ConfigurationProperty> configurationProperties = context.getConfigurationProperties();
    for (ConfigurationProperty property : configurationProperties) {
      if (property.getName().equals(CHUNK_SIZE_PROPERTY)) {
        return Integer.parseInt(property.getValues().get(0));
      }
    }
    // CompilerParameters.gwt.xml indicates that if this property is -1, then
    // no chunking is performed, so we return that as the default.  Since
    // Core.gwt.xml contains a definition for this property, this should never
    // happen in production, but some tests mock out the ConfigurationProperties
    // so we want to have a reasonable default rather than making them all add
    // a value for this property.
    return -1;
  }

  protected Collection<Artifact<?>> doEmitCompilation(TreeLogger logger,
      LinkerContext context, CompilationResult result, ArtifactSet artifacts)
      throws UnableToCompleteException {
    String[] js = result.getJavaScript();

    Collection<Artifact<?>> toReturn = new ArrayList<Artifact<?>>();

    byte[] primary = generatePrimaryFragment(logger, context, result, js, artifacts);
    toReturn.add(emitBytes(logger, primary, result.getStrongName()
        + getCompilationExtension(logger, context)));
    primary = null;

    for (int i = 1; i < js.length; i++) {
      String s = generateDeferredFragment(logger, context, i, js[i], artifacts,
          result);
      byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
      toReturn.add(emitBytes(logger, bytes, FRAGMENT_SUBDIR + File.separator
          + result.getStrongName() + File.separator + i + FRAGMENT_EXTENSION));
    }

    toReturn.addAll(emitSelectionInformation(result.getStrongName(), result));
    return toReturn;
  }

  protected List<Artifact<?>> emitSelectionInformation(String strongName,
      CompilationResult result) {
    List<Artifact<?>> emitted = new ArrayList<Artifact<?>>();

    for (SortedMap<SelectionProperty, String> propertyMap : result.getPropertyMap()) {
      TreeMap<String, String> propMap = new TreeMap<String, String>();
      for (Map.Entry<SelectionProperty, String> entry : propertyMap.entrySet()) {
        propMap.put(entry.getKey().getName(), entry.getValue());
      }

      // The soft properties may not be a subset of the existing set
      for (SoftPermutation soft : result.getSoftPermutations()) {
        // Make a copy we can add add more properties to
        TreeMap<String, String> softMap = new TreeMap<String, String>(propMap);
        // Make sure this SelectionInformation contains the soft properties
        for (Map.Entry<SelectionProperty, String> entry : soft.getPropertyMap().entrySet()) {
          softMap.put(entry.getKey().getName(), entry.getValue());
        }
        emitted.add(new SelectionInformation(strongName, soft.getId(), softMap));
      }
    }

    return emitted;
  }

  protected EmittedArtifact emitSelectionScript(TreeLogger logger,
      LinkerContext context, ArtifactSet artifacts)
      throws UnableToCompleteException {
    /*
     * Last modified is important to keep Development Mode refreses from
     * clobbering Production Mode compiles. We set the timestamp on the
     * Development Mode selection script to the same mod time as the module (to
     * allow updates). For Production Mode, we just set it to now.
     */
    long lastModified;
    if (permutationsUtil.getPermutationsMap().isEmpty()) {
      lastModified = context.getModuleLastModified();
    } else {
      lastModified = System.currentTimeMillis();
    }
    String ss = generateSelectionScript(logger, context, artifacts);
    return emitString(logger, ss, context.getModuleName()
        + ".nocache.js", lastModified);
  }

  /**
   * Generate a selection script. The selection information should previously have been scanned
   * using {@link PermutationsUtil#setupPermutationsMap(ArtifactSet)}.
   */
  protected String fillSelectionScriptTemplate(StringBuffer selectionScript,
      TreeLogger logger, LinkerContext context, ArtifactSet artifacts,
      CompilationResult result) throws
      UnableToCompleteException {
    String computeScriptBase;
    String processMetas;
    try {
      computeScriptBase = LinkerUtils.readClasspathFileAsString(COMPUTE_SCRIPT_BASE_JS);
      processMetas = LinkerUtils.readClasspathFileAsString(PROCESS_METAS_JS);
    } catch (IOException e) {
      logger.log(TreeLogger.ERROR, "Unable to read selection script template",
          e);
      throw new UnableToCompleteException();
    }
    replaceAll(selectionScript, "__COMPUTE_SCRIPT_BASE__", computeScriptBase);
    replaceAll(selectionScript, "__PROCESS_METAS__", processMetas);

    selectionScript = ResourceInjectionUtil.injectResources(selectionScript, artifacts);
    permutationsUtil.addPermutationsJs(selectionScript, logger, context);

    replaceAll(selectionScript, "__MODULE_FUNC__",
        context.getModuleFunctionName());
    replaceAll(selectionScript, "__MODULE_NAME__", context.getModuleName());
    replaceAll(selectionScript, "__HOSTED_FILENAME__", getHostedFilename());

    return selectionScript.toString();
  }

  /**
   * @param logger   a TreeLogger
   * @param context  a LinkerContext
   * @param fragment the fragment number
   */
  protected String generateDeferredFragment(TreeLogger logger,
      LinkerContext context, int fragment, String js, ArtifactSet artifacts,
      CompilationResult result)
      throws UnableToCompleteException {
    StringBuilder b = new StringBuilder();
    String strongName = result == null ? "" : result.getStrongName();
    String prefix = getDeferredFragmentPrefix(logger, context, fragment);
    b.append(prefix);
    b.append(js);
    String suffix = getDeferredFragmentSuffix2(logger, context, fragment, strongName);
    if (suffix == null) {
      logger.log(Type.ERROR, "getDeferredFragmentSuffix2 "
          + "was not overridden in linker: " + getClass().getName());
      throw new UnableToCompleteException();
    }
    b.append(suffix);
    SymbolMapsLinker.ScriptFragmentEditsArtifact editsArtifact
        = new SymbolMapsLinker.ScriptFragmentEditsArtifact(strongName, fragment);
    editsArtifact.prefixLines(prefix);
    artifacts.add(editsArtifact);
    return wrapDeferredFragment(logger, context, fragment, b.toString(), artifacts);
  }

  /**
   * Generate the primary fragment. The default implementation is based on {@link
   * #getModulePrefix(TreeLogger, LinkerContext, String, int)} and {@link
   * #getModuleSuffix2(TreeLogger, LinkerContext, String)}.
   */
  protected byte[] generatePrimaryFragment(TreeLogger logger,
      LinkerContext context, CompilationResult result, String[] js,
      ArtifactSet artifacts) throws UnableToCompleteException {
    String temp = splitPrimaryJavaScript(result.getStatementRanges()[0], js[0],
        charsPerChunk(context, logger), getScriptChunkSeparator(logger, context), context);
    String primaryFragmentString =
        generatePrimaryFragmentString(logger, context, result, temp, js.length, artifacts);
    return primaryFragmentString.getBytes(StandardCharsets.UTF_8);
  }

  protected String generatePrimaryFragmentString(TreeLogger logger,
      LinkerContext context, CompilationResult result, String js, int length,
      ArtifactSet artifacts)
      throws UnableToCompleteException {
    StringBuilder b = new StringBuilder();
    String strongName = result == null ? "" : result.getStrongName();

    String modulePrefix = getModulePrefix(logger, context, strongName, length);
    SymbolMapsLinker.ScriptFragmentEditsArtifact editsArtifact
        = new SymbolMapsLinker.ScriptFragmentEditsArtifact(strongName, 0);
    editsArtifact.prefixLines(modulePrefix);
    artifacts.add(editsArtifact);
    b.append(modulePrefix);
    b.append(js);
    String suffix = getModuleSuffix2(logger, context, strongName);
    if (suffix == null) {
      logger.log(Type.ERROR, "getModuleSuffix2 was not overridden in "
          + "linker: " + getClass().getName());
      throw new UnableToCompleteException();
    }
    b.append(suffix);
    return wrapPrimaryFragment(logger, context, b.toString(), artifacts, result);
  }

  protected String generateSelectionScript(TreeLogger logger,
      LinkerContext context, ArtifactSet artifacts) throws UnableToCompleteException {
    return generateSelectionScript(logger, context, artifacts, null);
  }

  protected String generateSelectionScript(TreeLogger logger,
      LinkerContext context, ArtifactSet artifacts, CompilationResult result)
      throws UnableToCompleteException {
    String selectionScriptText;
    StringBuffer buffer = readFileToStringBuffer(
        getSelectionScriptTemplate(logger, context), logger);
    selectionScriptText = fillSelectionScriptTemplate(
        buffer, logger, context, artifacts, result);
    selectionScriptText =
        context.optimizeJavaScript(logger, selectionScriptText);
    return selectionScriptText;
  }

  protected abstract String getCompilationExtension(TreeLogger logger,
      LinkerContext context) throws UnableToCompleteException;

  protected String getDeferredFragmentPrefix(TreeLogger logger, LinkerContext context,
      int fragment) {
    return "";
  }

  /**
   * Returns the suffix at the end of a JavaScript fragment other than the initial fragment.
   */
  protected String getDeferredFragmentSuffix2(TreeLogger logger, LinkerContext context,
      int fragment, String strongName) {
    return "";
  }

  /**
   * Returns the subdirectory name to be used by getModulPrefix when requesting a runAsync module.
   * It is specified by {@link #PROP_FRAGMENT_SUBDIR_OVERRIDE} and, aside from test cases, is always
   * {@link #FRAGMENT_SUBDIR}.
   */
  protected final String getFragmentSubdir(TreeLogger logger,
      LinkerContext context) throws UnableToCompleteException {
    String subdir = null;
    for (ConfigurationProperty prop : context.getConfigurationProperties()) {
      if (prop.getName().equals(PROP_FRAGMENT_SUBDIR_OVERRIDE)) {
        subdir = prop.getValues().get(0);
      }
    }

    if (subdir == null) {
      logger.log(TreeLogger.ERROR, "Could not find property "
          + PROP_FRAGMENT_SUBDIR_OVERRIDE);
      throw new UnableToCompleteException();
    }

    return subdir;
  }

  protected String getHostedFilename() {
    return "";
  }

  /**
   * Compute the beginning of a JavaScript file that will hold the main module implementation.
   */
  protected abstract String getModulePrefix(TreeLogger logger,
      LinkerContext context, String strongName)
      throws UnableToCompleteException;

  /**
   * Compute the beginning of a JavaScript file that will hold the main module implementation. By
   * default, calls {@link #getModulePrefix(TreeLogger, LinkerContext, String)}.
   *
   * @param strongName   strong name of the module being emitted
   * @param numFragments the number of fragments for this module, including the main fragment
   *                     (fragment 0)
   */
  protected String getModulePrefix(TreeLogger logger, LinkerContext context,
      String strongName, int numFragments) throws UnableToCompleteException {
    return getModulePrefix(logger, context, strongName);
  }

  /**
   * Returns the suffix for the initial JavaScript fragment.
   */
  protected String getModuleSuffix2(TreeLogger logger,
      LinkerContext context, String strongName) throws UnableToCompleteException {
    return null;
  }

  /**
   * Some subclasses support "chunking" of the primary fragment. If chunking will be supported, this
   * function should be overridden to return the string which should be inserted between each
   * chunk.
   */
  protected String getScriptChunkSeparator(TreeLogger logger, LinkerContext context) {
    return "";
  }

  protected abstract String getSelectionScriptTemplate(TreeLogger logger,
      LinkerContext context) throws UnableToCompleteException;

  /**
   * Add the Development Mode file to the artifact set.
   */
  protected void maybeAddHostedModeFile(TreeLogger logger,
      LinkerContext context, ArtifactSet artifacts, CompilationResult result)
      throws UnableToCompleteException {
    String hostedFilename = getHostedFilename();
    if ("".equals(hostedFilename) || result != null) {
      return;
    }
    try {
      URL resource = SelectionScriptLinker.class.getResource(hostedFilename);
      if (resource == null) {
        logger.log(TreeLogger.ERROR,
            "Unable to find support resource: " + hostedFilename);
        throw new UnableToCompleteException();
      }

      final URLConnection connection = resource.openConnection();
      // TODO: extract URLArtifact class?
      EmittedArtifact hostedFile = new EmittedArtifact(
          SelectionScriptLinker.class, hostedFilename) {
        @Override
        public InputStream getContents(TreeLogger logger)
            throws UnableToCompleteException {
          try {
            return connection.getInputStream();
          } catch (IOException e) {
            logger.log(TreeLogger.ERROR, "Unable to copy support resource", e);
            throw new UnableToCompleteException();
          }
        }

        @Override
        public long getLastModified() {
          return connection.getLastModified();
        }
      };
      artifacts.add(hostedFile);
    } catch (IOException e) {
      logger.log(TreeLogger.ERROR, "Unable to copy support resource", e);
      throw new UnableToCompleteException();
    }
  }

  protected void maybeOutputPropertyMap(TreeLogger logger,
      LinkerContext context, ArtifactSet toReturn) {
    return;
  }

  protected StringBuffer readFileToStringBuffer(String filename,
      TreeLogger logger) throws UnableToCompleteException {
    StringBuffer buffer;
    try {
      buffer = new StringBuffer(LinkerUtils.readClasspathFileAsString(filename));
    } catch (IOException e) {
      logger.log(TreeLogger.ERROR, "Unable to read file: " + filename, e);
      throw new UnableToCompleteException();
    }
    return buffer;
  }

  protected String wrapDeferredFragment(TreeLogger logger,
      LinkerContext context, int fragment, String script, ArtifactSet artifacts)
      throws UnableToCompleteException {
    return script;
  }

  protected String wrapPrimaryFragment(TreeLogger logger,
      LinkerContext context, String script, ArtifactSet artifacts,
      CompilationResult result) throws UnableToCompleteException {
    return script;
  }
}
