/*
 * Copyright (C) 2018 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 dagger.reflect;

import static dagger.reflect.Reflection.findQualifier;
import static dagger.reflect.Reflection.newProxy;

import dagger.MembersInjector;
import dagger.Subcomponent;
import dagger.reflect.Binding.LinkedBinding;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.concurrent.ConcurrentHashMap;
import org.jetbrains.annotations.Nullable;

final class ComponentInvocationHandler implements InvocationHandler {
  static <C> C forComponent(Class<C> cls) {
    Scope.Builder scopeBuilder = ComponentScopeBuilder.buildComponent(cls).get();
    return create(cls, scopeBuilder);
  }

  static <C> C create(Class<C> cls, Scope.Builder scopeBuilder) {
    Key componentKey = Key.of(null, cls);
    LinkedLateInstanceBinding<C> componentBinding = new LinkedLateInstanceBinding<>();
    scopeBuilder.addBinding(componentKey, componentBinding);

    Scope scope = scopeBuilder.build();
    C instance = newProxy(cls, new ComponentInvocationHandler(scope));
    componentBinding.setValue(instance);

    return instance;
  }

  private final Scope scope;
  private final ConcurrentHashMap<Method, MethodInvocationHandler> handlers =
      new ConcurrentHashMap<>();

  private ComponentInvocationHandler(Scope scope) {
    this.scope = scope;
  }

  @Override
  public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (method.getDeclaringClass() == Object.class) {
      return method.invoke(this, args);
    }

    MethodInvocationHandler handler = handlers.get(method);
    if (handler == null) {
      handler = createMethodInvocationHandler(method, scope);
      MethodInvocationHandler replaced = handlers.putIfAbsent(method, handler);
      if (replaced != null) {
        handler = replaced;
      }
    }
    return handler.invoke(args);
  }

  private static ComponentInvocationHandler.MethodInvocationHandler createMethodInvocationHandler(
      Method method, Scope scope) {
    Type returnType = method.getGenericReturnType();
    Class<?>[] parameterTypes = method.getParameterTypes();

    if (returnType instanceof Class<?>) {
      Class<?> returnClass = (Class<?>) returnType;
      if (returnClass.getAnnotation(Subcomponent.class) != null) {
        return new SubcomponentMethodInvocationHandler(method, returnClass, scope);
      }
      if (returnClass.getAnnotation(Subcomponent.Builder.class) != null) {
        if (parameterTypes.length != 0) {
          throw new IllegalStateException(method.toString()); // TODO
        }
        return new SubcomponentBuilderMethodInvocationHandler(returnClass, scope);
      }
      if (returnClass.getAnnotation(Subcomponent.Factory.class) != null) {
        if (parameterTypes.length != 0) {
          throw new IllegalStateException(method.toString()); // TODO
        }
        return new SubcomponentFactoryMethodInvocationHandler(returnClass, scope);
      }
    }

    if (parameterTypes.length == 0) {
      Key key = Key.of(findQualifier(method.getDeclaredAnnotations()), returnType);
      LinkedBinding<?> binding = scope.getBinding(key);
      return new ProvisionMethodInvocationHandler(binding);
    }

    if (parameterTypes.length == 1) {
      boolean returnInstance;
      if (returnType == void.class) {
        returnInstance = false;
      } else if (method.getReturnType().equals(parameterTypes[0])) {
        returnInstance = true;
      } else {
        throw new IllegalStateException(
            "Members injection methods may only return the injected type or void: "
                + method.getDeclaringClass().getName()
                + '.'
                + method.getName());
      }

      // RedundantCast: see https://youtrack.jetbrains.com/issue/IDEA-206560
      @SuppressWarnings({"unchecked", "RedundantCast"})
      MembersInjector<Object> injector =
          (MembersInjector<Object>) ReflectiveMembersInjector.create(parameterTypes[0], scope);
      return new MembersInjectorMethodInvocationHandler(injector, returnInstance);
    }

    throw new IllegalStateException(method.toString()); // TODO unsupported method shape
  }

  private interface MethodInvocationHandler {
    @Nullable
    Object invoke(Object[] args);
  }

  private static final class ProvisionMethodInvocationHandler implements MethodInvocationHandler {
    private final LinkedBinding<?> binding;

    ProvisionMethodInvocationHandler(LinkedBinding<?> binding) {
      this.binding = binding;
    }

    @Override
    public @Nullable Object invoke(Object[] args) {
      return binding.get();
    }
  }

  private static final class MembersInjectorMethodInvocationHandler
      implements MethodInvocationHandler {
    private final MembersInjector<Object> membersInjector;
    private final boolean returnInstance;

    MembersInjectorMethodInvocationHandler(
        MembersInjector<Object> membersInjector, boolean returnInstance) {
      this.membersInjector = membersInjector;
      this.returnInstance = returnInstance;
    }

    @Override
    public @Nullable Object invoke(Object[] args) {
      Object instance = args[0];
      membersInjector.injectMembers(instance);
      return returnInstance ? instance : null;
    }
  }

  private static final class SubcomponentMethodInvocationHandler
      implements MethodInvocationHandler {
    private final Method method;
    private final Class<?> cls;
    private final Scope scope;

    SubcomponentMethodInvocationHandler(Method method, Class<?> cls, Scope scope) {
      this.method = method;
      this.cls = cls;
      this.scope = scope;
    }

    @Override
    public Object invoke(Object[] args) {
      ComponentScopeBuilder scopeBuilder = ComponentScopeBuilder.buildSubcomponent(cls, scope);
      ComponentFactoryInvocationHandler.parseFactoryMethod(method, args, scopeBuilder);
      return create(cls, scopeBuilder.get());
    }
  }

  private static final class SubcomponentBuilderMethodInvocationHandler
      implements MethodInvocationHandler {
    private final Class<?> cls;
    private final Scope scope;

    SubcomponentBuilderMethodInvocationHandler(Class<?> cls, Scope scope) {
      this.cls = cls;
      this.scope = scope;
    }

    @Override
    public Object invoke(Object[] args) {
      return ComponentBuilderInvocationHandler.forSubcomponentBuilder(cls, scope);
    }
  }

  private static final class SubcomponentFactoryMethodInvocationHandler
      implements MethodInvocationHandler {
    private final Class<?> cls;
    private final Scope scope;

    SubcomponentFactoryMethodInvocationHandler(Class<?> cls, Scope scope) {
      this.cls = cls;
      this.scope = scope;
    }

    @Override
    public Object invoke(Object[] args) {
      return ComponentFactoryInvocationHandler.forSubcomponentFactory(cls, scope);
    }
  }
}