/*
 * Copyright 2011 JBoss, by Red Hat, 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 org.jboss.errai.ioc.rebind.ioc.injector.api;


import org.jboss.errai.codegen.Statement;
import org.jboss.errai.codegen.literal.LiteralFactory;
import org.jboss.errai.codegen.meta.MetaClass;
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.util.Refs;
import org.jboss.errai.codegen.util.Stmt;
import org.jboss.errai.ioc.rebind.ioc.injector.InjectUtil;
import org.jboss.errai.ioc.rebind.ioc.injector.Injector;

import java.lang.annotation.Annotation;

public class InjectableInstance<T extends Annotation> extends InjectionPoint<T> {

  public InjectableInstance(final T annotation, final TaskType taskType, final MetaConstructor constructor, final MetaMethod method,
                            final MetaField field, final MetaClass type, final MetaParameter parm, final Injector injector, final InjectionContext injectionContext) {

    super(annotation, taskType, constructor, method, field, type, parm, injector, injectionContext);
  }

  public static <T extends Annotation> InjectableInstance<T> getInjectedInstance(final T annotation,
                                                                                 final MetaClass type,
                                                                                 final Injector injector,
                                                                                 final InjectionContext context) {
    return new InjectableInstance<T>(annotation, TaskType.Type, null, null, null, type,
            null, injector, context);

  }

  public static <T extends Annotation> InjectableInstance<T> getMethodInjectedInstance(final MetaMethod method,
                                                                                       final Injector injector,
                                                                                       final InjectionContext context) {

    //noinspection unchecked
    return new InjectableInstance(
            context.getMatchingAnnotationForElementType(WiringElementType.InjectionPoint, method),
            !method.isPublic() ? TaskType.PrivateMethod : TaskType.Method, null,
            method, null,
            method.getDeclaringClass(),
            null, injector, context);

  }

  public static <T extends Annotation> InjectableInstance<T> getParameterInjectedInstance(final MetaParameter parm,
                                                                                          final Injector injector,
                                                                                          final InjectionContext context) {

    if (parm.getDeclaringMember() instanceof MetaConstructor) {

      //noinspection unchecked
      return new InjectableInstance(context.getMatchingAnnotationForElementType(WiringElementType.InjectionPoint,
              parm.getDeclaringMember()),
              TaskType.Parameter, ((MetaConstructor) parm.getDeclaringMember()),
              null, null, parm.getDeclaringMember().getDeclaringClass(), parm, injector, context);
    }
    else {
      //noinspection unchecked
      return new InjectableInstance(context.getMatchingAnnotationForElementType(WiringElementType.InjectionPoint,
              parm.getDeclaringMember()),
              TaskType.Parameter, null,
              ((MetaMethod) parm.getDeclaringMember()), null, parm.getDeclaringMember().getDeclaringClass(),
              parm, injector, context);
    }
  }

  public static <T extends Annotation> InjectableInstance<T> getFieldInjectedInstance(final MetaField field,
                                                                                      final Injector injector,
                                                                                      final InjectionContext context) {

    //noinspection unchecked
    return new InjectableInstance(context.getMatchingAnnotationForElementType(WiringElementType.InjectionPoint,
            field),
            !field.isPublic() ? TaskType.PrivateField : TaskType.Field, null,
            null, field,
            field.getDeclaringClass(),
            null, injector, context);

  }

  /**
   * Returns an instance of a {@link Statement} which represents the value associated for injection at this
   * InjectionPoint.
   *
   * @return a statement representing the value of the injection point.
   */
  public Statement getValueStatement() {

    final Statement[] stmt;
    final Statement val;

    if (getTargetInjector().getInjectedType().equals(getEnclosingType()) &&
        getTargetInjector().getQualifyingMetadata().equals(getQualifyingMetadata())) {

      val = Refs.get(getInjector().getInstanceVarName());
    }
    else {
      val = Refs.get(getTargetInjector().getInstanceVarName());
    }

    switch (taskType) {
      case Field:
      case PrivateField:
        return InjectUtil.getPublicOrPrivateFieldValue(injectionContext,
                val,
                field);

      case PrivateMethod:
      case Method:
        if (method.getReturnType().isVoid()) {
          return Stmt.load(Void.class);
        }

        stmt = InjectUtil.resolveInjectionDependencies(method.getParameters(), injectionContext, method);

        return InjectUtil.invokePublicOrPrivateMethod(injectionContext,
                val,
                method,
                stmt);

      case Parameter:
        final Statement inlineStmt = injectionContext.getInlineBeanReference(parm);
        if (inlineStmt == null) {
          return Stmt.loadVariable("context").invoke("getBeanInstance",
                  parm.getType(),
                  InjectUtil.getQualifiersFromAnnotationsAsArray(parm.getAnnotations()));
        }
        else {
          return inlineStmt;
        }
      case Type:
        return val;

      default:
        return LiteralFactory.getLiteral(null);
    }
  }

  public Injector getTargetInjector() {
    final MetaClass targetType = getInjector() == null ? getEnclosingType() : getInjector().getInjectedType();

    Injector targetInjector
            = isProxy() ? injectionContext.getProxiedInjector(targetType, getQualifyingMetadata()) :
            injectionContext.getQualifiedInjector(targetType, getQualifyingMetadata());

    if (!isProxy()) {
      if (!targetInjector.isCreated()) {
        targetInjector = InjectUtil.getOrCreateProxy(injectionContext, getEnclosingType(), getQualifyingMetadata());
        if (targetInjector.isEnabled()) {
          targetInjector.getBeanInstance(this);
        }
      }
    }

    return targetInjector;
  }

  public Statement callOrBind(final Statement... values) {
    final Injector targetInjector = injector;

    MetaMethod meth = method;
    switch (taskType) {
      case PrivateField:
      case Field:
        return InjectUtil.setPublicOrPrivateFieldValue(
            injectionContext,
            Refs.get(targetInjector.getInstanceVarName()),
            field,
            values[0]);

      case Parameter:
        if (parm.getDeclaringMember() instanceof MetaMethod) {
          meth = (MetaMethod) parm.getDeclaringMember();
        }
        else {
          throw new RuntimeException("cannot call task on element: " + parm.getDeclaringMember());
        }

      case Method:
      case PrivateMethod:
        return InjectUtil.invokePublicOrPrivateMethod(injectionContext,
            Refs.get(targetInjector.getInstanceVarName()),
            meth,
            values);

      case Type:
      default:
        throw new RuntimeException("cannot call tasktype: " + taskType);
    }
  }
}
