/*
 * Copyright (C) 2015 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.jboss.errai.ioc.rebind.ioc.bootstrapper;

import static org.jboss.errai.codegen.Parameter.finalOf;
import static org.jboss.errai.codegen.meta.MetaClassFactory.parameterizedAs;
import static org.jboss.errai.codegen.meta.MetaClassFactory.typeParametersOf;
import static org.jboss.errai.codegen.util.PrivateAccessUtil.addPrivateAccessStubs;
import static org.jboss.errai.codegen.util.PrivateAccessUtil.getPrivateMethodName;
import static org.jboss.errai.codegen.util.Stmt.declareFinalVariable;
import static org.jboss.errai.codegen.util.Stmt.if_;
import static org.jboss.errai.codegen.util.Stmt.invokeStatic;
import static org.jboss.errai.codegen.util.Stmt.load;
import static org.jboss.errai.codegen.util.Stmt.loadLiteral;
import static org.jboss.errai.codegen.util.Stmt.loadStatic;
import static org.jboss.errai.codegen.util.Stmt.loadVariable;
import static org.jboss.errai.codegen.util.Stmt.nestedCall;
import static org.jboss.errai.codegen.util.Stmt.newArray;
import static org.jboss.errai.codegen.util.Stmt.newObject;
import static org.jboss.errai.codegen.util.Stmt.throw_;
import static org.jboss.errai.codegen.util.Stmt.try_;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Map.Entry;

import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Default;
import javax.enterprise.inject.Typed;
import javax.inject.Named;
import javax.inject.Qualifier;

import org.jboss.errai.codegen.BooleanOperator;
import org.jboss.errai.codegen.InnerClass;
import org.jboss.errai.codegen.Modifier;
import org.jboss.errai.codegen.Parameter;
import org.jboss.errai.codegen.Statement;
import org.jboss.errai.codegen.builder.BlockBuilder;
import org.jboss.errai.codegen.builder.ClassStructureBuilder;
import org.jboss.errai.codegen.builder.ConstructorBlockBuilder;
import org.jboss.errai.codegen.builder.ContextualStatementBuilder;
import org.jboss.errai.codegen.builder.ElseBlockBuilder;
import org.jboss.errai.codegen.builder.MethodCommentBuilder;
import org.jboss.errai.codegen.builder.impl.AbstractStatementBuilder;
import org.jboss.errai.codegen.builder.impl.BooleanExpressionBuilder;
import org.jboss.errai.codegen.builder.impl.ClassBuilder;
import org.jboss.errai.codegen.builder.impl.StatementBuilder;
import org.jboss.errai.codegen.literal.LiteralFactory;
import org.jboss.errai.codegen.meta.HasAnnotations;
import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.meta.MetaClassFactory;
import org.jboss.errai.codegen.meta.MetaConstructor;
import org.jboss.errai.codegen.meta.MetaField;
import org.jboss.errai.codegen.meta.MetaMethod;
import org.jboss.errai.codegen.meta.MetaParameter;
import org.jboss.errai.codegen.meta.MetaParameterizedType;
import org.jboss.errai.codegen.meta.MetaType;
import org.jboss.errai.codegen.meta.impl.build.BuildMetaClass;
import org.jboss.errai.codegen.util.Stmt;
import org.jboss.errai.ioc.client.QualifierUtil;
import org.jboss.errai.ioc.client.api.ActivatedBy;
import org.jboss.errai.ioc.client.api.EntryPoint;
import org.jboss.errai.ioc.client.container.BeanActivator;
import org.jboss.errai.ioc.client.container.Context;
import org.jboss.errai.ioc.client.container.ContextManager;
import org.jboss.errai.ioc.client.container.Factory;
import org.jboss.errai.ioc.client.container.FactoryHandleImpl;
import org.jboss.errai.ioc.client.container.Proxy;
import org.jboss.errai.ioc.client.container.ProxyHelper;
import org.jboss.errai.ioc.client.container.ProxyHelperImpl;
import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraph;
import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraphBuilder.Dependency;
import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraphBuilder.DependencyType;
import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraphBuilder.ParamDependency;
import org.jboss.errai.ioc.rebind.ioc.graph.api.Injectable;
import org.jboss.errai.ioc.rebind.ioc.injector.api.FactoryController;
import org.jboss.errai.ioc.rebind.ioc.injector.api.InjectionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;

/**
 * Implements functionality common to most {@link FactoryBodyGenerator} such as
 * generating proxies and managing
 * {@link Factory#getReferenceAs(Object, String, Class) references}.
 *
 * @see FactoryBodyGenerator
 * @author Max Barkley <mbarkley@redhat.com>
 */
public abstract class AbstractBodyGenerator implements FactoryBodyGenerator {

