/*
 * Copyright (C) 2010 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.inject.mini;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import javax.inject.Provider;

/**
 * Proof of concept. A tiny injector suitable for tiny applications.
 *
 * @author jessewilson@google.com (Jesse Wilson)
 * @since 3.0
 */
public final class MiniGuice {
  private static final Object UNINITIALIZED = new Object();

  private MiniGuice() {}

  private final Map<Key, Provider<?>> bindings = new HashMap<>();
  private final Queue<RequiredKey> requiredKeys = new ArrayDeque<>();
  private final Set<Key> singletons = new HashSet<>();

  /**
   * Creates an injector defined by {@code modules} and immediately uses it to create an instance of
   * {@code type}. The modules can be of any type, and must contain {@code @Provides} methods.
   *
   * <p>The following injection features are supported:
   *
   * <ul>
   * <li>Field injection. A class may have any number of field injections, and fields may be of any
   *     visibility. Static fields will be injected each time an instance is injected.
   * <li>Constructor injection. A class may have a single {@code @Inject}-annotated constructor.
   *     Classes that have fields injected may omit the {@link @Inject} annotation if they have a
   *     public no-arguments constructor.
   * <li>Injection of {@code @Provides} method parameters.
   * <li>{@code @Provides} methods annotated {@code @Singleton}.
   * <li>Constructor-injected classes annotated {@code @Singleton}.
   * <li>Injection of {@link Provider}s.
   * <li>Binding annotations on injected parameters and fields.
   * <li>Guice annotations.
   * <li>JSR 330 annotations.
   * <li>Eager loading of singletons.
   * </ul>
   *
   * <p><strong>Note that method injection is not supported.</strong>
   */
  public static <T> T inject(Class<T> type, Object... modules) {
    Key key = new Key(type, null);
    MiniGuice miniGuice = new MiniGuice();
    for (Object module : modules) {
      miniGuice.install(module);
    }
    miniGuice.requireKey(key, "root injection");
    miniGuice.addJitBindings();
    miniGuice.addProviderBindings();
    miniGuice.eagerlyLoadSingletons();
    Provider<?> provider = miniGuice.bindings.get(key);
    return type.cast(provider.get());
  }

  private void addProviderBindings() {
    Map<Key, Provider<?>> providerBindings = new HashMap<>();
    for (final Map.Entry<Key, Provider<?>> binding : bindings.entrySet()) {
      Key key = binding.getKey();
      final Provider<?> value = binding.getValue();
      Provider<Provider<?>> providerProvider =
          new Provider<Provider<?>>() {
            @Override
            public Provider<?> get() {
              return value;
            }
          };
      providerBindings.put(
          new Key(new ProviderType(javax.inject.Provider.class, key.type), key.annotation),
          providerProvider);
    }
    bindings.putAll(providerBindings);
  }

  private void requireKey(Key key, Object requiredBy) {
    if (key.type instanceof ParameterizedType
        && (((ParameterizedType) key.type).getRawType() == Provider.class
            || ((ParameterizedType) key.type).getRawType() == javax.inject.Provider.class)) {
      Type type = ((ParameterizedType) key.type).getActualTypeArguments()[0];
      key = new Key(type, key.annotation);
    }

    requiredKeys.add(new RequiredKey(key, requiredBy));
  }

  private void eagerlyLoadSingletons() {
    for (Key key : singletons) {
      Provider<?> provider = bindings.get(key);
      final Object onlyInstance = provider.get();
      bindings.put(
          key,
          new Provider<Object>() {
            @Override
            public Object get() {
              return onlyInstance;
            }
          });
    }
  }

  public void install(Object module) {
    boolean hasProvidesMethods = false;
    for (Class<?> c = module.getClass(); c != Object.class; c = c.getSuperclass()) {
      for (Method method : c.getDeclaredMethods()) {
        if (method.isAnnotationPresent(com.google.inject.Provides.class)) {
          Key key = key(method, method.getGenericReturnType(), method.getAnnotations());
          addProviderMethodBinding(key, module, method);
          hasProvidesMethods = true;
        }
      }
    }
    if (!hasProvidesMethods) {
      throw new IllegalArgumentException("No @Provides methods on " + module);
    }
  }

