/*
 * 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.jbcsrc;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.template.soy.jbcsrc.BytecodeUtils.COMPILED_TEMPLATE_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.SOY_VALUE_PROVIDER_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.compareSoyEquals;
import static com.google.template.soy.jbcsrc.BytecodeUtils.constant;
import static com.google.template.soy.jbcsrc.BytecodeUtils.constantNull;
import static com.google.template.soy.jbcsrc.Statement.NULL_STATEMENT;
import static com.google.template.soy.jbcsrc.TemplateVariableManager.SaveStrategy.DERIVED;
import static com.google.template.soy.jbcsrc.TemplateVariableManager.SaveStrategy.STORE;

import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.data.SoyRecord;
import com.google.template.soy.data.internal.ParamStore;
import com.google.template.soy.data.restricted.IntegerData;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.jbcsrc.ControlFlow.IfBlock;
import com.google.template.soy.jbcsrc.ExpressionCompiler.BasicExpressionCompiler;
import com.google.template.soy.jbcsrc.MsgCompiler.SoyNodeToStringCompiler;
import com.google.template.soy.jbcsrc.TemplateVariableManager.SaveStrategy;
import com.google.template.soy.jbcsrc.TemplateVariableManager.Scope;
import com.google.template.soy.jbcsrc.TemplateVariableManager.Variable;
import com.google.template.soy.jbcsrc.shared.RenderContext;
import com.google.template.soy.msgs.internal.MsgUtils;
import com.google.template.soy.msgs.internal.MsgUtils.MsgPartsAndIds;
import com.google.template.soy.soytree.AbstractReturningSoyNodeVisitor;
import com.google.template.soy.soytree.CallBasicNode;
import com.google.template.soy.soytree.CallDelegateNode;
import com.google.template.soy.soytree.CallNode;
import com.google.template.soy.soytree.CallNode.DataAttribute;
import com.google.template.soy.soytree.CallParamContentNode;
import com.google.template.soy.soytree.CallParamNode;
import com.google.template.soy.soytree.CallParamValueNode;
import com.google.template.soy.soytree.CssNode;
import com.google.template.soy.soytree.DebuggerNode;
import com.google.template.soy.soytree.ForNode;
import com.google.template.soy.soytree.ForNode.RangeArgs;
import com.google.template.soy.soytree.ForeachIfemptyNode;
import com.google.template.soy.soytree.ForeachNode;
import com.google.template.soy.soytree.ForeachNonemptyNode;
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.LetContentNode;
import com.google.template.soy.soytree.LetValueNode;
import com.google.template.soy.soytree.LogNode;
import com.google.template.soy.soytree.MsgFallbackGroupNode;
import com.google.template.soy.soytree.MsgHtmlTagNode;
import com.google.template.soy.soytree.MsgNode;
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.BlockNode;
import com.google.template.soy.soytree.SoyNode.RenderUnitNode;
import com.google.template.soy.soytree.SwitchCaseNode;
import com.google.template.soy.soytree.SwitchDefaultNode;
import com.google.template.soy.soytree.SwitchNode;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.XidNode;

import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import java.util.ArrayList;
import java.util.List;

/**
 * Compiles {@link SoyNode soy nodes} into {@link Statement statements}.
 *
 * <p>The normal contract for {@link Statement statements} is that they leave the state of the
 * runtime stack unchanged before and after execution.  The SoyNodeCompiler requires that the
 * runtime stack be <em>empty</em> prior to any of the code produced.
 */
final class SoyNodeCompiler extends AbstractReturningSoyNodeVisitor<Statement> {
  // TODO(lukes): consider introducing a Builder or a non-static Factory.

  /**
   * Creates a SoyNodeCompiler
   *
   * @param innerClasses The current set of inner classes
   * @param stateField The field on the current class that holds the state variable
   * @param thisVar An expression that returns 'this'
   * @param appendableVar An expression that returns the current AdvisingAppendable that we are
   *     rendering into
   * @param variables The variable set for generating locals and fields
   * @param parameterLookup The variable lookup table for reading locals.
   */
  static SoyNodeCompiler create(
      CompiledTemplateRegistry registry,
      InnerClasses innerClasses,
      FieldRef stateField,
      Expression thisVar,
      AppendableExpression appendableVar,
      TemplateVariableManager variables,
      TemplateParameterLookup parameterLookup) {
    DetachState detachState = new DetachState(variables, thisVar, stateField);
    ExpressionCompiler expressionCompiler =
        ExpressionCompiler.create(detachState, parameterLookup, variables);
    ExpressionToSoyValueProviderCompiler soyValueProviderCompiler =
        ExpressionToSoyValueProviderCompiler.create(expressionCompiler, parameterLookup);
    return new SoyNodeCompiler(
        thisVar,
        registry,
        detachState,
        variables,
        parameterLookup,
        appendableVar,
        expressionCompiler,
        soyValueProviderCompiler,
        new LazyClosureCompiler(
            registry, innerClasses, parameterLookup, variables, soyValueProviderCompiler));
  }

