/*
 * Copyright 2014 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.sharedpasses.render;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.template.soy.data.SoyRecord;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.data.SoyValueProvider;
import com.google.template.soy.data.restricted.BooleanData;
import com.google.template.soy.data.restricted.IntegerData;
import com.google.template.soy.data.restricted.NullData;
import com.google.template.soy.data.restricted.UndefinedData;
import com.google.template.soy.exprtree.VarDefn;
import com.google.template.soy.exprtree.VarDefn.Kind;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.defn.LoopVar;
import com.google.template.soy.soytree.defn.TemplateParam;

/**
 * The local variable table.
 * 
 * <p>All declared {@code @param}s and {@code {let ...}} statements define variables that are 
 * stored in a table.  The mapping between local variable and 
 * 
 * <p>New empty environments can be created with the {@link #create} factory method and seeded with
 * the {@link #bind} method.
 * 
 * <p>For the most part this class is only used by this package, but it is publicly exposed to aid
 * in testing usecases.
 */
public abstract class Environment {
  Environment() {}  // package private constructor to limit subclasses to this package.

  /** 
   * The main way to create an environment. 
   * 
   * <p>Allocates the local variable table for the template and prepopulates it with data from the
   * given SoyRecords.
   */ 
  static Environment create(TemplateNode template, SoyRecord data, SoyRecord ijData) {
    return new Impl(template, data, ijData);
  }

  /**
   * For Prerendering we create an {@link Environment} for the given template where all entries are
   * initialized to UndefinedData.
   */
  public static Environment prerenderingEnvironment() {
    return new EmptyImpl();
  }

  /** Associates a value with the given variable. */
  abstract void bind(VarDefn var, SoyValueProvider value);
  
  /** Sets the 'isLast' boolean for the given LoopVar. */
  abstract void bindIsLast(LoopVar loopVar, boolean isLast);
  
  /** Sets the currentIndex for the given LoopVar. */
  abstract void bindCurrentIndex(LoopVar loopVar, int lastIndex);
  
  /** Returns the resolved SoyValue for the given VarDefn.  Guaranteed to not return null. */
  abstract SoyValue getVar(VarDefn var);

  /** Returns the resolved SoyValue for the given VarDefn.  Guaranteed to not return null. */
  abstract SoyValueProvider getVarProvider(VarDefn var);
  
  /** Returns {@code true} if we are the last iteration for the given loop variable. */
  abstract boolean isLast(LoopVar loopVar);
  
  /** Returns the current iterator inject for the given loop variable. */
  abstract int getIndex(LoopVar loopVar);

  private static final class Impl extends Environment {
    final SoyValueProvider[] localVariableTable;
    final SoyRecord data;
    
    Impl(TemplateNode template, SoyRecord data, SoyRecord ijData) {
      // seed the lvt with the params
      this.localVariableTable = new SoyValueProvider[template.getMaxLocalVariableTableSize()];
      this.data = data;
      for (TemplateParam param : template.getAllParams()) {
        SoyValueProvider provider = 
            (param.isInjected() ? ijData : data).getFieldProvider(param.name());
        if (provider == null) {
          provider = param.isRequired() ? UndefinedData.INSTANCE : NullData.INSTANCE;
        }
        localVariableTable[param.localVariableIndex()] = provider;
      }
    }

    @Override void bind(VarDefn var, SoyValueProvider value) {
      localVariableTable[var.localVariableIndex()] = value;
    }

    @Override void bindIsLast(LoopVar loopVar, boolean isLast) {
      localVariableTable[loopVar.isLastIteratorIndex()] = BooleanData.forValue(isLast);
    }

    @Override void bindCurrentIndex(LoopVar loopVar, int lastIndex) {
      localVariableTable[loopVar.currentLoopIndexIndex()] = IntegerData.forValue(lastIndex);
    }

    @Override SoyValueProvider getVarProvider(VarDefn var) {
      if (var.kind() == Kind.UNDECLARED) {
        // Special case for legacy templates with undeclared params.  Undeclared params aren't
        // assigned indices in the local variable table.
        SoyValueProvider provider = data.getFieldProvider(var.name());
        return provider != null ? provider : checkNotNull(UndefinedData.INSTANCE);
      }
      return localVariableTable[var.localVariableIndex()];
    }

    @Override SoyValue getVar(VarDefn var) {
      if (var.kind() == Kind.UNDECLARED) {
        // Special case for legacy templates with undeclared params.  Undeclared params aren't
        // assigned indices in the local variable table.
        SoyValue value = data.getField(var.name());
        return value != null ? value : checkNotNull(UndefinedData.INSTANCE);
      }
      return localVariableTable[var.localVariableIndex()].resolve();
    }

    @Override boolean isLast(LoopVar loopVar) {
      return localVariableTable[loopVar.isLastIteratorIndex()].resolve().booleanValue();
    }

    @Override int getIndex(LoopVar loopVar) {
      return localVariableTable[loopVar.currentLoopIndexIndex()].resolve().integerValue();
    }
  }

  /**
   * An environment that is empty and returns {@link UndefinedData} for everything.
   */
  private static final class EmptyImpl extends Environment {
    @Override void bind(VarDefn var, SoyValueProvider value) {
      throw new UnsupportedOperationException();
    }

    @Override void bindIsLast(LoopVar loopVar, boolean isLast) {
      throw new UnsupportedOperationException();
    }

    @Override void bindCurrentIndex(LoopVar loopVar, int lastIndex) {
      throw new UnsupportedOperationException();
    }

    @Override SoyValueProvider getVarProvider(VarDefn var) {
      return UndefinedData.INSTANCE;
    }

    @Override SoyValue getVar(VarDefn var) {
      return UndefinedData.INSTANCE;
    }

    @Override boolean isLast(LoopVar loopVar) {
      return UndefinedData.INSTANCE.booleanValue();
    }

    @Override int getIndex(LoopVar loopVar) {
      return UndefinedData.INSTANCE.integerValue();
    }
  }
}
