/*
 * 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.OBJECT;
import static com.google.template.soy.jbcsrc.BytecodeUtils.RENDER_CONTEXT_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.SOY_LIST_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.constant;
import static com.google.template.soy.jbcsrc.BytecodeUtils.logicalNot;

import com.google.common.base.Optional;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.soytree.CallNode;
import com.google.template.soy.soytree.MsgNode;
import com.google.template.soy.soytree.PrintNode;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.SoyType.Kind;
import com.google.template.soy.types.aggregate.ListType;
import com.google.template.soy.types.primitive.BoolType;
import com.google.template.soy.types.primitive.FloatType;
import com.google.template.soy.types.primitive.IntType;
import com.google.template.soy.types.primitive.NullType;
import com.google.template.soy.types.primitive.SanitizedType;
import com.google.template.soy.types.primitive.StringType;
import com.google.template.soy.types.primitive.UnknownType;
import java.util.ArrayList;
import java.util.List;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

/**
 * An Expression involving a soy value.
 *
 * <p>SoyExpressions can be {@link #box() boxed} into SoyValue subtypes and they also support some
 * implicit conversions.
 *
 * <p>All soy expressions are convertable to {@code boolean} or {@link String} valued expressions,
 * but depending on the type they may also support additional unboxing conversions.
 */
class SoyExpression extends Expression {

  static SoyExpression forSoyValue(SoyType type, Expression delegate) {
    return new SoyExpression(
        SoyRuntimeType.getBoxedType(type), delegate, Optional.<Expression>absent());
  }

  static SoyExpression forBool(Expression delegate) {
    return new SoyExpression(getUnboxedTypeUnchecked(BoolType.getInstance()), delegate);
  }

  static SoyExpression forFloat(Expression delegate) {
    return new SoyExpression(getUnboxedTypeUnchecked(FloatType.getInstance()), delegate);
  }

  static SoyExpression forInt(Expression delegate) {
    return new SoyExpression(getUnboxedTypeUnchecked(IntType.getInstance()), delegate);
  }

  static SoyExpression forString(Expression delegate) {
    return new SoyExpression(getUnboxedTypeUnchecked(StringType.getInstance()), delegate);
  }

  static SoyExpression forSanitizedString(Expression delegate, ContentKind kind) {
    return new SoyExpression(
        getUnboxedTypeUnchecked(SanitizedType.getTypeForContentKind(kind)), delegate);
  }

  static SoyExpression forList(ListType listType, Expression delegate) {
    return new SoyExpression(getUnboxedTypeUnchecked(listType), delegate);
  }

  static SoyExpression forProto(
      SoyRuntimeType type, Expression delegate, Expression renderContext) {
    checkArgument(renderContext.resultType().equals(RENDER_CONTEXT_TYPE));
    return new SoyExpression(type, delegate, Optional.of(renderContext));
  }

  /**
   * Returns an Expression that evaluates to a list containing all the items as boxed soy values.
   */
  static Expression asBoxedList(List<SoyExpression> items) {
    List<Expression> childExprs = new ArrayList<>(items.size());
    for (SoyExpression child : items) {
      childExprs.add(child.box());
    }
    return BytecodeUtils.asList(childExprs);
  }
  
  static final SoyExpression NULL =
      new SoyExpression(
          getUnboxedTypeUnchecked(NullType.getInstance()),
          new Expression(OBJECT.type(), Feature.CHEAP) {
            @Override
            void doGen(CodeBuilder adapter) {
              adapter.visitInsn(Opcodes.ACONST_NULL);
            }
          });

  static final SoyExpression TRUE =
      new SoyExpression(
          getUnboxedTypeUnchecked(BoolType.getInstance()), BytecodeUtils.constant(true)) {
        @Override
        SoyExpression box() {
          return new DefaultBoxed(
              SoyRuntimeType.getBoxedType(BoolType.getInstance()),
              this,
              FieldRef.BOOLEAN_DATA_TRUE.accessor(),
              Optional.<Expression>absent());
        }
      };