  private final Expression thisVar;
  private final CompiledTemplateRegistry registry;
  private final DetachState detachState;
  private final TemplateVariableManager variables;
  private final TemplateParameterLookup parameterLookup;
  private final AppendableExpression appendableExpression;
  private final ExpressionCompiler exprCompiler;
  private final ExpressionToSoyValueProviderCompiler expressionToSoyValueProviderCompiler;
  private final LazyClosureCompiler lazyClosureCompiler;
  private Scope currentScope;

  SoyNodeCompiler(
      Expression thisVar,
      CompiledTemplateRegistry registry,
      DetachState detachState,
      TemplateVariableManager variables,
      TemplateParameterLookup parameterLookup,
      AppendableExpression appendableExpression,
      ExpressionCompiler exprCompiler,
      ExpressionToSoyValueProviderCompiler expressionToSoyValueProviderCompiler,
      LazyClosureCompiler lazyClosureCompiler) {
    this.thisVar = checkNotNull(thisVar);
    this.registry = checkNotNull(registry);
    this.detachState = checkNotNull(detachState);
    this.variables = checkNotNull(variables);
    this.parameterLookup = checkNotNull(parameterLookup);
    this.appendableExpression = checkNotNull(appendableExpression);
    this.exprCompiler = checkNotNull(exprCompiler);
    this.expressionToSoyValueProviderCompiler = checkNotNull(expressionToSoyValueProviderCompiler);
    this.lazyClosureCompiler = checkNotNull(lazyClosureCompiler);
  }

  @AutoValue abstract static class CompiledMethodBody {
    static CompiledMethodBody create(Statement body, int numDetaches) {
      return new AutoValue_SoyNodeCompiler_CompiledMethodBody(body, numDetaches);
    }
    abstract Statement body();
    abstract int numberOfDetachStates();
  }

  CompiledMethodBody compile(TemplateNode node) {
    Statement templateBody = visit(node);
    return getCompiledBody(templateBody);
  }

  CompiledMethodBody compileChildren(RenderUnitNode node) {
    Statement templateBody = visitChildrenInNewScope(node);
    return getCompiledBody(templateBody);
  }

  private CompiledMethodBody getCompiledBody(Statement templateBody) {
    Statement jumpTable = detachState.generateReattachTable();
    return CompiledMethodBody.create(
        Statement.concat(jumpTable, templateBody),
        detachState.getNumberOfDetaches());
  }

  @Override protected Statement visit(SoyNode node) {
    try {
      return super.visit(node);
    } catch (UnexpectedCompilerFailureException e) {
      e.addLocation(node.getSourceLocation());
      throw e;
    } catch (Throwable t) {
      throw new UnexpectedCompilerFailureException(node.getSourceLocation(), t);
    }
  }

  @Override protected Statement visitTemplateNode(TemplateNode node) {
    return visitChildrenInNewScope(node);
  }

  private Statement visitChildrenInNewScope(BlockNode node) {
    Scope prev = currentScope;
    currentScope = variables.enterScope();
    List<Statement> children = visitChildren(node);
    Statement leave = currentScope.exitScope();
    children.add(leave);
    currentScope = prev;
    return Statement.concat(children).withSourceLocation(node.getSourceLocation());
  }

  @Override protected Statement visitIfNode(IfNode node) {
    List<IfBlock> ifs = new ArrayList<>();
    Optional<Statement> elseBlock = Optional.absent();
    for (SoyNode child : node.getChildren()) {
      if (child instanceof IfCondNode) {
        IfCondNode icn = (IfCondNode) child;
        SoyExpression cond = exprCompiler.compile(icn.getExprUnion().getExpr()).coerceToBoolean();
        Statement block = visitChildrenInNewScope(icn);
        ifs.add(IfBlock.create(cond, block));
      } else {
        IfElseNode ien = (IfElseNode) child;
        elseBlock = Optional.of(visitChildrenInNewScope(ien));
      }
    }
    return ControlFlow.ifElseChain(ifs, elseBlock).withSourceLocation(node.getSourceLocation());
  }