  private static final Logger logger = LoggerFactory.getLogger(AbstractBodyGenerator.class);

  protected static Multimap<DependencyType, Dependency> separateByType(final Collection<Dependency> dependencies) {
    final Multimap<DependencyType, Dependency> separated = HashMultimap.create();

    for (final Dependency dep : dependencies) {
      separated.put(dep.getDependencyType(), dep);
    }

    return separated;
  }

  /**
   * This is populated at the start of
   * {@link #generate(ClassStructureBuilder, Injectable, DependencyGraph, InjectionContext, TreeLogger, GeneratorContext)}
   * .
   *
   * Calls to {@link FactoryController#addInvokeAfter(MetaMethod, Statement)},
   * {@link FactoryController#addInvokeBefore(MetaMethod, Statement)}, or
   * {@link FactoryController#addProxyProperty(String, Class, Statement)} will
   * require the type produced by the generated factory to be proxiable.
   */
  protected FactoryController controller;

  protected void maybeImplementCreateProxy(final ClassStructureBuilder<?> bodyBlockBuilder, final Injectable injectable) {
    final MetaClass proxyImpl = maybeCreateProxyImplementation(injectable, bodyBlockBuilder);

    if (proxyImpl != null) {
      final BlockBuilder<?> createProxyBody = bodyBlockBuilder
              .publicMethod(parameterizedAs(Proxy.class, typeParametersOf(injectable.getInjectedType())), "createProxy",
                      finalOf(Context.class, "context"))
              .body();
      final Object proxyInstanceStmt;
      if (injectable.getInjectedType().isInterface() || getAccessibleNoArgConstructor(injectable.getInjectedType()) != null) {
        proxyInstanceStmt = newObject(proxyImpl);
      } else {
        bodyBlockBuilder
                .privateMethod(parameterizedAs(Proxy.class, typeParametersOf(injectable.getInjectedType())),
                        "createProxyWithErrorMessage")
                .body()
                .append(try_().append(load(newObject(proxyImpl)).returnValue()).finish()
                        .catch_(Throwable.class, "t").append(throw_(RuntimeException.class,
                                loadLiteral(injectableConstructorErrorMessage(injectable)), loadVariable("t")))
                        .finish())
                .finish();
        proxyInstanceStmt = loadVariable("this").invoke("createProxyWithErrorMessage");
      }

      createProxyBody
              .append(declareFinalVariable("proxyImpl",
                      parameterizedAs(Proxy.class, typeParametersOf(injectable.getInjectedType())),
                      proxyInstanceStmt))
              .append(loadVariable("proxyImpl").invoke("setProxyContext", loadVariable("context")))
              .append(loadVariable("proxyImpl").returnValue()).finish();
    }
  }

  private String injectableConstructorErrorMessage(final Injectable injectable) {
    final MetaConstructor constr = getAccessibleConstructor(injectable);

    return "While creating a proxy for " + injectable.getInjectedType().getFullyQualifiedName()
            + " an exception was thrown from this constructor: " + constr
            + "\nTo fix this problem, add a no-argument public or protected constructor for use in proxying.";
  }

  /**
   * @return Returns {@code null} if unproxiable and a proxy is not required.
   */
  private MetaClass maybeCreateProxyImplementation(final Injectable injectable, final ClassStructureBuilder<?> bodyBlockBuilder) {

    final ClassStructureBuilder<?> proxyImpl;
    final MetaClass injectedType = injectable.getInjectedType();
    final boolean requiresProxy = requiresProxy(injectable);
    if (requiresProxy && injectedType.isInterface()) {
      proxyImpl = ClassBuilder
              .define(injectable.getFactoryName() + "ProxyImpl")
              .privateScope()
              .implementsInterface(parameterizedAs(Proxy.class, typeParametersOf(injectedType)))
              .implementsInterface(injectedType).body();
      declareAndInitializeProxyHelper(injectable, proxyImpl);
    } else if (requiresProxy && isProxiableClass(injectable)) {
      proxyImpl = ClassBuilder
              .define(injectable.getFactoryName() + "ProxyImpl", injectedType)
              .privateScope()
              .implementsInterface(parameterizedAs(Proxy.class, typeParametersOf(injectedType))).body();
      declareAndInitializeProxyHelper(injectable, proxyImpl);
    } else if (!requiresProxy) {
      return null;
    } else {
      throw new RuntimeException(injectedType + " must be proxiable but is not.");
    }

    maybeImplementConstructor(proxyImpl, injectable);
    implementProxyMethods(proxyImpl, injectable);
    implementAccessibleMethods(proxyImpl, injectable, bodyBlockBuilder.getClassDefinition());

    bodyBlockBuilder.declaresInnerClass(new InnerClass(proxyImpl.getClassDefinition()));

    return proxyImpl.getClassDefinition();
  }

