/*
 * Copyright 2016 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.incrementaldomsrc;

import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.template.soy.base.internal.BaseUtils;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.jssrc.SoyJsSrcOptions;
import com.google.template.soy.jssrc.internal.GenCallCodeUtils;
import com.google.template.soy.jssrc.internal.GenJsCodeVisitorAssistantForMsgs;
import com.google.template.soy.jssrc.internal.GenJsExprsVisitor;
import com.google.template.soy.jssrc.internal.IsComputableAsJsExprsVisitor;
import com.google.template.soy.jssrc.internal.JsExprTranslator;
import com.google.template.soy.jssrc.internal.TemplateAliases;
import com.google.template.soy.jssrc.restricted.JsExpr;
import com.google.template.soy.shared.internal.CodeBuilder;
import com.google.template.soy.soytree.HtmlContext;
import com.google.template.soy.soytree.MsgFallbackGroupNode;
import com.google.template.soy.soytree.MsgPlaceholderNode;

import java.util.Deque;
import java.util.Map;

/**
 * Translates <code>{msg}</code> commands in HTML context into idom instructions.
 *
 * This class is not reusable.
 *
 * This will pass all interpolated values as special placeholder strings. It will then extract these
 * placeholders from the translated message and execute the idom commands instead.
 */
final class AssistantForHtmlMsgs extends GenJsCodeVisitorAssistantForMsgs {

  /**
   * Maps dynamic nodes within the translated message to placeholder values to pass to goog.getMsg()
   * and substitute for idom commands.
   */
  private final Map<String, MsgPlaceholderNode> placeholderNames = Maps.newHashMap();

  /**
   * Wrapper character around placeholder placeholders.  This is used to locate placeholder names in
   * the translated result so we can instead run the idom instructions in their MsgPlaceholderNodes.
   * The value is an arbitrary but short character that cannot appear in translated messages.
   */
  private static final String PLACEHOLDER_WRAPPER = "\u0001";

  /** A JS regex literal that matches our placeholder placeholders. */
  private static final String PLACEHOLDER_REGEX = "/\\x01\\d+\\x01/g";

  AssistantForHtmlMsgs(
      GenIncrementalDomCodeVisitor master,
      SoyJsSrcOptions jsSrcOptions,
      JsExprTranslator jsExprTranslator,
      GenCallCodeUtils genCallCodeUtils,
      IsComputableAsJsExprsVisitor isComputableAsJsExprsVisitor,
      CodeBuilder<JsExpr> jsCodeBuilder,
      Deque<Map<String, JsExpr>> localVarTranslations,
      TemplateAliases functionAliases,
      GenJsExprsVisitor genJsExprsVisitor,
      ErrorReporter errorReporter) {
    super(master, jsSrcOptions, jsExprTranslator, genCallCodeUtils, isComputableAsJsExprsVisitor,
        jsCodeBuilder, localVarTranslations, functionAliases, genJsExprsVisitor, errorReporter);
  }

  @Override
  public String generateMsgGroupVariable(MsgFallbackGroupNode node) {
    throw new IllegalStateException(
        "This class should only be used for via the new idom entry-point.");
  }