  @Override protected Statement visitSwitchNode(SwitchNode node) {
    // A few special cases:
    // 1. only a {default} block.  In this case we can skip all the switch logic and temporaries
    // 2. no children.  Just return the empty statement
    // Note that in both of these cases we do not evalutate (or generate code) for the switch
    // expression.
    List<SoyNode> children = node.getChildren();
    if (children.isEmpty()) {
      return Statement.NULL_STATEMENT;
    }
    if (children.size() == 1 && children.get(0) instanceof SwitchDefaultNode) {
      return visitChildrenInNewScope((SwitchDefaultNode) children.get(0));
    }
    // otherwise we need to evaluate the predicate and generate dispatching logic.
    SoyExpression expression = exprCompiler.compile(node.getExpr());
    Statement init;
    List<IfBlock> cases = new ArrayList<>();
    Optional<Statement> defaultBlock = Optional.absent();
    Scope scope = variables.enterScope();
    Variable variable = scope.createSynthetic(SyntheticVarName.forSwitch(node), expression, STORE);
    init = variable.initializer();
    expression = expression.withSource(variable.local());

    for (SoyNode child : children) {
      if (child instanceof SwitchCaseNode) {
        SwitchCaseNode caseNode = (SwitchCaseNode) child;
        Label reattachPoint = new Label();
        List<Expression> comparisons = new ArrayList<>();
        for (ExprRootNode caseExpr : caseNode.getExprList()) {
          comparisons.add(compareSoyEquals(expression,
              exprCompiler.compile(caseExpr, reattachPoint)));
        }
        Expression condition = BytecodeUtils.logicalOr(comparisons).labelStart(reattachPoint);
        Statement block = visitChildrenInNewScope(caseNode);
        cases.add(IfBlock.create(condition, block));
      } else {
        SwitchDefaultNode defaultNode = (SwitchDefaultNode) child;
        defaultBlock = Optional.of(visitChildrenInNewScope(defaultNode));
      }
    }
    Statement exitScope = scope.exitScope();

    // Soy allows arbitrary expressions to appear in {case} statements within a {switch}.
    // Java/C, by contrast, only allow some constant expressions in cases.
    // TODO(lukes): in practice the case statements are often constant strings/ints.  If everything
    // is typed to int/string we should consider implementing via the tableswitch/lookupswitch
    // instruction which would be way way way faster.  cglib has some helpers for string switch
    // generation that we could maybe use
    return Statement.concat(init, ControlFlow.ifElseChain(cases, defaultBlock), exitScope)
        .withSourceLocation(node.getSourceLocation());
  }

  @Override protected Statement visitForNode(ForNode node) {
    // Despite appearances, range() is not a soy function, it is essentially a keyword that only
    // works in for loops, there are 3 forms.
    // {for $i in range(3)}{$i}{/for} -> 0 1 2
    // {for $i in range(2, 5)} ... {/for} -> 2 3 4
    // {for $i in range(2, 8, 2)} ... {/for} -> 2 4 6

    Scope scope = variables.enterScope();
    final CompiledRangeArgs rangeArgs = calculateRangeArgs(node, scope);

    final Statement loopBody = visitChildrenInNewScope(node);

    // Note it is important that exitScope is called _after_ the children are visited.
    // TODO(lukes): this is somewhat error-prone... we could maybe manage it by have the scope
    // maintain a sequence of statements and then all statements would be added to Scope which would
    // return a statement for the whole thing at the end... would that be clearer?
    final Statement exitScope = scope.exitScope();
    return new Statement(node.getSourceLocation()) {
      @Override void doGen(CodeBuilder adapter) {
        for (Statement initializer : rangeArgs.initStatements()) {
          initializer.gen(adapter);
        }
        // We need to check for an empty loop by doing an entry test
        Label loopStart = adapter.mark();

        // If current >= limit we are done
        rangeArgs.currentIndex().gen(adapter);
        rangeArgs.limit().gen(adapter);
        Label end = new Label();
        adapter.ifCmp(Type.INT_TYPE, Opcodes.IFGE, end);

        loopBody.gen(adapter);

        // at the end of the loop we need to increment and jump back.
        rangeArgs.increment().gen(adapter);
        adapter.goTo(loopStart);
        adapter.mark(end);
        exitScope.gen(adapter);
      }
    };
  }

  @AutoValue abstract static class CompiledRangeArgs {
    /** Current loop index. */
    abstract Expression currentIndex();

    /** Where to end loop iteration, defaults to {@code 0}. */
    abstract Expression limit();

    /** This statement will increment the index by the loop stride. */
    abstract Statement increment();

    /** Statements that must have been run prior to using any of the above expressions. */
    abstract ImmutableList<Statement> initStatements();
  }

