package net.azib.ipscan.di;

import java.lang.reflect.Constructor;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;

import static java.util.Arrays.stream;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;

@SuppressWarnings("unchecked")
public class Injector {
	private final Map<Class<?>, Object> instances = new LinkedHashMap<>();
	{ register(Injector.class, this); }

	public <T> void register(Class<T> type, T impl) {
		instances.put(type, impl);
	}

	public <T> T require(Class<T> type) {
		// unfortunately, HashMap.computeIfAbsent() doesn't put values properly in a recursive scenario
		var value = (T) instances.get(type);
		if (value == null) instances.put(type, value = createInstance(type));
		return value;
	}

	public void register(Class<?> ... types) {
		stream(types).forEach(this::require);
	}

	public <T> List<T> requireAll(Class<T> type) {
		return instances.entrySet().stream().filter(e -> type.isAssignableFrom(e.getKey())).map(e -> (T) e.getValue()).collect(toList());
	}

	private <T> T createInstance(Class<T> type) {
		var constructor = (Constructor<T>) stream(type.getConstructors())
			.max(comparing(Constructor::getParameterCount))
			.orElseThrow(() -> new InjectException("No public constructors"));
		try {
			return constructor.newInstance(resolveDeps(constructor));
		}
		catch (Throwable e) {
			throw new InjectException("Cannot create " + type.getName() + ", deps: " + Arrays.toString(constructor.getGenericParameterTypes()), e);
		}
	}

	private Object[] resolveDeps(Constructor<?> constructor) {
		return stream(constructor.getGenericParameterTypes()).map(t -> isCollection(t) ?
			requireAll(collectionItemType(t)) : require(toClass(t))).toArray();
	}

	private Class<?> toClass(Type type) {
		if (type instanceof Class) return (Class<?>) type;
		else if (type instanceof ParameterizedType) return (Class<?>) ((ParameterizedType) type).getRawType();
		else throw new InjectException(type + " is not supported");
	}

	private boolean isCollection(Type type) {
		return type instanceof ParameterizedType && Collection.class.isAssignableFrom(toClass(type));
	}

	private Class<?> collectionItemType(Type type) {
		return (Class<?>) ((ParameterizedType) type).getActualTypeArguments()[0];
	}
}