  private boolean requiresProxy(final Injectable injectable) {
    return injectable.requiresProxy() || controller.requiresProxy();
  }

  private boolean isProxiableClass(final Injectable injectable) {
    final MetaClass type = injectable.getInjectedType();

    return !type.isFinal() && hasAccessibleConstructor(injectable);
  }

  private boolean hasAccessibleConstructor(final Injectable injectable) {
    return getAccessibleConstructor(injectable) != null;
  }

  private MetaConstructor getAccessibleConstructor(final Injectable injectable) {
    final MetaClass type = injectable.getInjectedType();
    final MetaConstructor noArgConstr = getAccessibleNoArgConstructor(type);

    if (noArgConstr != null) {
      return noArgConstr;
    }

    for (final Dependency dep : injectable.getDependencies()) {
      if (dep.getDependencyType().equals(DependencyType.Constructor)) {
        final MetaConstructor injectableConstr = (MetaConstructor) ((ParamDependency) dep).getParameter().getDeclaringMember();

        return (injectableConstr.isPublic() || injectableConstr.isProtected()) ? injectableConstr : null;
      }
    }

    return null;
  }

  private MetaConstructor getAccessibleNoArgConstructor(final MetaClass type) {
    final MetaConstructor noArgConstr = type.getDeclaredConstructor(new MetaClass[0]);

    if (noArgConstr != null && (noArgConstr.isPublic() || noArgConstr.isProtected())) {
      return noArgConstr;
    } else {
      return null;
    }
  }

  private void declareAndInitializeProxyHelper(final Injectable injectable, final ClassStructureBuilder<?> bodyBlockBuilder) {
    bodyBlockBuilder
            .privateField("proxyHelper",
                    parameterizedAs(ProxyHelper.class, typeParametersOf(injectable.getInjectedType())))
            .modifiers(Modifier.Final)
            .initializesWith(initializeProxyHelper(injectable))
            .finish();
  }

  private Statement initializeProxyHelper(final Injectable injectable) {
    return newObject(
            parameterizedAs(ProxyHelperImpl.class, typeParametersOf(injectable.getInjectedType())),
            injectable.getFactoryName());
  }

  private void maybeImplementConstructor(final ClassStructureBuilder<?> proxyImpl, final Injectable injectable) {
    if (injectable.getInjectedType().isInterface()) {
      return;
    }

    final MetaConstructor accessibleConstructor = getAccessibleConstructor(injectable);
    if (accessibleConstructor.getParameters().length > 0) {
      implementConstructor(proxyImpl, accessibleConstructor);
    }
  }

  private void implementConstructor(final ClassStructureBuilder<?> proxyImpl, final MetaConstructor accessibleConstructor) {
    final Object[] args = new Object[accessibleConstructor.getParameters().length];
    for (int i = 0; i < args.length; i++) {
      args[i] = loadLiteral(null);
    }

    proxyImpl.publicConstructor().callSuper(args).finish();
  }

  private void implementAccessibleMethods(final ClassStructureBuilder<?> proxyImpl, final Injectable injectable, final BuildMetaClass factoryClass) {
    final Multimap<String, MetaMethod> proxiedMethodsByName = HashMultimap.create();
    final MetaClass injectedType = injectable.getInjectedType();
    for (final MetaMethod method : injectedType.getMethods()) {
      if (shouldProxyMethod(method, proxiedMethodsByName)) {
        proxiedMethodsByName.put(method.getName(), method);
        final BlockBuilder<?> body = createProxyMethodDeclaration(proxyImpl, method);
        final StatementBuilder proxiedInstanceDeclaration = declareFinalVariable("proxiedInstance",
                injectable.getInjectedType(), loadVariable("proxyHelper").invoke("getInstance", loadVariable("this")));
        final ContextualStatementBuilder proxyHelperInvocation = proxyHelperInvocation(method, factoryClass);
        final Statement nonInitializedCase;
        final boolean nonInitializedReturns;
        if (injectedType.isInterface() || method.isAbstract()) {
          nonInitializedCase = throw_(RuntimeException.class, "Cannot invoke public method on proxied interface before constructor completes.");
          nonInitializedReturns = false;
        } else if (!(method.isPublic() || method.isPrivate() || method.isProtected())) {
          nonInitializedCase = throw_(RuntimeException.class, "Cannot invoke proxied package private method before constructor completes.");
          nonInitializedReturns = false;
        } else {
          nonInitializedCase = loadVariable("super").invoke(method.getName(), getParametersForInvocation(method));
          nonInitializedReturns = true;
        }
        final BlockBuilder<ElseBlockBuilder> ifBlock = if_(
                BooleanExpressionBuilder.create(loadVariable("proxyHelper"), BooleanOperator.NotEquals, null))
                        .append(proxiedInstanceDeclaration)
                        .appendAll(controller.getInvokeBeforeStatements(method));
        if (method.getReturnType().isVoid()) {
          ifBlock.append(proxyHelperInvocation);
          ifBlock.appendAll(controller.getInvokeAfterStatements(method));
          body.append(ifBlock.finish().else_().append(nonInitializedCase).finish());
        } else {
          ifBlock.append(declareFinalVariable("retVal", method.getReturnType().getErased(), proxyHelperInvocation));
          ifBlock.appendAll(controller.getInvokeAfterStatements(method));
          ifBlock.append(loadVariable("retVal").returnValue());
          if (nonInitializedReturns) {
            body.append(ifBlock.finish().else_().append(nestedCall(nonInitializedCase).returnValue()).finish());
          } else {
            body.append(ifBlock.finish().else_().append(nonInitializedCase).finish());
          }
        }

        body.finish();
      }
      else if (!(method.isPublic() || method.isPrivate() || method.isProtected()) && injectable.requiresProxy()) {
        logger.warn("The normal scoped type, " + injectable.getInjectedType().getFullyQualifiedName()
                + ", has a package-private method, " + method.getName()
                + ", that cannot be proxied. Invoking this method on an injected instance may cause errors.");
      }
    }
  }