  /**
   * Interprets the given expressions as the arguments of a {@code range(...)} expression in a
   * {@code for} loop.
   */
  private CompiledRangeArgs calculateRangeArgs(ForNode forNode, Scope scope) {
    RangeArgs rangeArgs = forNode.getRangeArgs();

    final ImmutableList.Builder<Statement> initStatements = ImmutableList.builder();
    final Variable currentIndex;
    if (rangeArgs.start().isPresent()) {
      Label startDetachPoint = new Label();
      Expression startIndex =
          MethodRef.INTS_CHECKED_CAST.invoke(
              exprCompiler.compile(rangeArgs.start().get(), startDetachPoint).unboxAs(long.class));
      currentIndex = scope.create(forNode.getVarName(), startIndex, STORE);
      initStatements.add(currentIndex.initializer().labelStart(startDetachPoint));
    } else {
      currentIndex = scope.create(forNode.getVarName(), constant(0), STORE);
      initStatements.add(currentIndex.initializer());
    }

    final Statement incrementCurrentIndex;
    if (rangeArgs.increment().isPresent()) {
      Label detachPoint = new Label();
      Expression increment =
          MethodRef.INTS_CHECKED_CAST.invoke(
              exprCompiler.compile(rangeArgs.increment().get(), detachPoint).unboxAs(long.class));
      // If the expression is non-trivial, make sure to save it to a field.
      final Variable incrementVariable =
          scope.createSynthetic(
              SyntheticVarName.forLoopIncrement(forNode),
              increment,
              increment.isCheap() ? DERIVED : STORE);
      initStatements.add(incrementVariable.initializer().labelStart(detachPoint));
      incrementVariable.local();
      incrementCurrentIndex = new Statement() {
        @Override void doGen(CodeBuilder adapter) {
          currentIndex.local().gen(adapter);
          incrementVariable.local().gen(adapter);
          adapter.visitInsn(Opcodes.IADD);
          adapter.visitVarInsn(Opcodes.ISTORE, currentIndex.local().index());
        }
      };
    } else {
      incrementCurrentIndex = new Statement() {
        @Override void doGen(CodeBuilder adapter) {
          adapter.iinc(currentIndex.local().index(), 1);
        }
      };
    }

    Label detachPoint = new Label();
    Expression limit =
        MethodRef.INTS_CHECKED_CAST.invoke(
            exprCompiler.compile(rangeArgs.limit(), detachPoint).unboxAs(long.class));
    // If the expression is non-trivial we should cache it in a local variable
    Variable variable =
        scope.createSynthetic(
            SyntheticVarName.forLoopLimit(forNode), limit, limit.isCheap() ? DERIVED : STORE);
    initStatements.add(variable.initializer().labelStart(detachPoint));
    limit = variable.local();

    return new AutoValue_SoyNodeCompiler_CompiledRangeArgs(
        currentIndex.local(), limit, incrementCurrentIndex, initStatements.build());
  }

  @Override protected Statement visitForeachNode(ForeachNode node) {
    ForeachNonemptyNode nonEmptyNode = (ForeachNonemptyNode) node.getChild(0);
    SoyExpression expr = exprCompiler.compile(node.getExpr()).unboxAs(List.class);
    Scope scope = variables.enterScope();
    final Variable listVar =
        scope.createSynthetic(SyntheticVarName.foreachLoopList(nonEmptyNode), expr, STORE);
    final Variable indexVar =
        scope.createSynthetic(SyntheticVarName.foreachLoopIndex(nonEmptyNode), constant(0), STORE);
    final Variable listSizeVar =
        scope.createSynthetic(SyntheticVarName.foreachLoopLength(nonEmptyNode),
            MethodRef.LIST_SIZE.invoke(listVar.local()), DERIVED);
    final Variable itemVar =
        scope.create(
            nonEmptyNode.getVarName(),
            MethodRef.LIST_GET
                .invoke(listVar.local(), indexVar.local())
                .cast(SOY_VALUE_PROVIDER_TYPE),
            SaveStrategy.DERIVED);
    final Statement loopBody = visitChildrenInNewScope(nonEmptyNode);
    final Statement exitScope = scope.exitScope();

    // it important for this to be generated after exitScope is called (or before enterScope)
    final Statement emptyBlock = node.numChildren() == 2
        ? visitChildrenInNewScope((ForeachIfemptyNode) node.getChild(1))
        : null;
    return new Statement() {
      @Override void doGen(CodeBuilder adapter) {
        listVar.initializer().gen(adapter);
        listSizeVar.initializer().gen(adapter);
        listSizeVar.local().gen(adapter);
        Label emptyListLabel = new Label();
        adapter.ifZCmp(Opcodes.IFEQ, emptyListLabel);
        indexVar.initializer().gen(adapter);
        Label loopStart = adapter.mark();
        itemVar.initializer().gen(adapter);

        loopBody.gen(adapter);

        adapter.iinc(indexVar.local().index(), 1);  // index++
        indexVar.local().gen(adapter);
        listSizeVar.local().gen(adapter);
        adapter.ifICmp(Opcodes.IFLT, loopStart);  // if index < list.size(), goto loopstart
        // exit the loop
        exitScope.gen(adapter);

        if (emptyBlock != null) {
          Label skipIfEmptyBlock = new Label();
          adapter.goTo(skipIfEmptyBlock);
          adapter.mark(emptyListLabel);
          emptyBlock.gen(adapter);
          adapter.mark(skipIfEmptyBlock);
        } else {
          adapter.mark(emptyListLabel);
        }
      }
    };
  }

