/*
 * Copyright 2015 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.pysrc.internal;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import com.google.template.soy.base.internal.BaseUtils;
import com.google.template.soy.base.internal.LegacyInternalSyntaxException;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.Operator;
import com.google.template.soy.msgs.internal.MsgUtils;
import com.google.template.soy.pysrc.internal.MsgFuncGenerator.MsgFuncGeneratorFactory;
import com.google.template.soy.pysrc.restricted.PyExpr;
import com.google.template.soy.pysrc.restricted.PyExprUtils;
import com.google.template.soy.pysrc.restricted.PyStringExpr;
import com.google.template.soy.pysrc.restricted.SoyPySrcPrintDirective;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
import com.google.template.soy.soytree.CallNode;
import com.google.template.soy.soytree.CallParamContentNode;
import com.google.template.soy.soytree.CssNode;
import com.google.template.soy.soytree.IfCondNode;
import com.google.template.soy.soytree.IfElseNode;
import com.google.template.soy.soytree.IfNode;
import com.google.template.soy.soytree.MsgFallbackGroupNode;
import com.google.template.soy.soytree.PrintDirectiveNode;
import com.google.template.soy.soytree.PrintNode;
import com.google.template.soy.soytree.RawTextNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.XidNode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Visitor for generating Python expressions for parse tree nodes.
 *
 * <p>Important: Do not use outside of Soy code (treat as superpackage-private).
 *
 */
public class GenPyExprsVisitor extends AbstractSoyNodeVisitor<List<PyExpr>> {

  /** Injectable factory for creating an instance of this class. */
  public static interface GenPyExprsVisitorFactory {
    public GenPyExprsVisitor create(LocalVariableStack localVarExprs, ErrorReporter errorReporter);
  }

  /** Map of all SoyPySrcPrintDirectives (name to directive). */
  private Map<String, SoyPySrcPrintDirective> soyPySrcDirectivesMap;

  private final IsComputableAsPyExprVisitor isComputableAsPyExprVisitor;

  private final GenPyExprsVisitorFactory genPyExprsVisitorFactory;

  private final GenPyCallExprVisitor genPyCallExprVisitor;

  private final MsgFuncGeneratorFactory msgFuncGeneratorFactory;

  private final LocalVariableStack localVarExprs;

  /** List to collect the results. */
  private List<PyExpr> pyExprs;

  private final ErrorReporter errorReporter;

  /** @param soyPySrcDirectivesMap Map of all SoyPySrcPrintDirectives (name to directive). */
  @AssistedInject
  GenPyExprsVisitor(
      ImmutableMap<String, SoyPySrcPrintDirective> soyPySrcDirectivesMap,
      IsComputableAsPyExprVisitor isComputableAsPyExprVisitor,
      GenPyExprsVisitorFactory genPyExprsVisitorFactory,
      MsgFuncGeneratorFactory msgFuncGeneratorFactory,
      GenPyCallExprVisitor genPyCallExprVisitor,
      @Assisted LocalVariableStack localVarExprs,
      @Assisted ErrorReporter errorReporter) {
    this.soyPySrcDirectivesMap = soyPySrcDirectivesMap;
    this.isComputableAsPyExprVisitor = isComputableAsPyExprVisitor;
    this.genPyExprsVisitorFactory = genPyExprsVisitorFactory;
    this.genPyCallExprVisitor = genPyCallExprVisitor;
    this.msgFuncGeneratorFactory = msgFuncGeneratorFactory;
    this.localVarExprs = localVarExprs;
    this.errorReporter = errorReporter;
  }

  @Override
  public List<PyExpr> exec(SoyNode node) {
    Preconditions.checkArgument(isComputableAsPyExprVisitor.exec(node));
    pyExprs = new ArrayList<>();
    visit(node);
    return pyExprs;
  }

  /**
   * Executes this visitor on the children of the given node, without visiting the given node
   * itself.
   */
  List<PyExpr> execOnChildren(ParentSoyNode<?> node) {
    Preconditions.checkArgument(isComputableAsPyExprVisitor.execOnChildren(node));
    pyExprs = new ArrayList<>();
    visitChildren(node);
    return pyExprs;
  }

  // -----------------------------------------------------------------------------------------------
  // Implementations for specific nodes.

