/*
 * 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.checkArgument;
import static com.google.template.soy.jbcsrc.BytecodeUtils.constant;
import static com.google.template.soy.jbcsrc.StandardNames.CURRENT_CALLEE_FIELD;
import static com.google.template.soy.jbcsrc.StandardNames.CURRENT_RENDEREE_FIELD;
import static com.google.template.soy.jbcsrc.StandardNames.MSG_PLACEHOLDER_MAP_FIELD;
import static com.google.template.soy.jbcsrc.StandardNames.TEMP_BUFFER_FIELD;

import com.google.auto.value.AutoValue;
import com.google.common.collect.Sets;
import com.google.template.soy.data.SoyValueProvider;
import com.google.template.soy.jbcsrc.TemplateVariableManager.VarKey.Kind;
import com.google.template.soy.jbcsrc.api.AdvisingStringBuilder;
import com.google.template.soy.jbcsrc.shared.CompiledTemplate;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;

/**
 * A variable in this set is a SoyValue that must be saved/restored.  This means each variable has:
 *
 * <ul>
 *     <li>A {@link FieldRef} that can be used to define the field.
 *     <li>A {@link Statement} that can be used to save the field.
 *     <li>A {@link Statement} that can be used to restore the field.
 *     <li>A {@link LocalVariable} that can be used to read the value.
 * </ul>
 *
 * <p>This class also manages other template level resources, like:
 * <ul>
 *     <li>A {@link #getTempBufferField() buffer} that can be used to render msg placeholders.
 *     <li>A {@link #getCurrentCalleeField() callee field} that holds the current template that is
 *         being called.
 *     <li>A {@link #getCurrentRenderee() renderee field} that holds the current incrementally
 *         rendering {@link SoyValueProvider} that is being called.
 *     <li>A {@link #getMsgPlaceholderMapField() map} that can be used to store message
 *         placeholders.
 * </ul>
 *
 * <p>Finally, this class can be used to generate static, per template fields for storing state.
 * Currently, this is used to store default english messages and raw text constants > 64K in
 * length.
 */
final class TemplateVariableManager {
  enum SaveStrategy {
    /** Means that the value of the variable should be recalculated rather than saved to a field. */
    DERIVED,
    /** Means that the value of the variable should saved to a field. */
    STORE;
  }

  abstract class Scope {
    private Scope() {}

    /**
     * Creates a new 'synthetic' variable.  A synthetic variable is a variable that is
     * introduced by the compiler rather than a user defined name.
     *
     * @param name A proposed name for the variable, the actual variable name may be modified to 
     *     ensure uniqueness
     * @param initializer The expression that can be used to derive the initial value.  Note, this
     *     expression must be save to gen() more than once if {@code isDerived} is {@code true}
     * @param strategy Set this if the value of the variable is trivially derivable from other 
     *     variables already defined.
     */
    abstract Variable createSynthetic(SyntheticVarName name, Expression initializer, 
        SaveStrategy strategy);

    /**
     * Creates a new 'synthetic' variable.  A synthetic variable is a variable that is
     * introduced by the compiler rather than a user defined name.
     *
     * @param name The name of the variable, the name is assumed to be unique (enforced by the 
     *     ResolveNamesVisitor).
     * @param initializer The expression that can be used to initialize the variable
     */
    abstract Variable create(String name, Expression initializer, SaveStrategy strategy);

    /**
     * Returns a statement that should be used when exiting the scope.  This is responsible for
     * appropriately clearing fields and visiting end labels.
     */
    abstract Statement exitScope();
  }

  /**
   * A sufficiently unique identifier.
   * 
   * <p>This key will uniquely identify a currently 'active' variable, but may not be unique over
   * all possible variables.
   */
  @AutoValue abstract static class VarKey {
    enum Kind {
      /** 
       * Includes @param, @inject, {let..}, and loop vars.
       * 
       * <p>Uniqueness of local variable names is enforced by the ResolveNamesVisitor pass, we
       * just need uniqueness for the field names 
       */
      USER_DEFINED,
      /**
       * There are certain operations in which a value must be used multiple times and may have
       * expensive initialization. For example, the collection being looped over in a
       * {@code foreach} loop.  For these we generate 'synthetic' variables to efficiently reference
       * the expression.
       */
      SYNTHETIC;
    }
    static VarKey create(Kind synthetic, String proposedName) {
      return new AutoValue_TemplateVariableManager_VarKey(synthetic, proposedName);
    }

    abstract Kind kind();
    abstract String name();
  }

  /**
   * A variable that may need to be saved/restored.
   */
  abstract class Variable {
    protected final Expression initExpression;
    protected final LocalVariable local;
    private final Statement initializer;

