package net.anotheria.moskito.webui.util;

import net.anotheria.anoplass.api.API;
import net.anotheria.anoplass.api.APIFinder;
import net.anotheria.moskito.webui.MoSKitoWebUIContext;
import net.anotheria.moskito.webui.accumulators.api.AccumulatorAPI;
import net.anotheria.moskito.webui.auth.api.AuthApi;
import net.anotheria.moskito.webui.dashboards.api.DashboardAPI;
import net.anotheria.moskito.webui.gauges.api.GaugeAPI;
import net.anotheria.moskito.webui.journey.api.JourneyAPI;
import net.anotheria.moskito.webui.loadfactors.api.LoadFactorsAPI;
import net.anotheria.moskito.webui.nowrunning.api.NowRunningAPI;
import net.anotheria.moskito.webui.producers.api.ProducerAPI;
import net.anotheria.moskito.webui.shared.api.AdditionalFunctionalityAPI;
import net.anotheria.moskito.webui.tags.api.TagAPI;
import net.anotheria.moskito.webui.threads.api.ThreadAPI;
import net.anotheria.moskito.webui.threshold.api.ThresholdAPI;
import net.anotheria.moskito.webui.tracers.api.TracerAPI;
import org.distributeme.core.ServiceDescriptor;

import javax.servlet.http.HttpSession;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * This utility is used to separate between local apis and remote apis and provide remote instances.
 *
 * @author lrosenberg
 * @since 21.03.14 17:41
 */
public class APILookupUtility {

	/**
	 * Currently setup remote instance.
	 */
	private static RemoteInstance currentRemoteInstance;

	/**
	 * A map with all remote instance.
	 */
	private static ConcurrentMap<RemoteInstance, ConcurrentMap<Class<? extends API>, API>> remotes = new ConcurrentHashMap<>(1);

	/**
	 * Currently configured ConnectivityMode (local or remote).
	 */
	private static ConnectivityMode currentConnectivityMode = WebUIConfig.getInstance().getConnectivityMode();

	public static boolean isLocal(){
		//if current usage mode is personal, every change made to the connectivity mode is global.
		if (WebUIConfig.getInstance().getUsageMode() == UsageMode.PERSONAL)
			return currentConnectivityMode == ConnectivityMode.LOCAL;
		HttpSession session = MoSKitoWebUIContext.getCallContext().getCurrentSession();
		//if session is null, which can't happen (hope so), we fall back to personal behaviour silently.
		if (session==null )
			return currentConnectivityMode == ConnectivityMode.LOCAL;
		ConnectivityMode mode = null;
		try{
			mode = (ConnectivityMode)session.getAttribute(ConnectivityMode.class.getName());
		}catch(IllegalStateException sessionAlreadyInvalidated){
			//ignore
		}
		return mode == null ?  true : mode == ConnectivityMode.LOCAL ;

	}

	public static RemoteInstance getCurrentRemoteInstance(){

		if (WebUIConfig.getInstance().getUsageMode()==UsageMode.PERSONAL)
			return getCurrentRemoteInstanceForPersonalUsage();
		return getCurrentRemoteInstanceForSharedUsage();
	}

	private static RemoteInstance getCurrentRemoteInstanceForPersonalUsage(){
		if (currentRemoteInstance !=null)
			return currentRemoteInstance;
		RemoteInstance[] instances = WebUIConfig.getInstance().getRemotes();
		if (instances==null || instances.length<1)
			throw new IllegalStateException("Can't select first remote instance, but obviously remote usage is configured");
		currentRemoteInstance = instances[0];
		return currentRemoteInstance;

	}

	private static RemoteInstance getCurrentRemoteInstanceForSharedUsage(){
		RemoteInstance mySelectedInstance = (RemoteInstance)MoSKitoWebUIContext.getCallContext().getCurrentSession().getAttribute(RemoteInstance.class.getName());
		if (mySelectedInstance !=null)
			return mySelectedInstance;
		RemoteInstance[] instances = WebUIConfig.getInstance().getRemotes();
		if (instances==null || instances.length<1)
			throw new IllegalStateException("Can't select first remote instance, but obviously remote usage is configured");
		mySelectedInstance = instances[0];
		MoSKitoWebUIContext.getCallContext().getCurrentSession().setAttribute(RemoteInstance.class.getName(), mySelectedInstance);
		return mySelectedInstance;

	}

	public static JourneyAPI getJourneyAPI(){
		return isLocal() ?
				APIFinder.findAPI(JourneyAPI.class) :
				findRemote(JourneyAPI.class);
	}

	public static ThresholdAPI getThresholdAPI(){
		return isLocal() ?
			APIFinder.findAPI(ThresholdAPI.class) :
			findRemote(ThresholdAPI.class);
	}

	public static AuthApi getAuthApi(){
		return APIFinder.findAPI(AuthApi.class);
	}

	public static AccumulatorAPI getAccumulatorAPI() {
		return isLocal() ?
				APIFinder.findAPI(AccumulatorAPI.class) :
				findRemote(AccumulatorAPI.class);
	}

	public static ThreadAPI getThreadAPI() {
		return isLocal() ?
				APIFinder.findAPI(ThreadAPI.class) :
				findRemote(ThreadAPI.class);
	}

	public static ProducerAPI getProducerAPI() {
		return isLocal() ?
				APIFinder.findAPI(ProducerAPI.class) :
				findRemote(ProducerAPI.class);
	}

	public static AdditionalFunctionalityAPI getAdditionalFunctionalityAPI() {
		return isLocal() ?
				APIFinder.findAPI(AdditionalFunctionalityAPI.class) :
				findRemote(AdditionalFunctionalityAPI.class);
	}