  private BlockBuilder<?> createProxyMethodDeclaration(final ClassStructureBuilder<?> proxyImpl, final MetaMethod method) {
    final MethodCommentBuilder<?> methodBuilder;
    if (method.isPublic()) {
      methodBuilder = proxyImpl.publicMethod(method.getReturnType().getErased(), method.getName(),
              getParametersForDeclaration(method));
    } else if (method.isProtected()) {
      methodBuilder = proxyImpl.protectedMethod(method.getReturnType().getErased(), method.getName(),
              getParametersForDeclaration(method));
    } else {
      final String methodType = (method.isProtected()) ? "private" : "package private";
      throw new RuntimeException(
              "Cannot proxy " + methodType + " method from " + method.getDeclaringClassName());
    }

    if (method.isPublic() || method.isProtected()) {
      return methodBuilder.annotatedWith(new Override() {
        @Override
        public Class<? extends Annotation> annotationType() {
          return Override.class;
        }
      }).throws_(method.getCheckedExceptions()).body();
    } else {
      return methodBuilder.throws_(method.getCheckedExceptions()).body();
    }
  }

  private ContextualStatementBuilder proxyHelperInvocation(final MetaMethod method, final BuildMetaClass factoryClass) {
    if (method.isPublic()) {
      return loadVariable("proxiedInstance").invoke(method.getName(), getParametersForInvocation(method));
    } else {
      controller.addExposedMethod(method);
      return invokeStatic(factoryClass, getPrivateMethodName(method), getParametersForInvocation(method, loadVariable("proxiedInstance")));
    }
  }

  private boolean shouldProxyMethod(final MetaMethod method, final Multimap<String, MetaMethod> proxiedMethodsByName) {
    return (method.getDeclaringClass() != null && method.getDeclaringClass().isInterface())
            || !method.isStatic() && (method.isPublic() || method.isProtected()) && !method.isFinal()
                    && methodIsNotFromObjectUnlessHashCode(method)
            && typesInSignatureAreVisible(method)
            && isNotAlreadyProxied(method, proxiedMethodsByName);
  }

  private boolean isNotAlreadyProxied(final MetaMethod method, final Multimap<String, MetaMethod> proxiedMethodsByName) {
    methodLoop:
    for (final MetaMethod proxiedMethod : proxiedMethodsByName.get(method.getName())) {
      final MetaParameter[] proxiedParams = proxiedMethod.getParameters();
      final MetaParameter[] methodParams = method.getParameters();
      if (proxiedParams.length == methodParams.length) {
        for (int i = 0; i < methodParams.length; i++) {
          if (!proxiedParams[i].getType().isAssignableTo(methodParams[i].getType())) {
            continue methodLoop;
          }
        }

        return false;
      }
    }

    return true;
  }

  private boolean methodIsNotFromObjectUnlessHashCode(final MetaMethod method) {
    return (method.asMethod() == null || method.asMethod().getDeclaringClass() == null
            || !method.asMethod().getDeclaringClass().equals(Object.class)
            || method.getName().equals("hashCode"))
            && isNotEqualsMethod(method);
  }

  private boolean isNotEqualsMethod(final MetaMethod method) {
    return !(method.getName().equals("equals") && method.getParameters().length == 1);
  }