    private Variable(Expression initExpression, LocalVariable local) {
      this.initExpression = initExpression;
      this.local = local;
      this.initializer = local.store(initExpression, local.start());
    }

    final Statement initializer() {
      return initializer;
    }

    abstract Statement save();

    abstract Statement restore();

    abstract void maybeDefineField(ClassVisitor writer);

    LocalVariable local() {
      return local;
    }
  }

  private final class FieldSavedVariable extends Variable {
    private FieldRef fieldRef;  // lazily allocated on a save/restore operation

    private FieldSavedVariable(Expression initExpression, LocalVariable local) {
      super(initExpression, local);
    }

    @Override Statement save() {
      return getField().putInstanceField(thisVar, local);
    }

    @Override Statement restore() {
      Expression fieldValue = getField().accessor(thisVar);
      return local.store(fieldValue);
    }

    private FieldRef getField() {
      if (fieldRef == null) {
        fieldRef = FieldRef.createField(owner, local.variableName(), local.resultType());
      }
      return fieldRef;
    }

    @Override void maybeDefineField(ClassVisitor writer) {
      if (fieldRef != null) {
        fieldRef.defineField(writer);
      }
    }
  }
  
  private final class DerivedVariable extends Variable {
    private DerivedVariable(Expression initExpression, LocalVariable local) {
      super(initExpression, local);
    }

    @Override Statement save() {
      return Statement.NULL_STATEMENT;
    }

    @Override Statement restore() {
      return local.store(initExpression);
    }

    @Override void maybeDefineField(ClassVisitor writer) {}
  }
  
  @AutoValue
  abstract static class StaticFieldVariable {
    abstract FieldRef field();

    abstract Expression initializer();
  }

  private final List<Variable> allVariables = new ArrayList<>();
  private final Deque<Map<VarKey, Variable>> frames = new ArrayDeque<>();
  private final List<StaticFieldVariable> staticFields = new ArrayList<>();
  private final UniqueNameGenerator fieldNames;
  private final BitSet availableSlots = new BitSet();
  private final TypeInfo owner;
  private final LocalVariable thisVar;
  // Allocated lazily
  @Nullable private FieldRef currentCalleeField; 
  // Allocated lazily
  @Nullable private FieldRef currentRendereeField; 
  // Allocated lazily
  @Nullable private FieldRef tempBufferField;
  // Allocated lazily
  @Nullable private FieldRef msgPlaceholderMapField;
  private int msgPlaceholderMapInitialSize = 0;

  /**
   * @param owner The type that is the owner of the method being generated
   * @param thisVar An expression returning the current 'this' reference
   * @param method The method being generated
   * @param fieldNames The field name set for the current class.
   */
  TemplateVariableManager(
      UniqueNameGenerator fieldNames, TypeInfo owner, LocalVariable thisVar, Method method) {
    this.fieldNames = fieldNames;
    this.fieldNames.claimName(CURRENT_CALLEE_FIELD);
    this.fieldNames.claimName(CURRENT_RENDEREE_FIELD);
    this.fieldNames.claimName(TEMP_BUFFER_FIELD);
    this.fieldNames.claimName(MSG_PLACEHOLDER_MAP_FIELD);
    this.owner = owner;
    this.thisVar = thisVar;
    availableSlots.set(0);   // for 'this'
    int from = 1;
    for (Type type : method.getArgumentTypes()) {
      int to = from + type.getSize();
      availableSlots.set(from, to);
      from = to;
    }
  }

