/*
 * 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.template.soy.jbcsrc.BytecodeUtils.SOY_RECORD_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.defineDefaultConstructor;
import static com.google.template.soy.jbcsrc.LocalVariable.createLocal;
import static com.google.template.soy.jbcsrc.LocalVariable.createThisVar;
import static com.google.template.soy.jbcsrc.StandardNames.FACTORY_CLASS;

import com.google.template.soy.data.SoyRecord;
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.commons.Method;

/**
 * Generates {@link com.google.template.soy.jbcsrc.shared.CompiledTemplate.Factory} implementations.
 *
 * <p>Each factory is incredibly simple, essentially we are generating this class: <pre>{@code
 *   public final class FooFactory implements CompiledTemplate.Factory {
 *     public CompiledTemplate create(SoyRecord params) {
 *       return new Foo(params);
 *     }
 *   }}</pre>
 *
 * <p>Where the only thing that differs is the name of the template being constructed.
 */
final class TemplateFactoryCompiler {
  private static final TypeInfo FACTORY_TYPE = TypeInfo.create(CompiledTemplate.Factory.class);

  private static final int FACTORY_ACCESS = Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL;

  private static final Method CREATE_METHOD;
  static {
    try {
      CREATE_METHOD = Method.getMethod(
          CompiledTemplate.Factory.class.getDeclaredMethod(
              "create", SoyRecord.class, SoyRecord.class));
    } catch (NoSuchMethodException | SecurityException e) {
      throw new AssertionError(e);
    }
  }

  private final CompiledTemplateMetadata template;
  private final InnerClasses innerClasses;

  TemplateFactoryCompiler(CompiledTemplateMetadata currentClass, InnerClasses innerClasses) {
    this.template = currentClass;
    this.innerClasses = innerClasses;
  }

  /** Compiles the factory. */
  void compile() {
    TypeInfo factoryType = innerClasses.registerInnerClass(FACTORY_CLASS, FACTORY_ACCESS);
    SoyClassWriter cw =
        SoyClassWriter.builder(factoryType)
            .implementing(FACTORY_TYPE)
            .setAccess(FACTORY_ACCESS)
            .sourceFileName(template.node().getSourceLocation().getFileName())
            .build();
    innerClasses.registerAsInnerClass(cw, factoryType);

    generateStaticInitializer(cw);
    defineDefaultConstructor(cw, factoryType);
    generateCreateMethod(cw, factoryType);
    cw.visitEnd();
    innerClasses.add(cw.toClassData());
  }

  /**
   * Generates a static initializer that references the CompiledTemplate class to force eager
   * classloading (and thus verification errors). For example, <pre>{@code
   *   static {
   *     Class<?> clz = GeneratedTemplateClass.class;
   *   }}</pre>
   */
  private void generateStaticInitializer(ClassVisitor cv) {
    if (Flags.DEBUG) {
      new Statement() {
        @Override
        void doGen(CodeBuilder adapter) {
          adapter.pushType(template.typeInfo().type());
          adapter.visitVarInsn(Opcodes.ASTORE, 0);
          adapter.returnValue();
        }
      }.writeMethod(Opcodes.ACC_STATIC, BytecodeUtils.CLASS_INIT, cv);
    }
  }

  /**
   * Writes the {@link CompiledTemplate.Factory#create} method, which directly delegates to the
   * constructor of the {@link #template}.
   */
  private void generateCreateMethod(ClassVisitor cv, TypeInfo factoryType) {
    final Label start = new Label();
    final Label end = new Label();
    final LocalVariable thisVar = createThisVar(factoryType, start, end);
    final LocalVariable paramsVar = createLocal("params", 1, SOY_RECORD_TYPE, start, end);
    final LocalVariable ijVar = createLocal("ij", 2, SOY_RECORD_TYPE, start, end);
    final Statement returnTemplate =
        Statement.returnExpression(template.constructor().construct(paramsVar, ijVar));
    new Statement() {
      @Override void doGen(CodeBuilder ga) {
        ga.mark(start);
        returnTemplate.gen(ga);
        ga.mark(end);
        thisVar.tableEntry(ga);
        paramsVar.tableEntry(ga);
        ijVar.tableEntry(ga);
      }
    }.writeMethod(Opcodes.ACC_PUBLIC, CREATE_METHOD, cv);
  }
}
