/*
 * 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.common.base.Preconditions.checkNotNull;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;

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

import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;

/**
 * An expression has a {@link #resultType()} and can {@link #gen generate} code to evaluate the
 * expression.
 *
 * <p>Expressions should be side effect free and also should not <em>consume</em> stack items.
 */
abstract class Expression extends BytecodeProducer {
  /** 
   * Expression features track additional metadata for expressions.
   * 
   * <p>Features should be defined such that not setting a feature on an expression is a safe 
   * default.  That way if they get accidentally dropped in a transformation we simply generate
   * less efficient code, not incorrect code.
   */
  enum Feature {
    /** The expression is guaranteed to not return null. */
    NON_NULLABLE,
    /** 
     * The expression is 'cheap'.  As a rule of thumb, if it involves allocation, it is not cheap.
     * 
     * <p>Cheapness is useful when deciding if it would be reasonable to evaluate an expression more
     * than once if the alternative is generating additional fields and save/restore code.
     */
    CHEAP;
    // TODO(lukes): an idempotent feature would be useful some expressions are not safe to gen more
    // than once.
  }

  /** An immutable wrapper of an EnumSet of {@link Feature}. */
  static final class Features {
    private static final Features EMPTY = new Features(EnumSet.noneOf(Feature.class));

    static Features of() {
      return EMPTY;
    }

    static Features of(Feature first, Feature ...rest) {
      EnumSet<Feature> set = EnumSet.of(first);
      Collections.addAll(set, rest);
      return new Features(set);
    }

    private static Features forType(Type expressionType, Features features) {
      switch (expressionType.getSort()) {
        case Type.OBJECT:
        case Type.ARRAY:
          return features;
        case Type.BOOLEAN:
        case Type.BYTE:
        case Type.CHAR:
        case Type.DOUBLE:
        case Type.INT:
        case Type.SHORT:
        case Type.LONG:
        case Type.FLOAT:
          // primitives are never null
          return features.plus(Feature.NON_NULLABLE);
        case Type.VOID:
        case Type.METHOD:
          throw new IllegalArgumentException("Invalid type: " + expressionType);
        default:
          throw new AssertionError("unexpected type " + expressionType);
      }
    }

    private final EnumSet<Feature> set;

    private Features(EnumSet<Feature> set) {
      this.set = checkNotNull(set);
    }

    boolean has(Feature feature) {
      return set.contains(feature);
    }

    Features plus(Feature feature) {
      if (set.contains(feature)) {
        return this;
      }
      EnumSet<Feature> newSet = copyFeatures();
      newSet.add(feature);
      return new Features(newSet);
    }

    Features minus(Feature feature) {
      if (!set.contains(feature)) {
        return this;
      }
      EnumSet<Feature> newSet = copyFeatures();
      newSet.remove(feature);
      return new Features(newSet);
    }

    private EnumSet<Feature> copyFeatures() {
      // Can't use EnumSet.copyOf() because it throws on empty collections!
      EnumSet<Feature> newSet = EnumSet.noneOf(Feature.class);
      newSet.addAll(set);
      return newSet;
    }
  }

  /** Returns true if all referenced expressions are {@linkplain #isCheap() cheap}. */
  static boolean areAllCheap(Iterable<? extends Expression> args) {
    for (Expression arg : args) {
      if (!arg.isCheap()) {
        return false;
      }
    }
    return true;
  }

  /** Returns true if all referenced expressions are {@linkplain #isCheap() cheap}. */
  static boolean areAllCheap(Expression first, Expression ...rest) {
    return areAllCheap(ImmutableList.<Expression>builder().add(first).add(rest).build());
  }

  /**
   * Checks that the given expressions are compatible with the given types.
   */
  static void checkTypes(ImmutableList<Type> types, Expression ...exprs) {
    if (Flags.DEBUG) {
      checkTypes(types, Arrays.asList(exprs));
    }
  }

  /**
   * Checks that the given expressions are compatible with the given types.
   */
  static void checkTypes(ImmutableList<Type> types, Iterable<? extends Expression> exprs) {
    if (Flags.DEBUG) {
      int size = Iterables.size(exprs);
      checkArgument(
          size == types.size(),
          "Supplied the wrong number of parameters. Expected %s, got %s",
          types.size(),
          size);
      int i = 0;
      for (Expression expr : exprs) {
        expr.checkAssignableTo(types.get(i), "Parameter %s", i);
        i++;
      }
    }
  }

  private final Features features;
  private final Type resultType;

  Expression(Type resultType) {
    this(resultType, Features.of());
  }

  Expression(Type resultType, Feature first, Feature ...rest) {
    this(resultType, Features.of(first, rest));
  }

  Expression(Type resultType, Features features) {
    this.resultType = checkNotNull(resultType);
    this.features = Features.forType(resultType, features);
  }

  /** 
   * Generate code to evaluate the expression.
   *   
   * <p>The generated code satisfies the invariant that the top of the runtime stack will contain a
   * value with this {@link #resultType()} immediately after evaluation of the code. 
   */
  @Override abstract void doGen(CodeBuilder adapter);
  
  /** The type of the expression. */
  final Type resultType() {
    return resultType;
  }

  /** Whether or not this expression is {@link Feature#CHEAP cheap}. */
  boolean isCheap() {
    return features.has(Feature.CHEAP);
  }

  /** Whether or not this expression is {@link Feature#NON_NULLABLE non nullable}. */
  boolean isNonNullable() {
    return features.has(Feature.NON_NULLABLE);
  }