  @Override protected Statement visitPrintNode(PrintNode node) {
    // First check our special case for compatible content types (no print directives) and an
    // expression that evaluates to a SoyValueProvider.  This will allow us to render incrementally
    if (node.getChildren().isEmpty()) {
      Label reattachPoint = new Label();
      ExprRootNode expr = node.getExprUnion().getExpr();
      Optional<Expression> asSoyValueProvider =
          expressionToSoyValueProviderCompiler.compileAvoidingBoxing(expr, reattachPoint);
      if (asSoyValueProvider.isPresent()) {
        return renderIncrementally(node, asSoyValueProvider.get(), reattachPoint);
      }
    }
    // otherwise we need to do some escapes or simply cannot do incremental rendering
    Label reattachPoint = new Label();
    SoyExpression value = compilePrintNodeAsExpression(node, reattachPoint);
    AppendableExpression renderSoyValue =
        appendableExpression.appendString(value.coerceToString()).labelStart(reattachPoint);
    return detachState.detachLimited(renderSoyValue)
        .withSourceLocation(node.getSourceLocation());
  }

  private SoyExpression compilePrintNodeAsExpression(PrintNode node, Label reattachPoint) {
    BasicExpressionCompiler basic = exprCompiler.asBasicCompiler(reattachPoint);
    SoyExpression value = basic.compile(node.getExprUnion().getExpr());
    // We may have print directives, that means we need to pass the render value through a bunch of
    // SoyJavaPrintDirective.apply methods.  This means lots and lots of boxing.
    // TODO(user): tracks adding streaming print directives which would help with this,
    // because instead of wrapping the soy value, we would just wrap the appendable.
    for (PrintDirectiveNode printDirective : node.getChildren()) {
      value =
          value.applyPrintDirective(
              parameterLookup.getRenderContext(),
              printDirective.getName(),
              basic.compileToList(printDirective.getArgs()));
    }
    return value;
  }

  /**
   * TODO(lukes): if the expression is a param, then this is kind of silly since it looks like
   * <pre>{@code
   *   SoyValueProvider localParam = this.param;
   *   this.currentRenderee = localParam;
   *   SoyValueProvider localRenderee = this.currentRenderee;
   *   localRenderee.renderAndResolve();
   * }</pre>
   *
   * <p>In this case we could elide the currentRenderee altogether if we knew the soyValueProvider
   * expression was just a field read... And this is the _common_case for .renderAndResolve calls.
   * to actually do this we could add a mechanism similar to the SaveStrategy enum for expressions,
   * kind of like {@link Expression#isCheap()} which isn't that useful in practice.
   */
  private Statement renderIncrementally(PrintNode node, Expression soyValueProvider,
      Label reattachPoint) {
    // In this case we want to render the SoyValueProvider via renderAndResolve which will
    // enable incremental rendering of parameters for lazy transclusions!
    // This actually ends up looking a lot like how calls work so we use the same strategy.
    FieldRef currentRendereeField = variables.getCurrentRenderee();
    Statement initRenderee =
        currentRendereeField.putInstanceField(thisVar, soyValueProvider)
            .labelStart(reattachPoint);

    // This cast will always succeed.
    Expression callRenderAndResolve = currentRendereeField.accessor(thisVar)
        .invoke(MethodRef.SOY_VALUE_PROVIDER_RENDER_AND_RESOLVE,
            appendableExpression,
            // the isLast param
            // TODO(lukes): pass a real value here when we have expression use analysis.
            constant(false));
    Statement doCall = detachState.detachForRender(callRenderAndResolve);
    Statement clearRenderee =
        currentRendereeField.putInstanceField(thisVar, constantNull(SOY_VALUE_PROVIDER_TYPE));
    return Statement.concat(initRenderee, doCall, clearRenderee)
        .withSourceLocation(node.getSourceLocation());
  }

  @Override protected Statement visitRawTextNode(RawTextNode node) {
    AppendableExpression render =
        appendableExpression.appendString(constant(node.getRawText(), variables));
    // TODO(lukes): add some heuristics about when to add this
    // ideas:
    // * never try to detach in certain 'contexts' (e.g. attribute context)
    // * never detach after rendering small chunks (< 128 bytes?)
    return detachState.detachLimited(render).withSourceLocation(node.getSourceLocation());
  }