  /**
   * Example:
   *
   * <pre>
   *   I'm feeling lucky!
   * </pre>
   *
   * generates
   *
   * <pre>
   *   'I\'m feeling lucky!'
   * </pre>
   */
  @Override
  protected void visitRawTextNode(RawTextNode node) {
    // Escape special characters in the text before writing as a string.
    String exprText = BaseUtils.escapeToSoyString(node.getRawText(), false);
    pyExprs.add(new PyStringExpr(exprText));
  }

  /**
   * Visiting a print node accomplishes 3 basic tasks. It loads data, it performs any operations
   * needed, and it executes the appropriate print directives.
   *
   * <p>TODO(dcphillips): Add support for local variables once LetNode are supported.
   *
   * <p>Example:
   *
   * <pre>
   *   {$boo |changeNewlineToBr}
   *   {$goo + 5}
   * </pre>
   *
   * might generate
   *
   * <pre>
   *   sanitize.change_newline_to_br(data.get('boo'))
   *   data.get('goo') + 5
   * </pre>
   */
  @Override
  protected void visitPrintNode(PrintNode node) {
    TranslateToPyExprVisitor translator =
        new TranslateToPyExprVisitor(localVarExprs, errorReporter);

    PyExpr pyExpr = translator.exec(node.getExpr());

    // Process directives.
    for (PrintDirectiveNode directiveNode : node.getChildren()) {

      // Get directive.
      SoyPySrcPrintDirective directive = soyPySrcDirectivesMap.get(directiveNode.getName());
      if (directive == null) {
        throw LegacyInternalSyntaxException.createWithMetaInfo(
            "Failed to find SoyPySrcPrintDirective with name '"
                + directiveNode.getName()
                + "'"
                + " (tag "
                + node.toSourceString()
                + ")",
            directiveNode.getSourceLocation());
      }

      // Get directive args.
      List<ExprRootNode> args = directiveNode.getArgs();
      if (!directive.getValidArgsSizes().contains(args.size())) {
        throw LegacyInternalSyntaxException.createWithMetaInfo(
            "Print directive '"
                + directiveNode.getName()
                + "' used with the wrong number of"
                + " arguments (tag "
                + node.toSourceString()
                + ").",
            directiveNode.getSourceLocation());
      }

      // Translate directive args.
      List<PyExpr> argsPyExprs = new ArrayList<>(args.size());
      for (ExprRootNode arg : args) {
        argsPyExprs.add(translator.exec(arg));
      }

      // Apply directive.
      pyExpr = directive.applyForPySrc(pyExpr, argsPyExprs);
    }

    pyExprs.add(pyExpr);
  }

  @Override
  protected void visitMsgFallbackGroupNode(MsgFallbackGroupNode node) {
    PyExpr msg =
        msgFuncGeneratorFactory.create(node.getMsg(), localVarExprs, errorReporter).getPyExpr();

    // MsgFallbackGroupNode could only have one child or two children. See MsgFallbackGroupNode.
    if (node.hasFallbackMsg()) {
      StringBuilder pyExprTextSb = new StringBuilder();
      PyExpr fallbackMsg =
          msgFuncGeneratorFactory
              .create(node.getFallbackMsg(), localVarExprs, errorReporter)
              .getPyExpr();

      // Build Python ternary expression: a if cond else c
      pyExprTextSb.append(msg.getText()).append(" if ");

      // The fallback message is only used if the first message is not available, but the fallback
      // is. So availability of both messages must be tested.
      long firstId = MsgUtils.computeMsgIdForDualFormat(node.getMsg());
      long secondId = MsgUtils.computeMsgIdForDualFormat(node.getFallbackMsg());
      pyExprTextSb
          .append(PyExprUtils.TRANSLATOR_NAME)
          .append(".is_msg_available(")
          .append(firstId)
          .append(")")
          .append(" or not ")
          .append(PyExprUtils.TRANSLATOR_NAME)
          .append(".is_msg_available(")
          .append(secondId)
          .append(")");

      pyExprTextSb.append(" else ").append(fallbackMsg.getText());
      msg =
          new PyStringExpr(
              pyExprTextSb.toString(), PyExprUtils.pyPrecedenceForOperator(Operator.CONDITIONAL));
    }

    // Escaping directives apply to messages, especially in attribute context.
    for (String directiveName : node.getEscapingDirectiveNames()) {
      SoyPySrcPrintDirective directive = soyPySrcDirectivesMap.get(directiveName);
      Preconditions.checkNotNull(
          directive, "Contextual autoescaping produced a bogus directive: %s", directiveName);
      msg = directive.applyForPySrc(msg, ImmutableList.<PyExpr>of());
    }
    pyExprs.add(msg);
  }