  private void addProviderMethodBinding(Key key, final Object instance, final Method method) {
    final Key[] parameterKeys =
        parametersToKeys(
            method, method.getGenericParameterTypes(), method.getParameterAnnotations());
    method.setAccessible(true);
    final Provider<Object> unscoped =
        new Provider<Object>() {
          @Override
          public Object get() {
            Object[] parameters = keysToValues(parameterKeys);
            try {
              return method.invoke(instance, parameters);
            } catch (IllegalAccessException e) {
              throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
              throw new RuntimeException(e.getCause());
            }
          }
        };

    boolean singleton = method.isAnnotationPresent(javax.inject.Singleton.class);
    putBinding(key, unscoped, singleton);
  }

  private void addJitBindings() {
    RequiredKey requiredKey;
    while ((requiredKey = requiredKeys.poll()) != null) {
      Key key = requiredKey.key;
      if (bindings.containsKey(key)) {
        continue;
      }
      if (!(key.type instanceof Class) || key.annotation != null) {
        throw new IllegalArgumentException("No binding for " + key);
      }
      addJitBinding(key, requiredKey.requiredBy);
    }
  }

  private void addJitBinding(Key key, Object requiredBy) {
    Class<?> type = (Class<?>) key.type;

    /*
     * Lookup the injectable fields and their corresponding keys.
     */
    final List<Field> injectedFields = new ArrayList<>();
    List<Object> fieldKeysList = new ArrayList<>();
    for (Class<?> c = type; c != Object.class; c = c.getSuperclass()) {
      for (Field field : c.getDeclaredFields()) {
        if (!field.isAnnotationPresent(javax.inject.Inject.class)) {
          continue;
        }
        field.setAccessible(true);
        injectedFields.add(field);
        Key fieldKey = key(field, field.getGenericType(), field.getAnnotations());
        fieldKeysList.add(fieldKey);
        requireKey(fieldKey, field);
      }
    }
    final Key[] fieldKeys = fieldKeysList.toArray(new Key[fieldKeysList.size()]);

    /*
     * Lookup @Inject-annotated constructors. If there's no @Inject-annotated
     * constructor, use a default constructor if the class has other injections.
     */
    Constructor<?> injectedConstructor = null;
    for (Constructor<?> constructor : type.getDeclaredConstructors()) {
      if (!constructor.isAnnotationPresent(javax.inject.Inject.class)) {
        continue;
      }
      if (injectedConstructor != null) {
        throw new IllegalArgumentException("Too many injectable constructors on " + type);
      }
      constructor.setAccessible(true);
      injectedConstructor = constructor;
    }
    if (injectedConstructor == null) {
      if (fieldKeys.length == 0) {
        throw new IllegalArgumentException(
            "No injectable constructor on " + type + " required by " + requiredBy);
      }
      try {
        injectedConstructor = type.getConstructor();
      } catch (NoSuchMethodException e) {
        throw new IllegalArgumentException(
            "No injectable constructor on " + type + " required by " + requiredBy);
      }
    }

    /*
     * Create a provider that invokes the constructor and sets its fields.
     */
    final Constructor<?> constructor = injectedConstructor;
    final Key[] parameterKeys =
        parametersToKeys(
            constructor,
            constructor.getGenericParameterTypes(),
            constructor.getParameterAnnotations());
    final Provider<Object> unscoped =
        new Provider<Object>() {
          @Override
          public Object get() {
            Object[] constructorParameters = keysToValues(parameterKeys);
            try {
              Object result = constructor.newInstance(constructorParameters);
              Object[] fieldValues = keysToValues(fieldKeys);
              for (int i = 0; i < fieldValues.length; i++) {
                injectedFields.get(i).set(result, fieldValues[i]);
              }
              return result;
            } catch (IllegalAccessException e) {
              throw new RuntimeException(e.getCause());
            } catch (InvocationTargetException e) {
              throw new RuntimeException(e.getCause());
            } catch (InstantiationException e) {
              throw new RuntimeException(e);
            }
          }
        };

    boolean singleton = type.isAnnotationPresent(javax.inject.Singleton.class);
    putBinding(new Key(type, null), unscoped, singleton);
  }