	public static GaugeAPI getGaugeAPI() {
		return isLocal() ?
				APIFinder.findAPI(GaugeAPI.class) :
				findRemote(GaugeAPI.class);
	}

	public static DashboardAPI getDashboardAPI() {
		return isLocal() ?
				APIFinder.findAPI(DashboardAPI.class) :
				findRemote(DashboardAPI.class);
	}

	public static TracerAPI getTracerAPI() {
		return isLocal() ?
				APIFinder.findAPI(TracerAPI.class) :
				findRemote(TracerAPI.class);
	}

	public static TagAPI getTagAPI(){
		return isLocal() ?
				APIFinder.findAPI(TagAPI.class) :
				findRemote(TagAPI.class);
	}

	public static LoadFactorsAPI getLoadFactorsAPI() {
		return isLocal() ?
				APIFinder.findAPI(LoadFactorsAPI.class) :
				findRemote(LoadFactorsAPI.class);
	}

	public static NowRunningAPI getNowRunningAPI() {
		return isLocal() ?
				APIFinder.findAPI(NowRunningAPI.class) :
				findRemote(NowRunningAPI.class);
	}

	public static void resetConnection(){
		setCurrentConnectivityMode(ConnectivityMode.LOCAL);
	}

	private static <T extends API> T findRemote(Class<T> targetClass){
		String serviceId = null;
		try{
			Class constantsClass = Class.forName(targetClass.getPackage().getName()+".generated."+targetClass.getSimpleName()+"Constants");
			Method m = constantsClass.getMethod("getServiceId");
			serviceId = (String)m.invoke(null);
		}catch(ClassNotFoundException e){
			throw new AssertionError("Can not find supporting classes for "+targetClass+ ' ' + e.getMessage());
		} catch (NoSuchMethodException e) {
			throw new AssertionError("Can not find supporting classes or methods for "+targetClass+ ' ' + e.getMessage());
		} catch (InvocationTargetException e) {
			throw new AssertionError("Can not obtain service id "+targetClass+ ' ' + e.getMessage());
		} catch (IllegalAccessException e) {
			throw new AssertionError("Can not obtain service id "+targetClass+ ' ' + e.getMessage());
		}

		Class<? extends T> remoteStubClass = null;
		try{
			remoteStubClass = (Class<? extends T> )Class.forName(targetClass.getPackage().getName()+".generated.Remote"+targetClass.getSimpleName()+"Stub");
		}catch(ClassNotFoundException e){
			throw new AssertionError("Can not find supporting classes for "+targetClass);
		}

		return findRemote(targetClass, remoteStubClass, serviceId);
	}

	private static <T extends API> T findRemote(Class<T> targetClass, Class<? extends T> remoteStubClass, String serviceId){
		RemoteInstance ri = getCurrentRemoteInstance();
		ConcurrentMap<Class<? extends API>, API> stubsByInterface = remotes.get(ri);
		if (stubsByInterface==null){
			ConcurrentHashMap<Class<? extends API>, API> newStubsByInterface = new ConcurrentHashMap<>(0);
			ConcurrentMap<Class<? extends API>, API> old = remotes.putIfAbsent(ri, newStubsByInterface);
			stubsByInterface = old == null ? newStubsByInterface : old;
		}

		T stub = (T)stubsByInterface.get(targetClass);
		if (stub!=null)
			return stub;

		//ok, we didn't have an object, we have to create it.
		ServiceDescriptor sd = new ServiceDescriptor(ServiceDescriptor.Protocol.RMI, serviceId, "any", ri.getHost(), ri.getPort());
		try{
			Constructor<? extends T> c = remoteStubClass.getConstructor(ServiceDescriptor.class);
			T newAPI = c.newInstance(sd);
			stubsByInterface.putIfAbsent(targetClass, newAPI);
			return newAPI;
		}catch (NoSuchMethodException e) {
			throw new IllegalStateException("Constructor with ServiceDescriptor parameter not found in remote stub", e);
		} catch (InvocationTargetException e) {
			resetConnection();
			throw new IllegalStateException("Cannot connect to "+ri+", due: "+e.getTargetException().getMessage()+". Server at "+ri.getHost()+", port: "+ri.getPort()+" is down or not properly configured", e);
		} catch (InstantiationException | IllegalAccessException e) {
			resetConnection();
			throw new IllegalStateException("Cannot connect to "+ri+", due: "+e.getMessage()+". Server at "+ri.getHost()+", port: "+ri.getPort()+" is down or not properly configured", e);
		}
	}

	public static final String describeConnectivity(){
		return isLocal() ? "Local" : "Remote: "+getCurrentRemoteInstance();
	}

	public static ConnectivityMode getCurrentConnectivityMode() {
		return currentConnectivityMode;
	}

	public static void setCurrentRemoteInstance(RemoteInstance currentRemoteInstance) {
		if (WebUIConfig.getInstance().getUsageMode()==UsageMode.PERSONAL) {
			APILookupUtility.currentRemoteInstance = currentRemoteInstance;
			return;
		}
		MoSKitoWebUIContext.getCallContext().getCurrentSession().setAttribute(RemoteInstance.class.getName(), currentRemoteInstance);

	}
	public static void setCurrentConnectivityMode(ConnectivityMode currentConnectivityMode) {
		if (WebUIConfig.getInstance().getUsageMode()==UsageMode.PERSONAL) {
			APILookupUtility.currentConnectivityMode = currentConnectivityMode;
			return;
		}
		MoSKitoWebUIContext.getCallContext().getCurrentSession().setAttribute(ConnectivityMode.class.getName(), currentConnectivityMode);

	}

}
