package dst.ass2.di.impl;

import dst.ass2.di.IInjectionController;
import dst.ass2.di.InjectionException;
import dst.ass2.di.annotation.Component;
import dst.ass2.di.annotation.ComponentId;
import dst.ass2.di.annotation.Inject;
import dst.ass2.di.annotation.Scope;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

public class InjectionControllerImpl implements IInjectionController {
    private final Map<Class<?>, Object> singletons = new ConcurrentHashMap<>();
    private final AtomicLong ids = new AtomicLong(0L);
    private final boolean isTransparent;

    public InjectionControllerImpl(boolean b) {
        isTransparent = b;
    }

    private void initializeClass(Object obj, Class<?> clazz, Long id) throws IllegalAccessException {
        if (clazz == null || !clazz.isAnnotationPresent(Component.class)) {
            return; // nothing to be done here.
        }

        boolean idPresent = false;
        for (Field f : clazz.getDeclaredFields()) {

            // handle @ComponentID
            if (f.isAnnotationPresent(ComponentId.class)) {
                idPresent = true;
                if (f.getType() != Long.class) {
                    throw new InjectionException(f.getName() + " is of wrong type");
                }

                boolean accessible = f.isAccessible();
                f.setAccessible(true);
                if (id == null) {
                    id = ids.getAndIncrement();
                }
                f.set(obj, id);
                f.setAccessible(accessible);
            }

            // handle @Inject
            if (f.isAnnotationPresent(Inject.class)) {
                Class<?> fieldType = f.getType();
                if (f.getAnnotation(Inject.class).specificType() != Void.class) {
                    fieldType = f.getAnnotation(Inject.class).specificType();
                }
                if (!fieldType.isAnnotationPresent(Component.class)) {
                    continue;
                }

                Object fieldObject = singletons.get(fieldType);

                if (fieldObject == null) {
                    try {
                        try {
                            fieldObject = fieldType.getConstructor(Inject.class).newInstance(f.getAnnotation(Inject.class));
                        } catch (InvocationTargetException | NoSuchMethodException e) {
                            fieldObject = fieldType.newInstance();
                        }
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    }
                    // only do this in standalone mode.
                    if (!isTransparent) {
                        initialize(fieldObject);
                    }
                }

                boolean accessible = f.isAccessible();
                f.setAccessible(true);
                try {
                    f.set(obj, fieldObject);
                } catch (IllegalAccessException | IllegalArgumentException e) {
                    if (f.getAnnotation(Inject.class).required()) {
                        throw new InjectionException(e);
                    }
                }
                f.setAccessible(accessible);
            }
        }

        if (!idPresent) {
            throw new InjectionException(clazz + " has no @ComponentID");
        }

        // only do this in standalone mode.
        if (!isTransparent) initializeClass(obj, clazz.getSuperclass(), id);
    }


    @Override
    public void initialize(Object obj) throws InjectionException {
        if (!obj.getClass().isAnnotationPresent(Component.class)) {
            throw new InjectionException(obj.getClass() + " isn't annotated with: @Component");
        }
        if (singletons.get(obj.getClass()) != null) {
            throw new InjectionException(obj.getClass() + " singleton already initialized");
        }

        try {
            initializeClass(obj, obj.getClass(), null);
        } catch (IllegalAccessException e) {
            throw new InjectionException(e);
        }

        // add singleton to singleton map, if singleton.
        if (obj.getClass().getAnnotation(Component.class).scope().equals(Scope.SINGLETON)) {
            singletons.put(obj.getClass(), obj);
        }
    }

    @Override
    public <T> T getSingletonInstance(Class<T> clazz) throws InjectionException {
        if (!clazz.isAnnotationPresent(Component.class)) {
            throw new InjectionException(clazz + " isn't annotated with: @Component");
        }
        if (clazz.getAnnotation(Component.class).scope().equals(Scope.PROTOTYPE)) {
            throw new InjectionException(clazz + " is not a SINGLETON");
        }

        T single = (T) singletons.get(clazz);
        if (single == null) {
            try {
                single = (T) clazz.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }

            // only do this in standalone mode.
            if (!isTransparent) {
                initialize(single);
            }
        }
        return single;
    }
}
