package cz.jzitnik.utils;

// Don't blame me that I'm using field injection instead of construction injection. I just like it more leave me alone.
// Yes I know I'll suffer in the unit tests. (who said there will be any? hmmm)

import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.MutableClassToInstanceMap;
import cz.jzitnik.annotations.Config;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectConfig;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;

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

@Slf4j
public class DependencyManager {
    private final ClassToInstanceMap<Object> configs = MutableClassToInstanceMap.create();
    private final ClassToInstanceMap<Object> data = MutableClassToInstanceMap.create();

    public <T> T getDependencyOrThrow(Class<T> clazz) {
        T instance = get(clazz);

        if (instance == null) {
            throw new RuntimeException("Class was not found!");
        }

        return instance;
    }

    public <T> Optional<T> getDependency(Class<T> clazz) {
        return Optional.ofNullable(get(clazz));
    }

    @SuppressWarnings("unchecked")
    private <T> T get(Class<T> clazz) {
        return (T) data.get(clazz);
    }

    public DependencyManager(Reflections reflections) {
        Set<Class<?>> configClasses = reflections.getTypesAnnotatedWith(Config.class);
        for (Class<?> configClass : configClasses) {
            try {
                Constructor<?> constructor = configClass.getDeclaredConstructor();
                var instance = constructor.newInstance();
                configs.put(configClass, instance);
            } catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
                     IllegalAccessException e) {
                log.error("Failed to instantiate config class: {}", configClass.getName(), e);
            }
        }

        data.put(ClassLoader.class, DependencyManager.class.getClassLoader());
        Set<Class<?>> classes = reflections.getTypesAnnotatedWith(Dependency.class);

        // Construct all classes
        for (Class<?> clazz : classes) {
            for (var constructor : clazz.getDeclaredConstructors()) {
                var paramTypes = constructor.getParameterTypes();
                var params = new Object[paramTypes.length];
                boolean suitable = true;

                for (int i = 0; i < paramTypes.length; i++) {
                    Class<?> type = paramTypes[i];
                    if (type == getClass())
                        params[i] = this;
                    else if (type == Reflections.class)
                        params[i] = reflections;
                    else {
                        suitable = false;
                        break;
                    }
                }

                if (!suitable) continue;

                constructor.setAccessible(true);
                try {
                    Object instance = constructor.newInstance(params);
                    Dependency annotation = clazz.getAnnotation(Dependency.class);

                    if (annotation.value() != Object.class) {
                        data.put(annotation.value(), instance);
                    } else {
                        data.put(clazz, instance);
                    }
                } catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
                break; // Found a matching constructor, go to next class
            }
        }

        for (Object instance : data.values()) {
            inject(instance);
        }

    }

    public void inject(Object instance) {
        StateManager stateManager = (StateManager) data.get(StateManager.class);

        List<Field> allFields = new ArrayList<>();
        Class<?> current = instance.getClass();

        while (current != null && current != Object.class) {
            allFields.addAll(Arrays.asList(current.getDeclaredFields()));
            current = current.getSuperclass();
        }

        for (Field field : allFields) {
            if (field.isAnnotationPresent(InjectDependency.class)) {
                field.setAccessible(true);

                if (!data.containsKey(field.getType()) && field.getType() != getClass()) continue;

                Object dependency = field.getType() == getClass() ? this : data.get(field.getType());

                if (!field.getType().isAssignableFrom(dependency.getClass())) continue;

                try {
                    field.set(instance, dependency);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            } else if (field.isAnnotationPresent(InjectState.class)) {
                field.setAccessible(true);

                Optional<?> stateOptional = stateManager.get(field.getType());

                if (stateOptional.isEmpty()) continue;

                try {
                    field.set(instance, stateOptional.get());
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            } else if (field.isAnnotationPresent(InjectConfig.class)) {
                field.setAccessible(true);
                Optional<?> config = Optional.ofNullable(configs.get(field.getType()));

                if (config.isEmpty()) continue;

                try {
                    field.set(instance, config.get());
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