  private boolean typesInSignatureAreVisible(final MetaMethod method) {
    if (!isVisibleType(method.getReturnType())) {
      return false;
    }

    for (final MetaParameter param : method.getParameters()) {
      if (!isVisibleType(param.getType())) {
        return false;
      }
    }

    return true;
  }

  private boolean isVisibleType(final MetaClass type) {
    if (type.isArray()) {
      return isVisibleType(type.getComponentType());
    } else {
      return type.isPublic() || type.isProtected() || type.isPrimitive();
    }
  }

  private Object[] getParametersForInvocation(final MetaMethod method, final Object... prependedParams) {
    final int paramLength = method.getParameters().length + prependedParams.length;
    final Object[] params = new Object[paramLength];
    for (int i = 0; i < prependedParams.length; i++) {
      params[i] = prependedParams[i];
    }
    final MetaParameter[] declaredParams = method.getParameters();
    for (int i = 0; i < declaredParams.length; i++) {
      params[prependedParams.length+i] = loadVariable(declaredParams[i].getName());
    }

    return params;
  }

  private Parameter[] getParametersForDeclaration(final MetaMethod method) {
    final MetaParameter[] metaParams = method.getParameters();
    final Parameter[] params = new Parameter[metaParams.length];

    for (int i = 0; i < params.length; i++) {
      params[i] = Parameter.of(metaParams[i].getType().getErased(), metaParams[i].getName());
    }

    return params;
  }

  private void implementProxyMethods(final ClassStructureBuilder<?> proxyImpl, final Injectable injectable) {
    implementInitProxyProperties(proxyImpl, injectable);
    implementAsBeanType(proxyImpl, injectable);
    implementSetInstance(proxyImpl, injectable);
    implementClearInstance(proxyImpl, injectable);
    implementSetContext(proxyImpl, injectable);
    implementGetContext(proxyImpl, injectable);
    implementUnwrappedInstance(proxyImpl, injectable);
    implementEquals(proxyImpl);
  }

  private void implementEquals(final ClassStructureBuilder<?> proxyImpl) {
    proxyImpl.publicMethod(boolean.class, "equals", Parameter.of(Object.class, "obj")).body()
      .append(loadVariable("obj").assignValue(invokeStatic(Factory.class, "maybeUnwrapProxy", loadVariable("obj"))))
      .append(loadVariable("proxyHelper").invoke("getInstance", loadVariable("this")).invoke("equals", loadVariable("obj")).returnValue())
      .finish();
  }

  private void implementInitProxyProperties(final ClassStructureBuilder<?> proxyImpl, final Injectable injectable) {
    final BlockBuilder<?> initBody = proxyImpl
            .publicMethod(void.class, "initProxyProperties", finalOf(injectable.getInjectedType(), "instance")).body();

    for (final Entry<String, Statement> prop : controller.getProxyProperties()) {
      proxyImpl.privateField(prop.getKey(), prop.getValue().getType()).finish();
      initBody.append(loadVariable(prop.getKey()).assignValue(prop.getValue()));
    }

    initBody.finish();
  }

  private void implementUnwrappedInstance(final ClassStructureBuilder<?> proxyImpl, final Injectable injectable) {
    proxyImpl.publicMethod(Object.class, "unwrap")
             .body()
             .append(loadVariable("proxyHelper").invoke("getInstance", loadVariable("this")).returnValue())
             .finish();
  }

  private void implementSetContext(final ClassStructureBuilder<?> proxyImpl, final Injectable injectable) {
    proxyImpl.publicMethod(void.class, "setProxyContext", finalOf(Context.class, "context"))
             .body()
             .append(loadVariable("proxyHelper").invoke("setProxyContext", loadVariable("context")))
             .finish();
  }

  private void implementGetContext(final ClassStructureBuilder<?> proxyImpl, final Injectable injectable) {
    proxyImpl.publicMethod(Context.class, "getProxyContext")
             .body()
             .append(loadVariable("proxyHelper").invoke("getProxyContext").returnValue())
             .finish();
  }

  private void implementClearInstance(final ClassStructureBuilder<?> proxyImpl, final Injectable injectable) {
    proxyImpl.publicMethod(void.class, "clearInstance")
             .body()
             .append(loadVariable("proxyHelper").invoke("clearInstance"))
             .finish();
  }

  private void implementSetInstance(final ClassStructureBuilder<?> proxyImpl, final Injectable injectable) {
    proxyImpl.publicMethod(void.class, "setInstance", finalOf(injectable.getInjectedType(), "instance"))
             .body()
             .append(loadVariable("proxyHelper").invoke("setInstance", loadVariable("instance")))
             .finish();
  }

  private void implementAsBeanType(final ClassStructureBuilder<?> proxyImpl, final Injectable injectable) {
    proxyImpl.publicMethod(injectable.getInjectedType(), "asBeanType")
             .body()
             .append(loadVariable("this").returnValue())
             .finish();
  }