  @Override protected Statement visitDebuggerNode(DebuggerNode node) {
    // intentional no-op.  java has no 'breakpoint' equivalent.  But we can add a label + line
    // number.  Which may be useful for debugging :)
    return NULL_STATEMENT.withSourceLocation(node.getSourceLocation());
  }

  // Note: xid and css translations are expected to be very short, so we do _not_ generate detaches
  // for them, even though they write to the output.

  @Override protected Statement visitXidNode(XidNode node) {
    return appendableExpression
        .appendString(
            parameterLookup
                .getRenderContext()
                .invoke(MethodRef.RENDER_CONTEXT_RENAME_XID, constant(node.getText())))
        .toStatement()
        .withSourceLocation(node.getSourceLocation());
  }

  // TODO(lukes):  The RenderVisitor optimizes css/xid renaming by stashing a one element cache in
  // the CSS node itself (keyed off the identity of the renaming map).  We could easily add such
  // an optimization via a static field in the Template class. Though im not sure it makes sense
  // as an optimization... this should just be an immutable map lookup keyed off of a constant
  // string. If we cared a lot, we could employ a simpler (and more compact) optimization by
  // assigning each selector a unique integer id and then instead of hashing we can just reference
  // an array (aka perfect hashing).  This could be part of our runtime library and ids could be
  // assigned at startup.

  @Override protected Statement visitCssNode(CssNode node) {
    Expression renamedSelector =
        parameterLookup
            .getRenderContext()
            .invoke(MethodRef.RENDER_CONTEXT_RENAME_CSS_SELECTOR, constant(node.getSelectorText()));

    if (node.getComponentNameExpr() != null) {
      Label reattachPoint = new Label();
      SoyExpression compiledComponent =
          exprCompiler.compile(node.getComponentNameExpr(), reattachPoint).coerceToString();
      return appendableExpression
          .appendString(compiledComponent)
          .appendChar(constant('-'))
          .appendString(renamedSelector)
          .labelStart(reattachPoint)
          .toStatement()
          .withSourceLocation(node.getSourceLocation());
    }
    return appendableExpression.appendString(renamedSelector)
        .toStatement()
        .withSourceLocation(node.getSourceLocation());
  }

  /**
   * MsgFallbackGroupNodes have either one or two children.  In the 2 child case the second child is
   * the {@code {fallbackmsg}} entry.  For this we generate code that looks like:
   * <pre> {@code
   *   if (renderContext.hasMsg(primaryId)) {
   *     <render primary msg>
   *   } else {
   *     <render fallback msg>
   *   }
   * }</pre>
   *
   * <p>All of the logic for actually rendering {@code msg} nodes is handled by the
   * {@link MsgCompiler}.
   */
  @Override protected Statement visitMsgFallbackGroupNode(MsgFallbackGroupNode node) {
    MsgNode msg = node.getMsg();
    MsgPartsAndIds idAndParts = MsgUtils.buildMsgPartsAndComputeMsgIdForDualFormat(msg);
    ImmutableList<String> escapingDirectives = node.getEscapingDirectiveNames();
    Statement renderDefault = getMsgCompiler().compileMessage(idAndParts, msg, escapingDirectives);
    // fallback groups have 1 or 2 children.  if there are 2 then the second is a fallback and we
    // need to check for presence.
    if (node.hasFallbackMsg()) {
      MsgNode fallback = node.getFallbackMsg();
      MsgPartsAndIds fallbackIdAndParts =
          MsgUtils.buildMsgPartsAndComputeMsgIdForDualFormat(fallback);
      IfBlock ifAvailableRenderDefault =
          IfBlock.create(
              parameterLookup
                  .getRenderContext()
                  .invoke(
                      MethodRef.RENDER_CONTEXT_USE_PRIMARY_MSG,
                      constant(idAndParts.id),
                      constant(fallbackIdAndParts.id)),
              renderDefault);
      return ControlFlow.ifElseChain(
          ImmutableList.of(ifAvailableRenderDefault),
          Optional.of(
              getMsgCompiler().compileMessage(fallbackIdAndParts, fallback, escapingDirectives)));
    } else {
      return renderDefault;
    }
  }

