/*
 * 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.template.soy.jssrc.internal;

import com.google.template.soy.shared.internal.AbstractGenerateSoyEscapingDirectiveCode;
import com.google.template.soy.shared.internal.DirectiveDigest;
import com.google.template.soy.shared.internal.EscapingConventions;
import com.google.template.soy.shared.internal.EscapingConventions.EscapingLanguage;
import com.google.template.soy.shared.internal.Sanitizers;
import com.google.template.soy.shared.internal.TagWhitelist;
import java.io.IOException;
import java.util.regex.Pattern;
import javax.annotation.ParametersAreNonnullByDefault;

/**
 * Generates JavaScript code relied upon by soyutils.js and soyutils_use_goog.js.
 *
 * <p>This is an ant task and can be invoked as: <xmp> <taskdef name="gen.escape.directives"
 * classname="com.google.template.soy.jssrc.internal.GenerateSoyUtilsEscapingDirectiveCode">
 * <classpath>
 * <!-- classpath to Soy classes and dependencies -->
 * </classpath> </taskdef> <gen.escape.directives> <input path="one or more JS files that use the
 * generated helpers"/> <output path="the output JS file"/> <libdefined pattern="goog.*"/>
 * <!-- enables closure alternatives -->
 * </gen.escape.directives> </xmp>
 *
 * <p>In the above, the first {@code <taskdef>} is an Ant builtin which links the element named
 * {@code <gen.escape.directives>} to this class.
 *
 * <p>That element contains zero or more {@code <input>}s which are JavaScript source files that may
 * use the helper functions generated by this task.
 *
 * <p>There must be exactly one {@code <output>} element which specifies where the output should be
 * written. That output contains the input sources and the generated helper functions.
 *
 * <p>There may be zero or more {@code <libdefined>} elements which specify which functions should
 * be available in the context in which {@code <output>} is run.
 */