  /**
   * Returns all the feature bits. 
   * Typically, users will want to invoke one of the convenience accessors {@link #isCheap()} or 
   * {@link #isNonNullable()}. 
   */
  Features features() {
    return features;
  }

  /**
   * Check that this expression is assignable to {@code expected}. 
   */
  final void checkAssignableTo(Type expected) {
    if (Flags.DEBUG) {
      checkAssignableTo(expected, "");
    }
  }

  /**
   * Check that this expression is assignable to {@code expected}. 
   */
  final void checkAssignableTo(Type expected, String fmt, Object ...args) {
    if (Flags.DEBUG) {
      if (BytecodeUtils.isPossiblyAssignableFrom(resultType(), expected)) {
        return;
      }
      String message = String.format("Type mismatch. Expected %s, got %s.", expected, resultType());
      if (!fmt.isEmpty()) {
        message = String.format(fmt, args) + ". " + message;
      }
      throw new IllegalArgumentException(message);
    }
  }

  /** 
   * Convert this expression to a statement, by executing it and throwing away the result.
   * 
   * <p>This is useful for invoking non-void methods when we don't care about the result.
   */
  Statement toStatement() {
    return new Statement() {
      @Override void doGen(CodeBuilder adapter) {
        Expression.this.gen(adapter);
        switch (resultType().getSize()) {
          case 0:
            throw new AssertionError("void expressions are not allowed");
          case 1:
            adapter.pop();
            break;
          case 2:
            adapter.pop2();
            break;
        }
      }
    };
  }
  
  /** Returns an equivalent expression where {@link #isCheap()} returns {@code true}. */
  Expression asCheap() {
    if (isCheap()) {
      return this;
    }
    return new Expression(resultType, features.plus(Feature.CHEAP)) {
      @Override void doGen(CodeBuilder adapter) {
        Expression.this.gen(adapter);
      }
    };
  }

  /** Returns an equivalent expression where {@link #isNonNullable()} returns {@code true}. */
  Expression asNonNullable() {
    if (isNonNullable()) {
      return this;
    }
    return new Expression(resultType, features.plus(Feature.NON_NULLABLE)) {
      @Override void doGen(CodeBuilder adapter) {
        Expression.this.gen(adapter);
      }
    };
  }

  Expression asNullable() {
    if (!isNonNullable()) {
      return this;
    }
    return new Expression(resultType, features.minus(Feature.NON_NULLABLE)) {
      @Override
      void doGen(CodeBuilder adapter) {
        Expression.this.gen(adapter);
      }
    };
  }

  /**
   * Returns an expression that performs a checked cast from the current type to the target type.
   *
   * @throws IllegalArgumentException if either type is not a reference type.
   */
  Expression cast(final Type target) {
    checkArgument(target.getSort() == Type.OBJECT, "cast targets must be reference types.");
    checkArgument(resultType().getSort() == Type.OBJECT, "you may only cast from reference types.");
    if (BytecodeUtils.isDefinitelyAssignableFrom(target, resultType())) {
      return this;
    }
    return new Expression(target, features()) {
      @Override void doGen(CodeBuilder adapter) {
        Expression.this.gen(adapter);
        adapter.checkCast(resultType());
      }
    };
  }

  /**
   * Returns an expression that performs a checked cast from the current type to the target type.
   *
   * @throws IllegalArgumentException if either type is not a reference type.
   */
  Expression cast(Class<?> target) {
    return cast(Type.getType(target));
  }

  /**
   * A simple helper that calls through to {@link MethodRef#invoke(Expression...)}, but allows a
   * more natural fluent call style.
   */
  Expression invoke(MethodRef method, Expression ...args) {
    return method.invoke(ImmutableList.<Expression>builder().add(this).add(args).build());
  }

  /**
   * A simple helper that calls through to {@link MethodRef#invokeVoid(Expression...)}, but allows a
   * more natural fluent call style.
   */
  Statement invokeVoid(MethodRef method, Expression ...args) {
    return method.invokeVoid(ImmutableList.<Expression>builder().add(this).add(args).build());
  }

  /**
   * Returns a new expression identical to this one but with the given label applied at the start
   * of the expression.
   */
  Expression labelStart(final Label label) {
    return new Expression(resultType(), features) {
      @Override void doGen(CodeBuilder adapter) {
        adapter.mark(label);
        Expression.this.gen(adapter);
      }
    };
  }

  /**
   * Returns a new expression identical to this one but with the given label applied at the end
   * of the expression.
   */
  Expression labelEnd(final Label label) {
    return new Expression(resultType(), features) {
      @Override
      void doGen(CodeBuilder adapter) {
        Expression.this.gen(adapter);
        adapter.mark(label);
      }
    };
  }

  @Override public String toString() {
    String name = getClass().getSimpleName();
    if (name.isEmpty()) {
      // provide a default for anonymous subclasses
      name = "Expression";
    }
    name = name + "(" + resultType + "){";
    boolean needsLeadingSpace = false;
    if (features.has(Feature.CHEAP)) {
      name += "cheap";
      needsLeadingSpace = true;
    }
    if (features.has(Feature.NON_NULLABLE) && !BytecodeUtils.isPrimitive(resultType)) {
      name += (needsLeadingSpace ? " " : "") + "non-null";
    }
    return name + "}<" + resultType() + ">:\n" + trace();
  }
}
