package com.taxonic.carml.rdf_mapper.impl;

import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toList;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;

import javax.inject.Inject;
import javax.inject.Qualifier;

import com.taxonic.carml.rdf_mapper.PropertyHandler;
import com.taxonic.carml.rdf_mapper.annotations.RdfProperty;

/** An instance of this class is used to create an instance of a
 * custom property handler, based on an annotation on a getter or setter.
 * This class facilitates also performs some dependency injection on
 * the instantiated handler, so that custom code may have access to
 * the current {@code Mapper} instance, for example.
 *
 * @see RdfProperty
 * @see DependencyResolver
 */
public class SpecifiedPropertyHandlerFactory {

	private DependencySettersCache cache;

	public SpecifiedPropertyHandlerFactory(DependencySettersCache cache) {
		this.cache = cache;
	}
	
	public Optional<PropertyHandler> createPropertyHandler(
		RdfProperty annotation,
		DependencyResolver resolver
	) {

		if (annotation.handler().equals(PropertyHandler.class)) {
			return Optional.empty();
		}

		Class<? extends PropertyHandler> handlerCls = annotation.handler();
		
		PropertyHandler handler = createInstance(handlerCls);

		injectDependencies(handlerCls, resolver, handler);

		return Optional.of(handler);
	}
	
	private <T extends PropertyHandler> T createInstance(Class<T> handlerCls) {
		try {
			return handlerCls.getConstructor().newInstance();
		}
		catch (
				InstantiationException |
				IllegalAccessException |
				IllegalArgumentException |
				InvocationTargetException |
				NoSuchMethodException |
				SecurityException e
		) {
			throw new RuntimeException("could not instantiate specified "
				+ "PropertyHandler class [" + handlerCls.getCanonicalName() + "]", e);
		}
	}
	
	private List<Consumer<Object>> createAndCacheDependencySetters(Class<?> cls, DependencyResolver resolver) {
		List<Consumer<Object>> dependencySetters = createDependencySetters(cls, resolver);
		cache.put(cls, dependencySetters);
		return dependencySetters;
	}
	
	private List<Consumer<Object>> createDependencySetters(Class<?> cls, DependencyResolver resolver) {

		return
		stream(cls.getMethods())

		// find methods annotated with @Inject
		.filter(m -> m.getAnnotation(Inject.class) != null)
		
		// for each such setter, create a consumer that will take a
		// handler instance, and will resolve and set the correct
		// dependency.
		.map(m ->
			createDependencySetter(
				m,
				resolver,
				getPropertyType(m),
				getPropertyQualifiers(m)
			)
		)
		
		.collect(toList());
		
	}
	
	// do dependency injection through setter methods annotated with @Inject
	private void injectDependencies(Class<?> cls, DependencyResolver resolver, Object instance) {
		cache
			.get(cls)
			.orElse(createAndCacheDependencySetters(cls, resolver))
			.forEach(i -> i.accept(instance));
	}

	private Type getPropertyType(Method method) {
		List<Type> parameterTypes = asList(method.getGenericParameterTypes());
		if (parameterTypes.isEmpty() || parameterTypes.size() > 1) {
			throw new RuntimeException("method [" + method.getName() + "], annotated "
				+ "with @Inject does NOT take exactly 1 parameter; it takes " + parameterTypes.size());
		}
		return parameterTypes.get(0);
	}
	
	private boolean isQualifierInstance(Annotation annotation) {
		return annotation.annotationType().getAnnotation(Qualifier.class) != null;
	}
	
	private List<Annotation> getPropertyQualifiers(Method method) {
		return
		asList(method.getAnnotations()).stream()
			.filter(this::isQualifierInstance)
			.collect(toList());
	}

	private Consumer<Object> createDependencySetter(
		Method method,
		DependencyResolver resolver,
		Type propertyType,
		List<Annotation> qualifiers
	) {
		return
		i -> {
			
			// resolve dependency through resolver, which may use the property
			// type and qualifiers, if any.
			Object propertyValue = resolver.resolve(propertyType, qualifiers);
			try {
				
				// invoke the setter to set the resolved value
				method.invoke(i, propertyValue);
				
			}
			catch (
				IllegalAccessException |
				IllegalArgumentException |
				InvocationTargetException e
			) {
				throw new RuntimeException("error invoking setter [" + method.getName() + "]", e);
			}
		};
	}
	
}