@ParametersAreNonnullByDefault
public final class GenerateSoyUtilsEscapingDirectiveCode
    extends AbstractGenerateSoyEscapingDirectiveCode {

  @Override
  protected EscapingLanguage getLanguage() {
    return EscapingLanguage.JAVASCRIPT;
  }

  @Override
  protected String getLineCommentSyntax() {
    return "//";
  }

  @Override
  protected String getLineEndSyntax() {
    return ";";
  }

  @Override
  protected String getRegexStart() {
    return "/";
  }

  @Override
  protected String getRegexEnd() {
    return "/g";
  }

  @Override
  protected String escapeOutputString(String input) {
    return EscapingConventions.EscapeJsString.INSTANCE.escape(input);
  }

  @Override
  protected String convertFromJavaRegex(Pattern javaPattern) {
    String body =
        javaPattern
            .pattern()
            .replace("\r", "\\r")
            .replace("\n", "\\n")
            .replace("\t", "\\t")
            .replace("\u0000", "\\u0000")
            .replace("\u0020", "\\u0020")
            .replace("\u2028", "\\u2028")
            .replace("\u2029", "\\u2029")
            .replace("\\A", "^")
            .replace("\\z", "$")
            .replaceAll("(?<!\\\\)(?:\\\\{2})*/", "\\\\/");
    // Some features supported by Java are not supported by JavaScript such as lookbehind,
    // DOTALL, and unicode character classes.
    if (body.contains("(?<")) {
      throw new IllegalArgumentException("Pattern " + javaPattern + " uses lookbehind.");
    } else if ((javaPattern.flags() & Pattern.DOTALL) != 0) {
      throw new IllegalArgumentException("Pattern " + javaPattern + " uses DOTALL.");
    } else if (NAMED_CLASS.matcher(body).find()) {
      throw new IllegalArgumentException(
          "Pattern " + javaPattern + " uses named characer classes.");
    }

    StringBuilder buffer = new StringBuilder(body.length() + 4);
    buffer.append('/').append(body).append('/');
    if ((javaPattern.flags() & Pattern.CASE_INSENSITIVE) != 0) {
      buffer.append('i');
    }
    if ((javaPattern.flags() & Pattern.MULTILINE) != 0) {
      buffer.append('m');
    }
    return buffer.toString();
  }

  @Override
  protected void generateCharacterMapSignature(StringBuilder outputCode, String mapName) {
    outputCode
        .append('\n')
        .append("/**\n")
        .append(" * Maps characters to the escaped versions for the named escape directives.\n")
        .append(" * @type {!Object<string, string>}\n")
        .append(" */\n")
        .append("const $$ESCAPE_MAP_FOR_")
        .append(mapName)
        .append("_");
  }

  @Override
  protected void generateMatcher(StringBuilder outputCode, String name, String matcher) {
    outputCode
        .append('\n')
        .append("/**\n")
        .append(" * Matches characters that need to be escaped for the named directives.\n")
        .append(" * @type {!RegExp}\n")
        .append(" */\n")
        .append("const $$MATCHER_FOR_")
        .append(name)
        .append("_ = ")
        .append(matcher)
        .append(";\n");
  }

  @Override
  protected void generateFilter(StringBuilder outputCode, String name, String filter) {
    outputCode
        .append('\n')
        .append("/**\n")
        .append(" * A pattern that vets values produced by the named directives.\n")
        .append(" * @type {!RegExp}\n")
        .append(" */\n")
        .append("const $$FILTER_FOR_")
        .append(name)
        .append("_ = ")
        .append(filter)
        .append(";\n");
  }

  @Override
  protected void generateCommonConstants(StringBuilder outputCode) {
    // Emit patterns and constants needed by escaping functions that are not part of any one
    // escaping convention.
    outputCode
        .append('\n')
        .append("/**\n")
        .append(" * Matches all tags, HTML comments, and DOCTYPEs in tag soup HTML.\n")
        .append(" * By removing these, and replacing any '<' or '>' characters with\n")
        .append(" * entities we guarantee that the result can be embedded into a\n")
        .append(" * an attribute without introducing a tag boundary.\n")
        .append(" *\n")
        .append(" * @type {!RegExp}\n")
        .append(" */\n")
        .append("const $$HTML_TAG_REGEX_ = ")
        .append(convertFromJavaRegex(EscapingConventions.HTML_TAG_CONTENT))
        .append("g;\n");

    outputCode
        .append("\n")
        .append("/**\n")
        .append(" * Matches all occurrences of '<'.\n")
        .append(" *\n")
        .append(" * @type {!RegExp}\n")
        .append(" */\n")
        .append("const $$LT_REGEX_ = /</g;\n");

    outputCode
        .append('\n')
        .append("/**\n")
        .append(" * Maps lower-case names of innocuous tags to true.\n")
        .append(" *\n")
        .append(" * @type {!Object<string, boolean>}\n")
        .append(" */\n")
        .append("const $$SAFE_TAG_WHITELIST_ = ")
        .append(toJsStringSet(TagWhitelist.FORMATTING.asSet()))
        .append(";\n");

    outputCode
        .append('\n')
        .append("/**\n")
        .append(" * Pattern for matching attribute name and value, where value is single-quoted\n")
        .append(" * or double-quoted.\n")
        .append(" * See http://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0\n")
        .append(" *\n")
        .append(" * @type {!RegExp}\n")
        .append(" */\n")
        .append("const $$HTML_ATTRIBUTE_REGEX_ = ")
        .append(convertFromJavaRegex(Sanitizers.HTML_ATTRIBUTE_PATTERN))
        .append("g;\n");
  }

  @Override
  protected void generateReplacerFunction(StringBuilder outputCode, String mapName) {
    outputCode
        .append('\n')
        .append("/**\n")
        .append(" * A function that can be used with String.replace.\n")
        .append(" * @param {string} ch A single character matched by a compatible matcher.\n")
        .append(" * @return {string} A token in the output language.\n")
        .append(" */\n")
        .append("const $$REPLACER_FOR_")
        .append(mapName)
        .append("_ = function(ch) {\n")
        .append("  return $$ESCAPE_MAP_FOR_")
        .append(mapName)
        .append("_[ch];\n")
        .append("};\n");
  }

  @Override
  protected void useExistingLibraryFunction(
      StringBuilder outputCode, String identifier, String existingFunction) {
    outputCode
        .append('\n')
        .append("/**\n")
        .append(" * @type {function (*) : string}\n")
        .append(" */\n")
        .append("const $$")
        .append(identifier)
        .append("Helper = function(v) {\n")
        .append("  return ")
        .append(existingFunction)
        .append("(String(v));\n")
        .append("};\n");
  }

  @Override
  protected void generateHelperFunction(StringBuilder outputCode, DirectiveDigest digest) {
    String name = digest.getDirectiveName();
    outputCode
        .append('\n')
        .append("/**\n")
        .append(" * A helper for the Soy directive |")
        .append(name)
        .append('\n')
        .append(" * @param {?} value Can be of any type but will be coerced to a string.\n")
        .append(" * @return {string} The escaped text.\n")
        .append(" */\n")
        .append("const $$")
        .append(name)
        .append("Helper = function(value) {\n")
        .append("  const str = String(value);\n");
    if (digest.getFilterName() != null) {
      String filterName = digest.getFilterName();
      outputCode.append("  if (!$$FILTER_FOR_").append(filterName).append("_.test(str)) {\n");
      if (availableIdentifiers.test("asserts.fail")) {
        outputCode
            .append("    asserts.fail('Bad value `%s` for |")
            .append(name)
            .append("', [str]);\n");
      }
      outputCode
          .append("    return '")
          .append(digest.getInnocuousOutput())
          .append("';\n")
          .append("  }\n");
    }

    if (digest.getNonAsciiPrefix() != null) {
      // TODO(msamuel): We can add a second replace of all non-ascii codepoints below.
      throw new UnsupportedOperationException("Non ASCII prefix escapers not implemented yet.");
    }
    if (digest.getEscapesName() != null) {
      String escapeMapName = digest.getEscapesName();
      String matcherName = digest.getMatcherName();
      outputCode
          .append("  return str.replace(\n")
          .append("      $$MATCHER_FOR_")
          .append(matcherName)
          .append("_,\n")
          .append("      $$REPLACER_FOR_")
          .append(escapeMapName)
          .append("_);\n");
    } else {
      outputCode.append("  return str;\n");
    }
    outputCode.append("};\n");
  }

  /** ["foo", "bar"] -> '{"foo": true, "bar": true}' */
  private String toJsStringSet(Iterable<String> strings) {
    StringBuilder sb = new StringBuilder();
    boolean isFirst = true;
    sb.append('{');
    for (String str : strings) {
      if (!isFirst) {
        sb.append(", ");
      }
      isFirst = false;
      writeStringLiteral(str, sb);
      sb.append(": true");
    }
    sb.append('}');
    return sb.toString();
  }

  /** Matches named character classes in Java regular expressions. */
  private static final Pattern NAMED_CLASS = Pattern.compile("(?<!\\\\)(\\\\{2})*\\\\p\\{");

  /** A non Ant interface for this class. */
  public static void main(String[] args) throws IOException {
    GenerateSoyUtilsEscapingDirectiveCode generator = new GenerateSoyUtilsEscapingDirectiveCode();
    generator.configure(args);
    generator.execute();
  }
}