  static final SoyExpression FALSE =
      new SoyExpression(
          getUnboxedTypeUnchecked(BoolType.getInstance()), BytecodeUtils.constant(false)) {
        @Override
        SoyExpression box() {
          return new DefaultBoxed(
              SoyRuntimeType.getBoxedType(BoolType.getInstance()),
              this,
              FieldRef.BOOLEAN_DATA_FALSE.accessor(),
              Optional.<Expression>absent());
        }
      };

  private static SoyRuntimeType getUnboxedTypeUnchecked(SoyType soyType) {
    return SoyRuntimeType.getUnboxedType(soyType).get();
  }

  private final SoyRuntimeType soyType;
  private final Expression delegate;
  private final Optional<Expression> renderContext;

  private SoyExpression(SoyRuntimeType soyType, Expression delegate) {
    this(soyType, delegate, Optional.<Expression>absent());
  }

  private SoyExpression(
      SoyRuntimeType soyType, Expression delegate, Optional<Expression> renderContext) {
    super(delegate.resultType(), delegate.features());
    checkArgument(
        BytecodeUtils.isPossiblyAssignableFrom(soyType.runtimeType(), delegate.resultType()),
        "delegate with type %s isn't compatible with asserted SoyExpression type %s",
        delegate.resultType(),
        soyType.runtimeType());
    this.soyType = soyType;
    this.delegate = delegate;
    this.renderContext = renderContext;
  }

  /** Returns the {@link SoyType} of the expression. */
  final SoyType soyType() {
    return soyRuntimeType().soyType();
  }

  final SoyRuntimeType soyRuntimeType() {
    return soyType;
  }

  @Override final void doGen(CodeBuilder adapter) {
    delegate.gen(adapter);
  }

  boolean assignableToNullableInt() {
    return soyRuntimeType().assignableToNullableInt();
  }

  boolean assignableToNullableFloat() {
    return soyRuntimeType().assignableToNullableFloat();
  }

  boolean assignableToNullableNumber() {
    return soyRuntimeType().assignableToNullableNumber();
  }

  boolean isKnownList() {
    return soyRuntimeType().isKnownList();
  }

  boolean isBoxed() {
    return soyRuntimeType().isBoxed();
  }

  /** Returns a SoyExpression that evaluates to a subtype of {@link SoyValue}. */
  SoyExpression box() {
    if (isBoxed()) {
      return this;
    }
    if (soyType.soyType().equals(NullType.getInstance())) {
      return this;
    }
    // If null is expected and it is a reference type we want to propagate null through the boxing
    // operation
    if (!delegate.isNonNullable()) {
      // now prefix with a null check and then box so null is preserved via 'boxing'
      final Label end = new Label();
      return withSource(
              new Expression(resultType(), features()) {
                @Override
                void doGen(CodeBuilder adapter) {
                  delegate.gen(adapter);
                  BytecodeUtils.nullCoalesce(adapter, end);
                }
              })
          .asNonNullable()
          .box()
          .asNullable()
          .labelEnd(end);
    }
    if (soyRuntimeType().isKnownBool()) {
      return asBoxed(MethodRef.BOOLEAN_DATA_FOR_VALUE.invoke(delegate));
    }
    if (soyRuntimeType().isKnownInt()) {
      return asBoxed(MethodRef.INTEGER_DATA_FOR_VALUE.invoke(delegate));
    }
    if (soyRuntimeType().isKnownFloat()) {
      return asBoxed(MethodRef.FLOAT_DATA_FOR_VALUE.invoke(delegate));
    }
    if (soyRuntimeType().isKnownSanitizedContent()) {
      return asBoxed(
          MethodRef.ORDAIN_AS_SAFE.invoke(
              delegate,
              FieldRef.enumReference(((SanitizedType) soyRuntimeType().soyType()).getContentKind())
                  .accessor()));
    }
    if (soyRuntimeType().isKnownString()) {
      return asBoxed(MethodRef.STRING_DATA_FOR_VALUE.invoke(delegate));
    }
    if (isKnownList()) {
      return asBoxed(MethodRef.LIST_IMPL_FOR_PROVIDER_LIST.invoke(delegate));
    }
    if (soyRuntimeType().isKnownProto()) {
      // dereference the context early in case our caller screwed up and we don't have one.
      final Expression context = renderContext.get();
      return asBoxed(
          new Expression(ProtoUtils.RENDER_CONTEXT_BOX.returnType(), delegate.features()) {
            @Override
            void doGen(CodeBuilder adapter) {
              delegate.gen(adapter);
              context.gen(adapter);
              // swap the two top items of the stack.
              // This ensures that the base expression is gen'd at a stack depth of zero, which is
              // important if it contains a label for a reattach point or if this is prefixed with a
              // null check.
              // TODO(lukes): this is super lame and is due to the fact that we generate expressions
              // with complex control flow that isn't completely encapsulated.   Ideas: inline all
              // the null checks so that the control flow is more encapsulated (though this will add
              // a lot of redundancy), promote the concept of an expression that must be gen()'d at
              // a depth of zero to a top level concept (an Expression Feature), so that we can flag
              // it more eagerly, something else?
              adapter.swap();
              ProtoUtils.RENDER_CONTEXT_BOX.invokeUnchecked(adapter);
            }
          });
    }
    throw new IllegalStateException("cannot box soy expression of type " + soyType);
  }

