package com.zandero.rest.context;

import com.zandero.rest.data.ClassFactory;
import com.zandero.rest.data.RouteDefinition;
import com.zandero.rest.exception.ClassFactoryException;
import com.zandero.rest.exception.ContextException;
import com.zandero.rest.injection.InjectionProvider;
import com.zandero.utils.Assert;
import io.vertx.core.Vertx;
import io.vertx.core.eventbus.EventBus;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.ext.auth.User;
import io.vertx.ext.web.RoutingContext;

import javax.ws.rs.core.Context;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * Storage of context providers
 */
public class ContextProviderFactory extends ClassFactory<ContextProvider> {

	/**
	 * Cache of classes that need or don't need context injection
	 * If class needs context injection .. a list of Fields to inject is provided
	 * If class doesn't need context injection the list of fields is empty (not null)
	 *
	 * HashMap contains pairs by full class name
	 */
	private static HashMap<String, List<Field>> contextCache = new HashMap<>();

	private static final String CONTEXT_DATA_KEY_PREFIX = "RestRouter-";

	@Override
	protected void init() {
		// nothing to
	}

	public ContextProvider getContextProvider(InjectionProvider provider,
	                                          Class clazzType,
	                                          Class<? extends ContextProvider> aClass,
	                                          RoutingContext context) throws ClassFactoryException,
	                                                                         ContextException {


		return get(clazzType, aClass, provider, context, null);
	}

	public void register(Class<?> aClass, Class<? extends ContextProvider> clazz) {
		super.register(aClass, clazz);
	}

	public void register(Class<?> aClass, ContextProvider instance) {
		super.register(aClass, instance);
	}

	private static List<Field> checkForContext(Class<?> clazz) {

		// check if any class members are injected
		Field[] fields = clazz.getDeclaredFields();
		List<Field> contextFields = new ArrayList<>();
		for (Field field : fields) {
			Annotation found = field.getAnnotation(Context.class);
			if (found != null) {
				contextFields.add(field);
			}
		}

		return contextFields;
	}

	private static List<Field> getContextFields(Class<?> clazz) {

		List<Field> contextFields = contextCache.get(clazz.getName());
		if (contextFields == null) {
			contextFields = checkForContext(clazz);
			contextCache.put(clazz.getName(), contextFields);
		}

		return contextFields;
	}

	public static <T> boolean hasContext(Class<? extends T> clazz) {

		return getContextFields(clazz).size() > 0;
	}

	/**
	 * Provides vertx context of desired type if possible
	 *
	 * @param type         context type
	 * @param defaultValue default value if given
	 * @param context      to provider / extract values from
	 * @return found context or null if not found
	 * @throws ContextException in case context could not be provided
	 */
	public static Object provideContext(Class<?> type,
	                                    String defaultValue,
	                                    RoutingContext context) throws ContextException {

		// vert.x context
		if (type.isAssignableFrom(HttpServerResponse.class)) {
			return context.response();
		}

		if (type.isAssignableFrom(HttpServerRequest.class)) {
			return context.request();
		}

		if (type.isAssignableFrom(RoutingContext.class)) {
			return context;
		}

		// provide vertx via @Context
		if (type.isAssignableFrom(Vertx.class)) {
			return context.vertx();
		}

		// provide event bus via @Context
		if (type.isAssignableFrom(EventBus.class)) {
			return context.vertx().eventBus();
		}

		if (type.isAssignableFrom(User.class)) {
			return context.user();
		}

		// internal context / reflection of route definition
		if (type.isAssignableFrom(RouteDefinition.class)) {
			// TODO: fix this (issue #60) ... we need original RouteDefinition not evaluated one
			return new RouteDefinition(context);
		}

		// browse through context storage
		if (context.data() != null && context.data().size() > 0) {

			Object item = context.data().get(getContextDataKey(type));
			if (item != null) { // found in storage ... return
				return item;
			}
		}

		if (defaultValue != null) {
			// check if type has constructor that can be used with defaultValue ...
			// and create Context type on the fly constructed with defaultValue
			try {
				return ClassFactory.constructType(type, defaultValue);
			}
			catch (ClassFactoryException e) {
				throw new ContextException("Can't provide @Context of type: " + type + ". " + e.getMessage());
			}
		}

		throw new ContextException("Can't provide @Context of type: " + type);
	}

	public static void injectContext(Object instance, RoutingContext routeContext) throws ContextException {

		if (instance == null) {
			return;
		}

		List<Field> contextFields = getContextFields(instance.getClass());

		for (Field field : contextFields) {
			Annotation found = field.getAnnotation(Context.class);
			if (found != null) {

				Object context = provideContext(field.getType(), null, routeContext);
				try {
					field.setAccessible(true);
					field.set(instance, context);
				}
				catch (IllegalAccessException e) {
					throw new ContextException("Can't provide @Context for: " + field.getType() + " - " + e.getMessage());
				}
			}
		}
	}

	public static String getContextDataKey(Object object) {

		Assert.notNull(object, "Expected object but got null!");
		if (object instanceof Class) {
			return CONTEXT_DATA_KEY_PREFIX + ((Class) object).getName();
		}

		return CONTEXT_DATA_KEY_PREFIX + object.getClass().getName();
	}
}