  /**
   * Enters a new scope.  Variables may only be defined within a scope.
   */
  Scope enterScope() {
    final Map<VarKey, Variable> currentFrame = new LinkedHashMap<>();
    final Label scopeExit = new Label();
    frames.push(currentFrame);
    return new Scope() {
      @Override
      Variable createSynthetic(
          SyntheticVarName varName, Expression initExpr, SaveStrategy strategy) {
        VarKey key = VarKey.create(Kind.SYNTHETIC, varName.name());
        // synthetics are prefixed by $ by convention
        String name = fieldNames.generateName("$" + varName.name());
        return doCreate(name, new Label(), scopeExit, initExpr, key, strategy);
      }

      @Override
      Variable create(String name, Expression initExpr, SaveStrategy strategy) {
        VarKey key = VarKey.create(Kind.USER_DEFINED, name);
        name = fieldNames.generateName(name);
        return doCreate(name, new Label(), scopeExit, initExpr, key, strategy);
      }

      @Override
      Statement exitScope() {
        frames.pop();
        // Use identity semantics to make sure we visit each label at most once.  visiting a label
        // more than once tends to corrupt internal asm state.
        final Set<Label> endLabels = Sets.newIdentityHashSet();
        for (Variable var : currentFrame.values()) {
          endLabels.add(var.local.end());
          availableSlots.clear(
              var.local.index(), var.local.index() + var.local.resultType().getSize());
        }
        return new Statement() {
          // TODO(lukes): we could generate null writes for when object typed fields go out of
          // scope.  This would potentially allow intermediate results to be collected sooner.
          @Override
          void doGen(CodeBuilder adapter) {
            for (Label label : endLabels) {
              adapter.visitLabel(label);
            }
          }
        };
      }

      private Variable doCreate(
          String name,
          Label start,
          Label end,
          Expression initExpr,
          VarKey key,
          SaveStrategy strategy) {
        int index = reserveSlotFor(initExpr.resultType());
        LocalVariable local =
            LocalVariable.createLocal(name, index, initExpr.resultType(), start, end);
        Variable var;
        switch (strategy) {
          case DERIVED:
            var = new DerivedVariable(initExpr, local);
            break;
          case STORE:
            var = new FieldSavedVariable(initExpr, local);
            break;
          default:
            throw new AssertionError();
        }
        currentFrame.put(key, var);
        allVariables.add(var);
        return var;
      }
    };
  }

  /** Write a local variable table entry for every registered variable. */
  void generateTableEntries(CodeBuilder ga) {
    for (Variable var : allVariables) {
      var.local.tableEntry(ga);
    }
  }

  
  /**
   * Adds a private static final field and returns a reference to it.
   *
   * @param proposedName  The proposed name,  the actual name will be this plus an optional suffix
   *     to ensure uniqueness
   * @param initializer An expression to initialize the field.
   */
  FieldRef addStaticField(String proposedName, Expression initializer) {
    String name = fieldNames.generateName(proposedName);
    FieldRef ref =
        new AutoValue_FieldRef(
            owner,
            name,
            initializer.resultType(),
            Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_PRIVATE,
            !initializer.isNonNullable());
    staticFields.add(new AutoValue_TemplateVariableManager_StaticFieldVariable(ref, initializer));
    return ref;
  }

  // TODO(lukes): consider moving all these optional 'one per template' fields to a different object
  // for management.

  /** 
   * Defines all the fields necessary for the registered variables.
   * 
   * @return a statement to initialize the fields
   */
  @CheckReturnValue Statement defineFields(ClassVisitor writer) {
    List<Statement> initializers = new ArrayList<>();
    for (Variable var : allVariables) {
      var.maybeDefineField(writer);
    }
    if (currentCalleeField != null) {
      currentCalleeField.defineField(writer);
    }
    if (currentRendereeField != null) {
      currentRendereeField.defineField(writer);
    }
    if (tempBufferField != null) {
      tempBufferField.defineField(writer);
      // If a template needs a temp buffer then we initialize it eagerly in the template constructor
      // this may be wasteful in the case that the buffer is only used on certain call paths, but
      // if it turns out to be expensive, this could always be solved by an author by refactoring
      // their templates (e.g. extract the conditional logic into another template)
      final Expression newStringBuilder = ConstructorRef.ADVISING_STRING_BUILDER.construct();
      initializers.add(new Statement() {
        @Override void doGen(CodeBuilder adapter) {
          adapter.loadThis();
          newStringBuilder.gen(adapter);
          tempBufferField.putUnchecked(adapter);
        }
      });
    }
    if (msgPlaceholderMapField != null) {
      msgPlaceholderMapField.defineField(writer);
      // same comment as above about eager initialization.
      final Expression newHashMap =
          ConstructorRef.LINKED_HASH_MAP_SIZE.construct(constant(msgPlaceholderMapInitialSize));
      initializers.add(new Statement() {
        @Override void doGen(CodeBuilder adapter) {
          adapter.loadThis();
          newHashMap.gen(adapter);
          msgPlaceholderMapField.putUnchecked(adapter);
        }
      });
    }
    return Statement.concat(initializers);
  }
  
  /**
   * Adds definitions for all the static fields managed by this variable set and adds a
   * {@code <clinit>} method to the given class.
   *
   * @param writer The class to add the fields and static initializer to.
   */
  void defineStaticFields(ClassVisitor writer) {
    if (staticFields.isEmpty()) {
      return;
    }
    List<Statement> statements = new ArrayList<>();
    for (StaticFieldVariable staticField : staticFields) {
      staticField.field().defineField(writer);
      statements.add(staticField.field().putStaticField(staticField.initializer()));
    }
    statements.add(Statement.RETURN);
    Statement.concat(statements).writeMethod(Opcodes.ACC_STATIC, BytecodeUtils.CLASS_INIT, writer);
  }