  private DefaultBoxed asBoxed(Expression expr) {
    return new DefaultBoxed(soyRuntimeType().box(), this, expr, renderContext);
  }

  /** Coerce this expression to a boolean value. */
  SoyExpression coerceToBoolean() {
    // First deal with primitives which don't have to care about null.
    if (BytecodeUtils.isPrimitive(resultType())) {
      return coercePrimitiveToBoolean();
    }
    if (soyType.soyType().equals(NullType.getInstance())) {
      return FALSE;
    }
    if (delegate.isNonNullable()) {
      return coerceNonNullableReferenceTypeToBoolean();
    } else {
      // If we are potentially nullable, then map null to false and run the normal logic recursively
      // for the non-nullable branch.
      final Label end = new Label();
      return withSource(
              new Expression(delegate.resultType(), delegate.features()) {
                @Override
                void doGen(CodeBuilder adapter) {
                  delegate.gen(adapter);
                  adapter.dup();
                  Label nonNull = new Label();
                  adapter.ifNonNull(nonNull);
                  adapter.pop();
                  adapter.pushBoolean(false);
                  adapter.goTo(end);
                  adapter.mark(nonNull);
                }
              })
          .asNonNullable()
          .coerceToBoolean()
          .labelEnd(end);
    }
  }

  private SoyExpression coercePrimitiveToBoolean() {
    if (resultType().equals(Type.BOOLEAN_TYPE)) {
      return this;
    } else if (resultType().equals(Type.DOUBLE_TYPE)) {
      return forBool(MethodRef.RUNTIME_COERCE_DOUBLE_TO_BOOLEAN.invoke(delegate));
    } else if (resultType().equals(Type.LONG_TYPE)) {
      return forBool(BytecodeUtils.compare(Opcodes.IFNE, delegate, BytecodeUtils.constant(0L)));
    } else {
      throw new AssertionError(
          "resultType(): " + resultType() + " is not a valid type for a SoyExpression");
    }
  }

  private SoyExpression coerceNonNullableReferenceTypeToBoolean() {
    if (isBoxed()) {
      // If we are boxed, just call the SoyValue method
      return forBool(delegate.invoke(MethodRef.SOY_VALUE_COERCE_TO_BOOLEAN));
    }
    // unboxed non-primitive types.  This would be strings, protos or lists
    if (soyRuntimeType().isKnownString()) {
      return forBool(logicalNot(delegate.invoke(MethodRef.STRING_IS_EMPTY)));
    }
    // All other types are always truthy, but we still need to eval the delegate in case it has
    // side effects or contains a null exit branch.
    return forBool(
            new Expression(Type.BOOLEAN_TYPE, delegate.features()) {
              @Override void doGen(CodeBuilder adapter) {
                delegate.gen(adapter);
                adapter.pop();
                adapter.pushBoolean(true);
              }
            });
  }