  /**
   * Given this delcall:
   * {@code {delcall foo.bar variant="$expr" allowemptydefault="true"}}
   *
   * Generate code that looks like:
   * <pre> {@code
   *   renderContext.getDeltemplate("foo.bar", <variant-expression>, true)
   *       .create(<prepareParameters>, ijParams)
   *       .render(appendable, renderContext)
   *
   * }</pre>
   *
   * <p>We share logic with {@link #visitCallBasicNode(CallBasicNode)} around the actual calling
   * convention (setting up detaches, storing the template in a field). As well as the logic for
   * preparing the data record.  The only interesting part of delcalls is calculating the
   * {@code variant} and the fact that we have to invoke the {@link RenderContext} runtime to do
   * the deltemplate lookup.
   */
  @Override protected Statement visitCallDelegateNode(CallDelegateNode node) {
    Label reattachPoint = new Label();
    Expression variantExpr;
    if (node.getDelCalleeVariantExpr() == null) {
      variantExpr = constant("");
    } else {
      variantExpr =
          exprCompiler.compile(node.getDelCalleeVariantExpr(), reattachPoint).coerceToString();
    }
    Expression calleeExpression =
        parameterLookup
            .getRenderContext()
            .invoke(
                MethodRef.RENDER_CONTEXT_GET_DELTEMPLATE,
                constant(node.getDelCalleeName()),
                variantExpr,
                constant(node.allowsEmptyDefault()),
                prepareParamsHelper(node, reattachPoint),
                parameterLookup.getIjRecord());
    if (!node.getEscapingDirectiveNames().isEmpty()) {
      Expression directives = getEscapingDirectivesList(node);
      if (registry.hasDelTemplateDefinition(node.getDelCalleeName())) {
        ContentKind kind = registry.getDelTemplateContentKind(node.getDelCalleeName());
        calleeExpression =
            MethodRef.RUNTIME_APPLY_ESCAPERS.invoke(
                calleeExpression, BytecodeUtils.constant(kind), directives);
      } else {
        // only use dynamic resolution if we need to, to avoid runtime kind checks
        calleeExpression =
            MethodRef.RUNTIME_APPLY_ESCAPERS_DYNAMIC.invoke(calleeExpression, directives);
      }
    }
    return visitCallNodeHelper(
        node,
        reattachPoint,
        calleeExpression);
  }

  @Override protected Statement visitCallBasicNode(CallBasicNode node) {
    // Basic nodes are basic! We can just call the node directly.
    CompiledTemplateMetadata callee = registry.getTemplateInfoByTemplateName(node.getCalleeName());
    Label reattachPoint = new Label();
    Expression calleeExpression =
        callee
            .constructor()
            .construct(prepareParamsHelper(node, reattachPoint), parameterLookup.getIjRecord());
    if (!node.getEscapingDirectiveNames().isEmpty()) {
      calleeExpression =
          MethodRef.RUNTIME_APPLY_ESCAPERS.invoke(
              calleeExpression,
              BytecodeUtils.constant(callee.node().getContentKind()),
              getEscapingDirectivesList(node));
    }
    return visitCallNodeHelper(
        node,
        reattachPoint,
        calleeExpression);
  }

  private Statement visitCallNodeHelper(CallNode node,
      Label reattachPoint,
      Expression calleeExpression) {
    FieldRef currentCalleeField = variables.getCurrentCalleeField();
    Statement initCallee =
        currentCalleeField.putInstanceField(thisVar, calleeExpression).labelStart(reattachPoint);

    Expression callRender =
        currentCalleeField
            .accessor(thisVar)
            .invoke(
                MethodRef.COMPILED_TEMPLATE_RENDER,
                appendableExpression,
                parameterLookup.getRenderContext());
    Statement callCallee = detachState.detachForRender(callRender);
    Statement clearCallee =
        currentCalleeField.putInstanceField(
            thisVar, BytecodeUtils.constantNull(COMPILED_TEMPLATE_TYPE));
    return Statement.concat(initCallee, callCallee, clearCallee)
        .withSourceLocation(node.getSourceLocation());
  }

  private Expression getEscapingDirectivesList(CallNode node) {
    List<Expression> directiveExprs = new ArrayList<>(node.getEscapingDirectiveNames().size());
    for (String directive : node.getEscapingDirectiveNames()) {
      directiveExprs.add(
          parameterLookup
              .getRenderContext()
              .invoke(MethodRef.RENDER_CONTEXT_GET_PRINT_DIRECTIVE, constant(directive)));
    }
    return BytecodeUtils.asList(directiveExprs);
  }

  private Expression prepareParamsHelper(CallNode node, Label reattachPoint) {
    DataAttribute dataAttribute = node.dataAttribute();
    if (node.numChildren() == 0) {
      // Easy, just use the data attribute
      return getDataExpression(dataAttribute, reattachPoint);
    } else {
      // Otherwise we need to build a dictionary from {param} statements.
      Expression paramStoreExpression = getParamStoreExpression(node, dataAttribute, reattachPoint);
      for (CallParamNode child : node.getChildren()) {
        String paramKey = child.getKey();
        Expression valueExpr;
        if (child instanceof CallParamContentNode) {
          valueExpr = lazyClosureCompiler.compileLazyContent(
              "param", (CallParamContentNode) child, paramKey);
        } else {
          valueExpr = lazyClosureCompiler.compileLazyExpression(
              "param", child, paramKey, ((CallParamValueNode) child).getValueExprUnion().getExpr());
        }
        // ParamStore.setField return 'this' so we can just chain the invocations together.
        paramStoreExpression =
            MethodRef.PARAM_STORE_SET_FIELD
                .invoke(paramStoreExpression, BytecodeUtils.constant(paramKey), valueExpr);
      }
      return paramStoreExpression;
    }
  }