  protected void implementCreateInstance(final ClassStructureBuilder<?> bodyBlockBuilder, final Injectable injectable, final List<Statement> createInstanceStatements) {
    String createInstanceMethodName;
    Parameter[] params;
    if (injectable.isContextual()) {
      createInstanceMethodName = "createContextualInstance";
      params = new Parameter[] {
          finalOf(ContextManager.class, "contextManager"),
          finalOf(Class[].class, "typeArgs"),
          finalOf(Annotation[].class, "qualifiers") };
    }
    else {
      createInstanceMethodName = "createInstance";
      params = new Parameter[] { finalOf(ContextManager.class, "contextManager") };
    }
    bodyBlockBuilder.publicMethod(injectable.getInjectedType(), createInstanceMethodName, params)
                    .appendAll(createInstanceStatements)
                    .finish();
  }

  protected void addReturnStatement(final List<Statement> createInstanceStatements) {
    createInstanceStatements.add(loadVariable("this").invoke("setIncompleteInstance", loadLiteral(null)));
    createInstanceStatements.add(loadVariable("instance").returnValue());
  }

  /**
   * @param bodyBlockBuilder
   *          The {@link ClassStructureBuilder} for the {@link Factory} being
   *          generated.
   * @param injectable
   *          Contains metadata (including dependencies) or the bean that the
   *          generated factory will produce.
   * @param graph
   *          The dependency graph that the {@link Injectable} parameter is
   *          from.
   * @param injectionContext
   *          The single injection context shared between all
   *          {@link FactoryBodyGenerator FactoryBodyGenerators}.
   * @return A list of statements that will generated in the
   *         {@link Factory#createInstance(ContextManager)} method.
   */
  protected abstract List<Statement> generateCreateInstanceStatements(ClassStructureBuilder<?> bodyBlockBuilder,
          Injectable injectable, DependencyGraph graph, InjectionContext injectionContext);

  @Override
  public void generate(final ClassStructureBuilder<?> bodyBlockBuilder, final Injectable injectable,
          final DependencyGraph graph, final InjectionContext injectionContext, final TreeLogger logger,
          final GeneratorContext context) {
    controller = new FactoryController(injectable.getInjectedType(), injectable.getFactoryName(), bodyBlockBuilder.getClassDefinition());
    preGenerationHook(bodyBlockBuilder, injectable, graph, injectionContext);

    final List<Statement> factoryInitStatements = generateFactoryInitStatements(bodyBlockBuilder, injectable, graph, injectionContext);
    final List<Statement> createInstanceStatements = generateCreateInstanceStatements(bodyBlockBuilder, injectable, graph, injectionContext);
    final List<Statement> destroyInstanceStatements = generateDestroyInstanceStatements(bodyBlockBuilder, injectable, graph, injectionContext);
    final List<Statement> invokePostConstructStatements = generateInvokePostConstructsStatements(bodyBlockBuilder, injectable, graph, injectionContext);

    implementConstructor(bodyBlockBuilder, injectable);
    maybeImplementFactoryInit(bodyBlockBuilder, injectable, factoryInitStatements);
    implementCreateInstance(bodyBlockBuilder, injectable, createInstanceStatements);
    maybeImplementDestroyInstance(bodyBlockBuilder, injectable, destroyInstanceStatements);
    maybeImplementInvokePostConstructs(bodyBlockBuilder, injectable, invokePostConstructStatements);
    maybeImplementCreateProxy(bodyBlockBuilder, injectable);

    addPrivateAccessors(bodyBlockBuilder);
  }

  private void addPrivateAccessors(final ClassStructureBuilder<?> bodyBlockBuilder) {
    for (final MetaField field : controller.getExposedFields()) {
      addPrivateAccessStubs("jsni", bodyBlockBuilder, field);
    }
    for (final MetaMethod method : controller.getExposedMethods()) {
      addPrivateAccessStubs("jsni", bodyBlockBuilder, method);
    }
    for (final MetaConstructor constructor : controller.getExposedConstructors()) {
      addPrivateAccessStubs("jsni", bodyBlockBuilder, constructor);
    }
  }

  protected void preGenerationHook(final ClassStructureBuilder<?> bodyBlockBuilder, final Injectable injectable,
          final DependencyGraph graph, final InjectionContext injectionContext) {
  }