  /** Coerce this expression to a string value. */
  SoyExpression coerceToString() {
    if (soyRuntimeType().isKnownString() && !isBoxed()) {
      return this;
    }
    if (BytecodeUtils.isPrimitive(resultType())) {
      if (resultType().equals(Type.BOOLEAN_TYPE)) {
        return forString(MethodRef.BOOLEAN_TO_STRING.invoke(delegate));
      } else if (resultType().equals(Type.DOUBLE_TYPE)) {
        return forString(MethodRef.DOUBLE_TO_STRING.invoke(delegate));
      } else if (resultType().equals(Type.LONG_TYPE)) {
        return forString(MethodRef.LONG_TO_STRING.invoke(delegate));
      } else {
        throw new AssertionError(
            "resultType(): " + resultType() + " is not a valid type for a SoyExpression");
      }
    }
    if (!isBoxed()) {
      // this is for unboxed reference types (strings, lists, protos) String.valueOf handles null
      // implicitly
      return forString(MethodRef.STRING_VALUE_OF.invoke(delegate));
    }
    return forString(MethodRef.RUNTIME_COERCE_TO_STRING.invoke(delegate));
  }

  /**
   * Coerce to a double, useful for float-int comparisons
   */
  SoyExpression coerceToDouble() {
    if (!isBoxed()) {
      if (soyRuntimeType().isKnownFloat()) {
        return this;
      }
      if (soyRuntimeType().isKnownInt()) {
        return forFloat(BytecodeUtils.numericConversion(delegate, Type.DOUBLE_TYPE));
      }
      throw new UnsupportedOperationException("Can't convert " + resultType() + " to a double");
    }
    if (soyRuntimeType().isKnownFloat()) {
      return forFloat(delegate.invoke(MethodRef.SOY_VALUE_FLOAT_VALUE));
    }
    return forFloat(delegate.invoke(MethodRef.SOY_VALUE_NUMBER_VALUE));
  }

  // TODO(lukes): split this into a set of specialized methods, one per target type like we did
  // for the 'coerce' methods.

  /**
   * Unboxes this to a {@link SoyExpression} with a runtime type of {@code asType}.
   *
   * <p>This method is appropriate when you know (likely via inspection of the {@link #soyType()},
   * or other means) that the value does have the appropriate type but you prefer to interact with
   * it as its unboxed representation. If you simply want to 'coerce' the given value to a new type
   * consider {@link #coerceToBoolean()} {@link #coerceToDouble()} or {@link #coerceToString()}
   * which are designed for that use case.
   */
  SoyExpression unboxAs(Class<?> asType) {
    checkArgument(
        !SoyValue.class.isAssignableFrom(asType),
        "Cannot use unboxAs() to convert to a SoyValue: %s, use .box() instead",
        asType);
    // no op conversion, always allow.
    if (BytecodeUtils.isDefinitelyAssignableFrom(Type.getType(asType), soyType.runtimeType())) {
      return this;
    }
    if (!isBoxed()) {
      throw new IllegalStateException(
          "Trying to unbox an unboxed value ("
              + soyType
              + ") doesn't make sense, "
              + "should you be using a type coercion? e.g. .coerceToBoolean()");
    }
    if (asType.equals(boolean.class)) {
      return forBool(delegate.invoke(MethodRef.SOY_VALUE_BOOLEAN_VALUE));
    }
    if (asType.equals(long.class)) {
      return forInt(delegate.invoke(MethodRef.SOY_VALUE_LONG_VALUE));
    }
    if (asType.equals(double.class)) {
      return forFloat(delegate.invoke(MethodRef.SOY_VALUE_FLOAT_VALUE));
    }
    if (delegate.isNonNullable()) {
      if (asType.equals(String.class)) {
        Expression unboxedString = delegate.invoke(MethodRef.SOY_VALUE_STRING_VALUE);
        // We need to ensure that santized types don't lose their content kinds
        return soyRuntimeType().isKnownSanitizedContent()
            ? forSanitizedString(
                unboxedString, ((SanitizedType) soyType.soyType()).getContentKind())
            : forString(unboxedString);
      }
      if (asType.equals(List.class)) {
        return unboxAsList();
      }
    } else {
      // else it must be a List/Proto/String all of which must preserve null through the unboxing
      // operation
      final Label end = new Label();
      Expression nonNullDelegate =
          new Expression(resultType(), features()) {
            @Override void doGen(CodeBuilder adapter) {
              delegate.gen(adapter);
              BytecodeUtils.nullCoalesce(adapter, end);
            }
          };
      return withSource(nonNullDelegate).asNonNullable().unboxAs(asType).asNullable().labelEnd(end);
    }
    throw new UnsupportedOperationException("Can't unbox " + soyType + " as " + asType);
  }