  @Override
  protected void visitXidNode(XidNode node) {
    // preliminary xid implementation.  just uses the css renaming map
    StringBuilder sb =
        new StringBuilder("runtime.get_xid_name('").append(node.getText()).append("')");
    pyExprs.add(new PyExpr(sb.toString(), Integer.MAX_VALUE));
  }

  @Override
  protected void visitCssNode(CssNode node) {
    StringBuilder sb = new StringBuilder("runtime.get_css_name(");

    ExprRootNode componentNameExpr = node.getComponentNameExpr();
    if (componentNameExpr != null) {
      TranslateToPyExprVisitor translator =
          new TranslateToPyExprVisitor(localVarExprs, errorReporter);
      PyExpr basePyExpr = translator.exec(componentNameExpr);
      sb.append(basePyExpr.getText()).append(", ");
    }

    sb.append("'").append(node.getSelectorText()).append("')");
    pyExprs.add(new PyExpr(sb.toString(), Integer.MAX_VALUE));
  }

  /**
   * If all the children are computable as expressions, the IfNode can be written as a ternary
   * conditional expression.
   */
  @Override
  protected void visitIfNode(IfNode node) {
    // Create another instance of this visitor for generating Python expressions from children.
    GenPyExprsVisitor genPyExprsVisitor =
        genPyExprsVisitorFactory.create(localVarExprs, errorReporter);
    TranslateToPyExprVisitor translator =
        new TranslateToPyExprVisitor(localVarExprs, errorReporter);

    StringBuilder pyExprTextSb = new StringBuilder();

    boolean hasElse = false;
    for (SoyNode child : node.getChildren()) {

      if (child instanceof IfCondNode) {
        IfCondNode icn = (IfCondNode) child;

        // Python ternary conditional expressions modify the order of the conditional from
        // <conditional> ? <true> : <false> to
        // <true> if <conditional> else <false>
        PyExpr condBlock = PyExprUtils.concatPyExprs(genPyExprsVisitor.exec(icn)).toPyString();
        condBlock =
            PyExprUtils.maybeProtect(
                condBlock, PyExprUtils.pyPrecedenceForOperator(Operator.CONDITIONAL));
        pyExprTextSb.append(condBlock.getText());

        // Append the conditional and if/else syntax.
        PyExpr condPyExpr = translator.exec(icn.getExpr());
        pyExprTextSb.append(" if ").append(condPyExpr.getText()).append(" else ");

      } else if (child instanceof IfElseNode) {
        hasElse = true;
        IfElseNode ien = (IfElseNode) child;

        PyExpr elseBlock = PyExprUtils.concatPyExprs(genPyExprsVisitor.exec(ien)).toPyString();
        pyExprTextSb.append(elseBlock.getText());
      } else {
        throw new AssertionError("Unexpected if child node type. Child: " + child);
      }
    }

    if (!hasElse) {
      pyExprTextSb.append("''");
    }

    // By their nature, inline'd conditionals can only contain output strings, so they can be
    // treated as a string type with a conditional precedence.
    pyExprs.add(
        new PyStringExpr(
            pyExprTextSb.toString(), PyExprUtils.pyPrecedenceForOperator(Operator.CONDITIONAL)));
  }

  @Override
  protected void visitIfCondNode(IfCondNode node) {
    visitChildren(node);
  }

  @Override
  protected void visitIfElseNode(IfElseNode node) {
    visitChildren(node);
  }

  @Override
  protected void visitCallNode(CallNode node) {
    pyExprs.add(genPyCallExprVisitor.exec(node, localVarExprs, errorReporter));
  }

  @Override
  protected void visitCallParamContentNode(CallParamContentNode node) {
    visitChildren(node);
  }
}