  private void putBinding(Key key, Provider<Object> provider, boolean singleton) {
    if (singleton) {
      singletons.add(key);
      final Provider<Object> unscoped = provider;
      provider =
          new Provider<Object>() {
            private Object onlyInstance = UNINITIALIZED;

            @Override
            public Object get() {
              if (onlyInstance == UNINITIALIZED) {
                onlyInstance = unscoped.get();
              }
              return onlyInstance;
            }
          };
    }

    if (bindings.put(key, provider) != null) {
      throw new IllegalArgumentException("Duplicate binding " + key);
    }
  }

  private Object[] keysToValues(Key[] parameterKeys) {
    Object[] parameters = new Object[parameterKeys.length];
    for (int i = 0; i < parameterKeys.length; i++) {
      parameters[i] = bindings.get(parameterKeys[i]).get();
    }
    return parameters;
  }

  private Key[] parametersToKeys(Member member, Type[] types, Annotation[][] annotations) {
    final Key[] parameterKeys = new Key[types.length];
    for (int i = 0; i < parameterKeys.length; i++) {
      String name = member + " parameter " + i;
      parameterKeys[i] = key(name, types[i], annotations[i]);
      requireKey(parameterKeys[i], name);
    }
    return parameterKeys;
  }

  public Key key(Object subject, Type type, Annotation[] annotations) {
    Annotation bindingAnnotation = null;
    for (Annotation a : annotations) {
      if (!a.annotationType().isAnnotationPresent(javax.inject.Qualifier.class)) {
        continue;
      }
      if (bindingAnnotation != null) {
        throw new IllegalArgumentException("Too many binding annotations on " + subject);
      }
      bindingAnnotation = a;
    }
    return new Key(type, bindingAnnotation);
  }

  private static boolean equal(Object a, Object b) {
    return a == null ? b == null : a.equals(b);
  }

  private static final class Key {
    final Type type;
    final Annotation annotation;

    Key(Type type, Annotation annotation) {
      this.type = type;
      this.annotation = annotation;
    }

    @Override
    public boolean equals(Object o) {
      return o instanceof Key
          && ((Key) o).type.equals(type)
          && equal(annotation, ((Key) o).annotation);
    }

    @Override
    public int hashCode() {
      int result = type.hashCode();
      if (annotation != null) {
        result += (37 * annotation.hashCode());
      }
      return result;
    }

    @Override
    public String toString() {
      return "key[type=" + type + ",annotation=" + annotation + "]";
    }
  }

  private static class RequiredKey {
    private final Key key;
    private final Object requiredBy;

    private RequiredKey(Key key, Object requiredBy) {
      this.key = key;
      this.requiredBy = requiredBy;
    }
  }

  private static final class ProviderType implements ParameterizedType {
    private final Class<?> rawType;
    private final Type typeArgument;

    public ProviderType(Class<?> rawType, Type typeArgument) {
      this.rawType = rawType;
      this.typeArgument = typeArgument;
    }

    @Override
    public Type getRawType() {
      return rawType;
    }

    @Override
    public Type[] getActualTypeArguments() {
      return new Type[] {typeArgument};
    }

    @Override
    public Type getOwnerType() {
      return null;
    }

    @Override
    public boolean equals(Object o) {
      if (o instanceof ParameterizedType) {
        ParameterizedType that = (ParameterizedType) o;
        return Arrays.equals(getActualTypeArguments(), that.getActualTypeArguments())
            && that.getRawType() == rawType;
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Arrays.hashCode(getActualTypeArguments()) ^ rawType.hashCode();
    }
  }
}
