/*
 * 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.restricted.EscapingConventions;
import com.google.template.soy.shared.restricted.EscapingConventions.EscapingLanguage;
import com.google.template.soy.shared.restricted.Sanitizers;
import com.google.template.soy.shared.restricted.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(" * @private {!Object<string, string>}\n")
        .append(" */\n")
        .append("soy.esc.$$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(" * @private {!RegExp}\n")
        .append(" */\n")
        .append("soy.esc.$$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(" * @private {!RegExp}\n")
        .append(" */\n")
        .append("soy.esc.$$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(" * @private {!RegExp}\n")
        .append(" */\n")
        .append("soy.esc.$$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(" * @private {!RegExp}\n")
        .append(" */\n")
        .append("soy.esc.$$LT_REGEX_ = /</g;\n");

    outputCode.append('\n')
        .append("/**\n")
        .append(" * Maps lower-case names of innocuous tags to true.\n")
        .append(" *\n")
        .append(" * @private {!Object<string, boolean>}\n")
        .append(" */\n")
        .append("soy.esc.$$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(" * @private {!RegExp}\n")
        .append(" */\n")
        .append("soy.esc.$$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(" * @private\n")
        .append(" */\n")
        .append("soy.esc.$$REPLACER_FOR_")
        .append(mapName)
        .append("_ = function(ch) {\n")
        .append("  return soy.esc.$$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("soy.esc.$$").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("soy.esc.$$").append(name).append("Helper = function(value) {\n")
        .append("  var str = String(value);\n");
    if (digest.getFilterName() != null) {
      String filterName = digest.getFilterName();
      outputCode
          .append("  if (!soy.esc.$$FILTER_FOR_").append(filterName).append("_.test(str)) {\n");
      if (availableIdentifiers.apply("goog.asserts.fail")) {
        outputCode
            .append("    goog.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(user): 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("      soy.esc.$$MATCHER_FOR_").append(matcherName).append("_,\n")
          .append("      soy.esc.$$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();
  }
}
