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

import com.google.template.soy.base.SourceLocation;

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

import java.io.IOException;
import java.util.Arrays;

/**
 * A statement models a section of code.  Unlike {@linkplain Expression expressions}, statements
 * may have side effects.  For example, the Soy code {@code {'foo'}} could be implemented as a 
 * statement that writes to an output stream.
 *
 * <p>The generated code should satisfy the invariant that the runtime stack and local variables
 * are are unchanged by the generated code (i.e. the frame map at the start of the statement is
 * identical to the frame map at the end of the statement).
 */
abstract class Statement extends BytecodeProducer {
  private static final Type[] IO_EXCEPTION_ARRAY = new Type[] {Type.getType(IOException.class)};

  static final Statement NULL_STATEMENT =
      new Statement() {
        @Override
        void doGen(CodeBuilder adapter) {}
      };

  static final Statement RETURN =
      new Statement() {
        @Override
        void doGen(CodeBuilder adapter) {
          adapter.returnValue();
        }
      };

  /**
   * Generates a statement that returns the value produced by the given expression.
   * 
   * <p>This does not validate that the return type is appropriate.  It is our callers 
   * responsibility to do that.
   */
  static Statement returnExpression(final Expression expression) {
    // TODO(lukes): it would be nice to do a checkType operation here to make sure that expression
    // is compatible with the return type of the method, but i don't know how to get that
    // information here (reasonably).  So it is the caller's responsibility.
    return new Statement() {
      @Override void doGen(CodeBuilder adapter) {
        expression.gen(adapter);
        adapter.returnValue();
      }
    };
  }

 /**
  * Generates a statement that throws the throwable produced by the given expression.
  * 
  * <p>This does not validate that the throwable is compatible with the methods throws clause. 
  */
  static Statement throwExpression(final Expression expression) {
    expression.checkAssignableTo(THROWABLE_TYPE);
    return new Statement() {
      @Override
      void doGen(CodeBuilder adapter) {
        expression.gen(adapter);
        adapter.throwException();
      }
    };
  }

  /** Returns a statement that concatenates all the provided statements. */
  static Statement concat(Statement ...statements) {
    return concat(Arrays.asList(statements));
  }

  /** Returns a statement that concatenates all the provided statements. */
  static Statement concat(final Iterable<? extends Statement> statements) {
    checkNotNull(statements);
    return new Statement() {
      @Override void doGen(CodeBuilder adapter) {
        for (Statement statement : statements) {
          statement.gen(adapter);
        }
      }
    };
  }

  Statement() {
    super();
  }

  Statement(SourceLocation location) {
    super(location);
  }

  /**
   * Writes this statement to the {@link ClassVisitor} as a method.
   * 
   * @param access The access modifiers of the method
   * @param method The method signature
   * @param visitor The class visitor to write it to
   */
  final void writeMethod(int access, Method method, ClassVisitor visitor) {
    writeMethodTo(new CodeBuilder(access, method, null, visitor));
  }

  /**
   * Writes this statement to the {@link ClassVisitor} as a method.
   *
   * @param access The access modifiers of the method
   * @param method The method signature
   * @param visitor The class visitor to write it to
   */
  final void writeIOExceptionMethod(int access, Method method, ClassVisitor visitor) {
    writeMethodTo(new CodeBuilder(access, method, IO_EXCEPTION_ARRAY, visitor));
  }

  /** Writes this statement as the complete method body to {@code ga}. */
  private final void writeMethodTo(CodeBuilder builder) {
    builder.visitCode();
    gen(builder);
    try {
      builder.endMethod();
    } catch (Throwable t) {
      // ASM fails in bizarre ways, attach a trace of the thing we tried to generate to the 
      // exception.
      throw new RuntimeException("Failed to generate method:\n" + this, t);
    }
  }
  /**
   * Returns a new statement identical to this one but with the given label applied at the start
   * of the statement.
   */
  final Statement labelStart(final Label label) {
    return new Statement() {
      @Override void doGen(CodeBuilder adapter) {
        adapter.mark(label);
        Statement.this.gen(adapter);
      }
    };
  }

  /**
   * Returns a new {@link Statement} with the source location attached.
   */
  final Statement withSourceLocation(SourceLocation location) {
    checkNotNull(location);
    return new Statement(location) {
      @Override void doGen(CodeBuilder adapter) {
        Statement.this.gen(adapter);
      }
    };
  }

  @Override public String toString() {
    return "Statement:\n" + trace();
  }
}