  /**
   * @param bodyBlockBuilder
   *          The {@link ClassStructureBuilder} for the {@link Factory} being
   *          generated.
   * @param injectable
   *          Contains metadata (including dependencies) or the bean that the
   *          generated factory will produce.
   * @param graph
   *          The dependency graph that the {@link Injectable} parameter is
   *          from.
   * @param injectionContext
   *          The single injection context shared between all
   *          {@link FactoryBodyGenerator FactoryBodyGenerators}.
   * @return A list of statements that will generated in the
   *         {@link Factory#init(Context)} method.
   */
  protected List<Statement> generateFactoryInitStatements(final ClassStructureBuilder<?> bodyBlockBuilder,
          final Injectable injectable, final DependencyGraph graph, final InjectionContext injectionContext) {
    return Collections.emptyList();
  }

  /**
   * @param bodyBlockBuilder
   *          The {@link ClassStructureBuilder} for the {@link Factory} being
   *          generated.
   * @param injectable
   *          Contains metadata (including dependencies) or the bean that the
   *          generated factory will produce.
   * @param graph
   *          The dependency graph that the {@link Injectable} parameter is
   *          from.
   * @param injectionContext
   *          The single injection context shared between all
   *          {@link FactoryBodyGenerator FactoryBodyGenerators}.
   * @return A list of statements that will generated in the
   *         {@link Factory#invokePostConstructs(Object)} method.
   */
  protected List<Statement> generateInvokePostConstructsStatements(final ClassStructureBuilder<?> bodyBlockBuilder,
          final Injectable injectable, final DependencyGraph graph, final InjectionContext injectionContext) {
    return Collections.emptyList();
  }

  private void maybeImplementFactoryInit(final ClassStructureBuilder<?> bodyBlockBuilder, final Injectable injectable,
          final List<Statement> factoryInitStatements) {
    if (!factoryInitStatements.isEmpty()) {
      bodyBlockBuilder.publicMethod(void.class, "init", finalOf(Context.class, "context")).appendAll(factoryInitStatements).finish();
    }
  }

  private void maybeImplementInvokePostConstructs(final ClassStructureBuilder<?> bodyBlockBuilder,
          final Injectable injectable, final List<Statement> invokePostConstructStatements) {
    if (!invokePostConstructStatements.isEmpty()) {
      bodyBlockBuilder
      .publicMethod(MetaClassFactory.get(void.class), "invokePostConstructs", finalOf(injectable.getInjectedType(), "instance"))
      .appendAll(invokePostConstructStatements).finish();
    }
  }

  private void maybeImplementDestroyInstance(final ClassStructureBuilder<?> bodyBlockBuilder, final Injectable injectable,
          final List<Statement> destroyInstanceStatements) {
    if (!destroyInstanceStatements.isEmpty()) {
      bodyBlockBuilder
      .publicMethod(void.class, "generatedDestroyInstance", finalOf(Object.class, "instance"),
              finalOf(ContextManager.class, "contextManager"))
      .append(loadVariable("this").invoke("destroyInstanceHelper",
              Stmt.castTo(injectable.getInjectedType(), loadVariable("instance")),
              loadVariable("contextManager")))
      .finish();
      bodyBlockBuilder.publicMethod(void.class, "destroyInstanceHelper",
              finalOf(injectable.getInjectedType(), "instance"), finalOf(ContextManager.class, "contextManager"))
      .appendAll(destroyInstanceStatements).finish();
    }
  }

  /**
   * @param bodyBlockBuilder
   *          The {@link ClassStructureBuilder} for the {@link Factory} being
   *          generated.
   * @param injectable
   *          Contains metadata (including dependencies) or the bean that the
   *          generated factory will produce.
   * @param graph
   *          The dependency graph that the {@link Injectable} parameter is
   *          from.
   * @param injectionContext
   *          The single injection context shared between all
   *          {@link FactoryBodyGenerator FactoryBodyGenerators}.
   * @return A list of statements that will generated in the
   *         {@link Factory#destroyInstance(Object, ContextManager)} method.
   */
  protected List<Statement> generateDestroyInstanceStatements(final ClassStructureBuilder<?> bodyBlockBuilder,
          final Injectable injectable, final DependencyGraph graph, final InjectionContext injectionContext) {
    return controller.getDestructionStatements();
  }

  protected void implementConstructor(final ClassStructureBuilder<?> bodyBlockBuilder, final Injectable injectable) {
    final Statement newObject = generateFactoryHandleStatement(injectable);
    final ConstructorBlockBuilder<?> con = bodyBlockBuilder.publicConstructor();
    con.callSuper(newObject);
    con.append(loadVariable("handle").invoke("setAssignableTypes", getAssignableTypesArrayStmt(injectable)));

    final org.jboss.errai.ioc.rebind.ioc.graph.api.Qualifier qualifier = injectable.getQualifier();
    if (!qualifier.isDefaultQualifier()) {
      final AbstractStatementBuilder qualArray =
              getAnnotationArrayStmt(qualifier);
      con.append(loadVariable("handle").invoke("setQualifiers", qualArray));
    }
    con.finish();
  }