  /**
   * Generates idom instructions that output the contents of a translated message as HTML.
   * For example:
   * <pre>
   *   {msg desc="Says hello to a person."}Hello {$name}!{/msg}
   * </pre>
   * compiles to
   * <pre>
   *   /** @desc Says hello to a person. *{@literal /}
   *   var MSG_EXTERNAL_6936162475751860807 = goog.getMsg(
   *       'Hello {$name}!',
   *       {'name': '\u00010\u0001'});
   *   var lastIndex_1153 = 0, partRe_1153 = /\x01\d+\x01/g, match_1153;
   *   do {
   *     match_1153 = partRe_1153.exec(MSG_EXTERNAL_6936162475751860807) || undefined;
   *     itext(goog.string.unescapeEntities(MSG_EXTERNAL_6936162475751860807.substring(lastIndex_1153, match_1153 && match_1153.index)));
   *     lastIndex_1153 = partRe_1153.lastIndex;
   *     switch (match_1153 && match_1153[0]) {
   *       case '\u00010\u0001':
   *         var dyn8 = opt_data.name;
   *         if (typeof dyn8 == 'function') dyn8(); else if (dyn8 != null) itext(dyn8);
   *         break;
   *     }
   *   } while (match_1153);
   * </pre>
   * Each interpolated MsgPlaceholderNode (either for HTML tags or for print statements) compiles to
   * a separate {@code case} statement.
   */
  void generateMsgGroupCode(MsgFallbackGroupNode node) {
    Preconditions.checkState(placeholderNames.isEmpty(), "This class is not reusable.");
    // Non-HTML {msg}s should be extracted into LetContentNodes and handled by jssrc.
    Preconditions.checkArgument(node.getHtmlContext() == HtmlContext.HTML_PCDATA,
        "AssistantForHtmlMsgs is only for HTML {msg}s.");

    // All of these helper variables must have uniquely-suffixed names because {msg}s can be nested.

    // It'd be nice to move this codegen to a Soy template...

    // The raw translated text, with placeholder placeholders.
    String translationVar = super.generateMsgGroupVariable(node);

    // If there are no placeholders, we don't need anything special (but we still need to unescape).
    if (placeholderNames.isEmpty()) {
      jsCodeBuilder.appendLine("itext(goog.string.unescapeEntities(", translationVar, "));");
      return;
    }

    // The mutable (tracking index of last match) regex to find the placeholder placeholders.
    String regexVar = "partRe_" + node.getId();
    // The current placeholder placeholder from the regex.
    String matchVar = "match_" + node.getId();
    // The index of the end of the previous placeholder, where the next raw text run starts.
    String lastIndexVar = "lastIndex_" + node.getId();

    // Declare everything.
    jsCodeBuilder.appendLineStart("var ", lastIndexVar, " = 0, ");
    jsCodeBuilder.appendLineEnd(regexVar, " = ", PLACEHOLDER_REGEX, ", ", matchVar, ";");

    // For each placeholder.
    jsCodeBuilder.appendLine("do {");
    jsCodeBuilder.increaseIndent();
    // Find the placeholder.
    jsCodeBuilder.appendLine(matchVar, " = ", regexVar,
        ".exec(", translationVar, ") || undefined;");
    // Replace null with undefined.  This is necessary to make substring() treat falsy as an omitted
    // parameter, so that it goes until the end of the string.  Otherwise, the non-numeric parameter
    // would be coerced to zero.

    // Emit the (possibly-empty) run of raw text since the last placeholder, until this placeholder,
    // or until the end of the source string.
    jsCodeBuilder.appendLine("itext(goog.string.unescapeEntities(", translationVar,
        ".substring(", lastIndexVar, ", ", matchVar, " && ", matchVar, ".index)));");
    jsCodeBuilder.appendLine(lastIndexVar, " = ", regexVar, ".lastIndex;");

    // Handle the actual placeholder.
    jsCodeBuilder.appendLine("switch (", matchVar, " && ", matchVar, "[0]) {");
    jsCodeBuilder.increaseIndent();

    for (Map.Entry<String, MsgPlaceholderNode> ph : placeholderNames.entrySet()) {
      jsCodeBuilder.appendLine("case ", BaseUtils.escapeToSoyString(ph.getKey(), true), ":");
      jsCodeBuilder.increaseIndent();
      master.visitForUseByAssistants(ph.getValue());
      jsCodeBuilder.appendLine("break;");
      jsCodeBuilder.decreaseIndent();
    }
    jsCodeBuilder.decreaseIndent();
    jsCodeBuilder.appendLine("}");

    jsCodeBuilder.decreaseIndent();
    jsCodeBuilder.appendLine("} while (", matchVar, ");");
  }

  @Override
  protected JsExpr genGoogMsgPlaceholderExpr(MsgPlaceholderNode msgPhNode) {
    // Mark the node so we know what instructions to emit.
    String name = PLACEHOLDER_WRAPPER + placeholderNames.size() + PLACEHOLDER_WRAPPER;
    placeholderNames.put(name, msgPhNode);

    // Return the marker string to insert into the translated text.
    return new JsExpr(BaseUtils.escapeToSoyString(name, true), Integer.MAX_VALUE);
  }
}