  private SoyExpression unboxAsList() {
    ListType asListType;
    if (isKnownList()) {
      asListType = (ListType) soyType.soyType();
    } else {
      Kind kind = soyType.soyType().getKind();
      if (kind == Kind.UNKNOWN) {
        asListType = ListType.of(UnknownType.getInstance());
      } else {
        // The type checker should have already rejected all of these
        throw new UnsupportedOperationException("Cannot convert " + soyType + " to List");
      }
    }
    return forList(
        asListType, delegate.cast(SOY_LIST_TYPE).invoke(MethodRef.SOY_LIST_AS_JAVA_LIST));
  }

  /**
   * Returns a new {@link SoyExpression} with the same type but a new delegate expression.
   */
  SoyExpression withSource(Expression expr) {
    return new SoyExpression(soyType, expr, renderContext);
  }

  SoyExpression withRenderContext(Expression renderContext) {
    if (this.renderContext.isPresent() && this.renderContext.get().equals(renderContext)) {
      return this;
    }
    return new SoyExpression(soyType, delegate, Optional.of(renderContext));
  }

  /**
  * Applies a print directive to the soyValue, only useful for parameterless print directives such
  * as those applied to {@link MsgNode msg nodes} and {@link CallNode call nodes} for autoescaping.
  * For {@link PrintNode print nodes}, the directives may be parameterized by arbitrary soy 
  * expressions. 
  */
  SoyExpression applyPrintDirective(Expression renderContext, String directive) {
    return applyPrintDirective(renderContext, directive, MethodRef.IMMUTABLE_LIST_OF.invoke());
  }

  /**
  * Applies a print directive to the soyValue.
  */
  SoyExpression applyPrintDirective(
      Expression renderContext, String directive, Expression argsList) {
    // Technically the type is either StringData or SanitizedContent depending on this type, but 
    // boxed.  Consider propagating the type more accurately, currently there isn't (afaict) much
    // benefit (and strangely there is no common super type for SanitizedContent and String), this
    // is probably because after escaping, the only thing you would ever do is convert to a string.
    return SoyExpression.forSoyValue(UnknownType.getInstance(), 
        MethodRef.RUNTIME_APPLY_PRINT_DIRECTIVE.invoke(
            renderContext
                .invoke(MethodRef.RENDER_CONTEXT_GET_PRINT_DIRECTIVE, constant(directive)),
                this.box(),
            argsList));
  }

  @Override SoyExpression asCheap() {
    return withSource(delegate.asCheap());
  }

  @Override SoyExpression asNonNullable() {
    return new SoyExpression(soyType.asNonNullable(), delegate.asNonNullable(), renderContext);
  }

  @Override
  public SoyExpression asNullable() {
    return new SoyExpression(soyType.asNullable(), delegate.asNullable(), renderContext);
  }

  @Override SoyExpression labelStart(Label label) {
    return withSource(delegate.labelStart(label));
  }

  @Override SoyExpression labelEnd(Label label) {
    return withSource(delegate.labelEnd(label));
  }

  /**
   * Default subtype of {@link SoyExpression} used by our core expression implementations.
   */
  private static final class DefaultBoxed extends SoyExpression {
    private final SoyExpression unboxed;

    DefaultBoxed(
        SoyRuntimeType soyType,
        SoyExpression unboxed,
        Expression delegate,
        Optional<Expression> expr) {
      super(soyType, delegate, expr);
      this.unboxed = unboxed;
    }

    @Override final SoyExpression unboxAs(Class<?> asType) {
      return unboxed.unboxAs(asType);
    }

    @Override SoyExpression coerceToBoolean() {
      return unboxed.coerceToBoolean();
    }

    @Override SoyExpression coerceToString() {
      return unboxed.coerceToString();
    }

    @Override SoyExpression coerceToDouble() {
      return unboxed.coerceToDouble();
    }

    @Override final SoyExpression box() {
      return this;
    }
  }
}