  public static AbstractStatementBuilder getAssignableTypesArrayStmt(final Injectable injectable) {
    final Object[] assignableTypes =
            injectable.getAnnotatedObject()
            .flatMap(annotated -> Optional.ofNullable(annotated.getAnnotation(Typed.class)))
            .map(typedAnno -> typedAnno.value())
            // Ensure that Object is an assignable type
            .map(beanTypes -> {
              if (Arrays.stream(beanTypes).anyMatch(type -> Object.class.equals(type))) {
                return (Object[]) beanTypes;
              }
              else {
                final Class<?>[] copyWithObject = Arrays.copyOf(beanTypes, beanTypes.length+1);
                copyWithObject[beanTypes.length] = Object.class;
                return (Object[]) copyWithObject;
              }
            })
            .orElseGet(() -> getAllAssignableTypes(injectable.getInjectedType()).stream().filter(MetaClass::isPublic).toArray());

    return newArray(Class.class).initialize(assignableTypes);
  }

  public static AbstractStatementBuilder getAnnotationArrayStmt(final org.jboss.errai.ioc.rebind.ioc.graph.api.Qualifier qualifier) {
    return newArray(Annotation.class).initialize(qualifier.stream().map(AbstractBodyGenerator::annotationLiteral).toArray());
  }

  protected Statement generateFactoryHandleStatement(final Injectable injectable) {
    final Statement newObject;
    if (injectable.getInjectedType().isAnnotationPresent(ActivatedBy.class)) {
      final Class<? extends BeanActivator> activatorType = injectable.getInjectedType().getAnnotation(ActivatedBy.class).value();
      newObject = newObject(FactoryHandleImpl.class, loadLiteral(injectable.getInjectedType()),
              injectable.getFactoryName(), injectable.getScope(), isEager(injectable.getInjectedType()),
              injectable.getBeanName(), !injectable.isContextual(), loadLiteral(activatorType));
    } else {
      newObject = newObject(FactoryHandleImpl.class, loadLiteral(injectable.getInjectedType()),
              injectable.getFactoryName(), injectable.getScope(), isEager(injectable.getInjectedType()),
              injectable.getBeanName(), !injectable.isContextual());
    }
    return newObject;
  }

  protected static Object isEager(final MetaClass injectedType) {
    return injectedType.isAnnotationPresent(EntryPoint.class) ||
            // TODO review this before adding any scopes other than app-scoped and depdendent
            (!injectedType.isAnnotationPresent(Dependent.class) && hasStartupAnnotation(injectedType));
  }

  protected static boolean hasStartupAnnotation(final MetaClass injectedType) {
    for (final Annotation anno : injectedType.getAnnotations()) {
      if (anno.annotationType().getName().equals("javax.ejb.Startup")) {
        return true;
      }
    }

    return false;
  }

  public static Statement annotationLiteral(final Annotation qual) {
    if (qual.annotationType().equals(Any.class)) {
      return loadStatic(QualifierUtil.class, "ANY_ANNOTATION");
    } else if (qual.annotationType().equals(Default.class)) {
      return loadStatic(QualifierUtil.class, "DEFAULT_ANNOTATION");
    } else if (qual.annotationType().equals(Named.class)) {
      return invokeStatic(QualifierUtil.class, "createNamed", ((Named) qual).value());
    } else {
      return LiteralFactory.getLiteral(qual);
    }
  }

  protected Collection<Annotation> getQualifiers(final HasAnnotations injectedType) {
    final Collection<Annotation> annos = new ArrayList<>();
    for (final Annotation anno : injectedType.getAnnotations()) {
      if (anno.annotationType().isAnnotationPresent(Qualifier.class)) {
        annos.add(anno);
      }
    }

    return annos;
  }

  protected MetaClass[] getTypeArguments(final MetaClass type) {
    final MetaParameterizedType pType = type.getParameterizedType();
    final MetaType[] typeArgs = (pType != null ? pType.getTypeParameters() : new MetaType[0]);
    final MetaClass[] typeArgsClasses = new MetaClass[typeArgs.length];

    for (int i = 0; i < typeArgs.length; i++) {
      final MetaType argType = typeArgs[i];

      if (argType instanceof MetaClass) {
        typeArgsClasses[i] = (MetaClass) argType;
      }
      else if (argType instanceof MetaParameterizedType) {
        typeArgsClasses[i] = (MetaClass) ((MetaParameterizedType) argType).getRawType();
      }
    }
    return typeArgsClasses;
  }

  public static Collection<MetaClass> getAllAssignableTypes(final MetaClass injectedType) {
    return injectedType.getAllSuperTypesAndInterfaces();
  }

}