  /**
   * Returns the field that holds the current callee template.
   * 
   * <p>Unlike normal variables the VariableSet doesn't maintain responsibility for saving and
   * restoring the current callee to a local.
   */
  FieldRef getCurrentCalleeField() {
    FieldRef local = currentCalleeField;
    if (local == null) {
      local = currentCalleeField = 
          FieldRef.createField(owner, CURRENT_CALLEE_FIELD, CompiledTemplate.class);
    }
    return local;
  }

  /**
   * Returns the field that holds the current temp buffer.
   */
  FieldRef getTempBufferField() {
    FieldRef local = tempBufferField;
    if (local == null) {
      local = tempBufferField =
          FieldRef.createFinalField(owner, TEMP_BUFFER_FIELD, AdvisingStringBuilder.class)
              .asNonNull();
    }
    return local;
  }

  /**
   * Returns the field that holds a map used for rendering msg placeholders.
   */
  FieldRef getMsgPlaceholderMapField() {
    FieldRef local = msgPlaceholderMapField;
    if (local == null) {
      local = msgPlaceholderMapField =
          FieldRef.createFinalField(owner, MSG_PLACEHOLDER_MAP_FIELD, LinkedHashMap.class)
              .asNonNull();
    }
    return local;
  }

  /**
   * Configures a minimum size for the {@link #getMsgPlaceholderMapField()}.
   */
  void setMsgPlaceholderMapMinSize(int size) {
    // we use the max of all the requested minimum sizes for the initial size
    this.msgPlaceholderMapInitialSize = Math.max(msgPlaceholderMapInitialSize, size);
  }

  /**
   * Returns the field that holds the currently rendering SoyValueProvider.
   * 
   * <p>Unlike normal variables the VariableSet doesn't maintain responsibility for saving and
   * restoring the current renderee to a local.
   */
  FieldRef getCurrentRenderee() {
    FieldRef local = currentRendereeField;
    if (local == null) {
      local = currentRendereeField = 
          FieldRef.createField(owner, CURRENT_RENDEREE_FIELD, SoyValueProvider.class);
    }
    return local;
  }

  /**
   * Looks up a user defined variable with the given name.  The variable must have been created
   * in a currently active scope.
   */
  Variable getVariable(String name) {
    VarKey varKey = VarKey.create(Kind.USER_DEFINED, name);
    return getVariable(varKey);
  }

  /**
   * Looks up a synthetic variable with the given name.  The variable must have been created
   * in a currently active scope.
   */
  Variable getVariable(SyntheticVarName name) {
    VarKey varKey = VarKey.create(Kind.SYNTHETIC, name.name());
    return getVariable(varKey);
  }

  private Variable getVariable(VarKey varKey) {
    Variable potentialMatch = null;
    for (Map<VarKey, Variable> f : frames) {
      Variable variable = f.get(varKey);
      if (variable != null) {
        if (potentialMatch == null) {
          potentialMatch = variable;
        } else {
          throw new IllegalArgumentException("Ambiguous variable: " + varKey);
        }
      }
    }
    if (potentialMatch != null) {
      return potentialMatch;
    }
    throw new IllegalArgumentException("No variable: '" + varKey + "' is bound");
  }

  /** Statements for saving and restoring local variables in class fields. */
  @AutoValue abstract static class SaveRestoreState {
    abstract Statement save();
    abstract Statement restore();
  }

  /** Returns a {@link SaveRestoreState} for the current state of the variable set. */
  SaveRestoreState saveRestoreState() {
    List<Statement> saves = new ArrayList<>();
    List<Statement> restores = new ArrayList<>();
    // Iterate backwards so that we restore variables in order of definition which will ensure that
    // derived fields work correctly.
    for (Iterator<Map<VarKey, Variable>> iterator = frames.descendingIterator();
        iterator.hasNext();) {
      Map<VarKey, Variable> frame = iterator.next();
      for (Variable var : frame.values()) {
        saves.add(var.save());
        restores.add(var.restore());
      }
    }
    return new AutoValue_TemplateVariableManager_SaveRestoreState(
        Statement.concat(saves), Statement.concat(restores));
  }

  private int reserveSlotFor(Type type) {
    int size = type.getSize();
    checkArgument(size != 0);
    int start = 0;
    while (true) {
      int nextClear = availableSlots.nextClearBit(start);
      if (size == 2 && availableSlots.get(nextClear + 1)) {
        start = nextClear + 1;
      }
      availableSlots.set(nextClear, nextClear + size);
      return nextClear;
    }
  }
}

