package com.episode6.hackit.mockspresso.internal;

import com.episode6.hackit.mockspresso.annotation.RealObject;
import com.episode6.hackit.mockspresso.api.DependencyProvider;
import com.episode6.hackit.mockspresso.exception.RealObjectMappingMismatchException;
import com.episode6.hackit.mockspresso.reflect.DependencyKey;
import com.episode6.hackit.mockspresso.reflect.ReflectUtil;
import com.episode6.hackit.mockspresso.reflect.TypeToken;

import java.lang.reflect.Field;
import java.util.*;

/**
 * An internal class used to keep track of, and fill in, null fields annotated with @RealObject.
 *
 * To use, first pass all of your test objects (objects with annotated fields) into
 * {@link #scanNullRealObjectFields(Object)}. This will populate a map of all field-object pairs
 * that must be filled in.
 *
 * After all test objects have been scanned, call {@link #createAndAssignTrackedRealObjects()}.
 * The provided {@link RealObjectMaker} will be used to create all of the missing @RealObjects. If
 * One @RealObject depends on another @RealObject, they will be created in-order.
 */
public class RealObjectFieldTracker {

  private final HashMap<DependencyKey, Entry> mNullRealObjectFields = new HashMap<>();

  private final RealObjectMaker mRealObjectMaker;
  private final DependencyMap mDependencyMap;
  private final CustomDependencyProvider mCustomDependencyProvider;

  /**
   * We require the full {@link MockspressoConfigContainer} here to ensure the DependencyProvider
   * we work with is attached to the DependencyMap we're populating.
   */
  public RealObjectFieldTracker(MockspressoConfigContainer configContainer) {
    mRealObjectMaker = configContainer.getRealObjectMaker();
    mDependencyMap = configContainer.getDependencyMap();
    mCustomDependencyProvider = new CustomDependencyProvider(configContainer.getDependencyProvider());
  }

  public void scanNullRealObjectFields(Object object) {
    for (Field field : ReflectUtil.getAllDeclaredFields(object.getClass())) {
      if (!field.isAnnotationPresent(RealObject.class)) {
        continue;
      }

      if (!field.isAccessible()) {
        field.setAccessible(true);
      }

      trackFieldIfNull(field, object);
    }
  }

  public void createAndAssignTrackedRealObjects() {
    // create a copy of the keyset, since the contents of mNullRealObjectFields
    // will change unexpectedly depending on the order of dependencies.
    Set<DependencyKey> keySet = new HashSet<>(mNullRealObjectFields.keySet());
    for (DependencyKey key : keySet) {
      createAndAssignObjectIfNeeded(key);
    }
  }

  private void trackFieldIfNull(Field field, Object object) {
    try {
      if (field.get(object) != null) {
        return;
      }
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    }

    DependencyKey key = DependencyKey.fromField(field);
    Entry entry = mNullRealObjectFields.get(key);
    if (entry == null) {
      entry = new Entry(field.getAnnotation(RealObject.class).implementation());
      mNullRealObjectFields.put(key, entry);
    }
    entry.add(field, object);
  }

  @SuppressWarnings("unchecked")
  private void createAndAssignObjectIfNeeded(DependencyKey key) {
    Entry entry = mNullRealObjectFields.remove(key);
    if (entry == null) {
      // dependency has already been handled
      return;
    }

    // if we have a custom implementation class, create it, otherwise use the provided TypeToken.
    TypeToken typeToken = entry.hasCustomImplementation() ? TypeToken.of(entry.implementationClass) : key.typeToken;
    Object value = mRealObjectMaker.createObject(mCustomDependencyProvider, typeToken);
    mDependencyMap.put(key, value);
    for (FieldInstance field : entry.fields) {
      field.setValue(value);
    }
  }

  /**
   * A custom implementation of DependencyProvider. We use this to
   * ensure we check our internal map of null @RealObject dependencies
   * before returning from the provided DependencyProvider;
   */
  private class CustomDependencyProvider implements DependencyProvider {

    private final DependencyProvider mDependencyProvider;

    CustomDependencyProvider(DependencyProvider dependencyProvider) {
      mDependencyProvider = dependencyProvider;
    }

    @Override
    public <T> T get(DependencyKey<T> key) {
      createAndAssignObjectIfNeeded(key);
      return mDependencyProvider.get(key);
    }
  }

  private static class Entry {
    final Class<?> implementationClass;
    final Collection<FieldInstance> fields;

    Entry(Class<?> implementationClass) {
      this.implementationClass = implementationClass;
      this.fields = new LinkedList<>();
    }

    void add(Field field, Object object) {
      Class<?> fieldImplementationClass = field.getAnnotation(RealObject.class).implementation();
      if (fieldImplementationClass != implementationClass) {
        throw new RealObjectMappingMismatchException(DependencyKey.fromField(field));
      }
      fields.add(new FieldInstance(field, object));
    }

    boolean hasCustomImplementation() {
      return implementationClass != RealObject.class;
    }
  }

  private static class FieldInstance {
    final Field field;
    final Object object;

    FieldInstance(Field field, Object object) {
      this.field = field;
      this.object = object;
    }

    void setValue(Object value) {
      try {
        field.set(object, value);
      } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
      }
    }
  }

}

@@ -2966,57 +2966,16 @@
 ield
-.getAnnotation(RealObject.class).implementation()
+, object
 );%0A 
@@ -3019,25 +3019,34 @@
 ntry);%0A    %7D
-%0A
+ else %7B%0A  
     entry.ad
@@ -3063,16 +3063,22 @@
 bject);%0A
+    %7D%0A
   %7D%0A%0A  @
@@ -4358,32 +4358,336 @@
 ey);%0A    %7D%0A  %7D%0A%0A
+  /**%0A   * An entry in our mNullRealObjectFields hashmap. Holds a collection of%0A   * Field/Object pairs and holds a ref to the implementation class to be used.%0A   * When adding new field/object pairs, the implementation class is always checked%0A   * and an exception thrown if there is a mis-match.%0A   */%0A
   private static
@@ -4800,36 +4800,44 @@
 try(
-Class%3C?%3E implementationClass
+Field firstField, Object firstObject
 ) %7B%0A
@@ -4869,16 +4869,59 @@
 Class = 
+firstField.getAnnotation(RealObject.class).
 implemen
@@ -4922,29 +4922,26 @@
 plementation
-Class
+()
 ;%0A      this
@@ -4966,24 +4966,91 @@
 edList%3C%3E();%0A
+      this.fields.add(new FieldInstance(firstField, firstObject));%0A
     %7D%0A%0A    v
@@ -5496,32 +5496,102 @@
 ass;%0A    %7D%0A  %7D%0A%0A
+  /**%0A   * An instance of a field and the object it belongs to.%0A   */%0A
   private static

add some comments an clean up usage in RealObjectFieldTracker