  /**
   * Returns an expression that creates a new {@link ParamStore} suitable for holding all the
   */
  private Expression getParamStoreExpression(CallNode node, DataAttribute dataAttribute,
      Label reattachPoint) {
    Expression paramStoreExpression;
    if (dataAttribute.isPassingData()) {
      paramStoreExpression = ConstructorRef.AUGMENTED_PARAM_STORE.construct(
          getDataExpression(dataAttribute, reattachPoint), constant(node.numChildren()));
    } else {
      paramStoreExpression = ConstructorRef.BASIC_PARAM_STORE.construct(
          constant(node.numChildren()));
    }
    return paramStoreExpression;
  }

  private Expression getDataExpression(DataAttribute dataAttribute, Label reattachPoint) {
    if (dataAttribute.isPassingData()) {
      if (dataAttribute.isPassingAllData()) {
        return parameterLookup.getParamsRecord();
      } else {
        return exprCompiler.compile(dataAttribute.dataExpr(), reattachPoint).box()
            .cast(SoyRecord.class);
      }
    } else {
      return FieldRef.EMPTY_DICT.accessor();
    }
  }

  @Override protected Statement visitLogNode(LogNode node) {
    return compilerWithNewAppendable(AppendableExpression.logger())
        .visitChildrenInNewScope(node);
  }

  @Override protected Statement visitLetValueNode(LetValueNode node) {
    Expression newLetValue =
        lazyClosureCompiler.compileLazyExpression(
            "let", node, node.getVarName(), node.getValueExpr());
    return currentScope.create(node.getVarName(), newLetValue, STORE).initializer();
  }

  @Override protected Statement visitLetContentNode(LetContentNode node) {
    Expression newLetValue =
        lazyClosureCompiler.compileLazyContent("let", node, node.getVarName());
    return currentScope.create(node.getVarName(), newLetValue, STORE).initializer();
  }

  @Override protected Statement visitMsgHtmlTagNode(MsgHtmlTagNode node) {
    // trivial node that is just a number of children surrounded by raw text nodes.
    return Statement.concat(visitChildren(node)).withSourceLocation(node.getSourceLocation());
  }

  @Override protected Statement visitSoyNode(SoyNode node) {
    throw new UnsupportedOperationException(
        "The jbcsrc backend doesn't support: " + node.getKind() + " nodes yet.");
  }

  private MsgCompiler getMsgCompiler() {
    return new MsgCompiler(
        thisVar,
        detachState,
        variables,
        parameterLookup,
        appendableExpression,
        new SoyNodeToStringCompiler() {
          @Override
          public Statement compileToBuffer(
              MsgHtmlTagNode htmlTagNode, AppendableExpression appendable) {
            return compilerWithNewAppendable(appendable).visit(htmlTagNode);
          }

          @Override
          public Expression compileToString(PrintNode node, Label reattachPoint) {
            return compilePrintNodeAsExpression(node, reattachPoint).coerceToString();
          }

          @Override
          public Statement compileToBuffer(CallNode call, AppendableExpression appendable) {
            // TODO(lukes): in the case that CallNode has to be escaped we will render all the bytes
            // into a buffer, box it into a soy value, escape it, then copy the bytes into this
            // buffer.  Consider optimizing at least one of the buffer copies away.
            return compilerWithNewAppendable(appendable).visit(call);
          }

          @Override
          public Expression compileToString(ExprRootNode node, Label reattachPoint) {
            return exprCompiler.compile(node, reattachPoint).coerceToString();
          }

          @Override
          public Expression compileToInt(ExprRootNode node, Label reattachPoint) {
            return exprCompiler.compile(node, reattachPoint).box().cast(IntegerData.class);
          }
        });
  }

  /** Returns a {@link SoyNodeCompiler} identical to this one but with an alternate appendable. */
  private SoyNodeCompiler compilerWithNewAppendable(AppendableExpression appendable) {
    return new SoyNodeCompiler(
        thisVar,
        registry,
        detachState,
        variables,
        parameterLookup,
        appendable,
        exprCompiler,
        expressionToSoyValueProviderCompiler,
        lazyClosureCompiler);
  }
}
