SaacEnv.java

package eu.javaexperience.saac;

import java.lang.reflect.Array;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import eu.javaexperience.collection.enumerations.EnumTools;
import eu.javaexperience.datareprez.DataArray;
import eu.javaexperience.datareprez.DataCommon;
import eu.javaexperience.datareprez.DataObject;
import eu.javaexperience.datareprez.convertFrom.DataLike;
import eu.javaexperience.exceptions.UnimplementedCaseException;
import eu.javaexperience.functional.saac.Functions.Param;
import eu.javaexperience.functional.saac.Functions.PreparedFunction;
import eu.javaexperience.interfaces.simple.SimpleGet;
import eu.javaexperience.interfaces.simple.getBy.GetBy1;
import eu.javaexperience.interfaces.simple.getBy.GetBy2;
import eu.javaexperience.interfaces.simple.publish.SimplePublish1;
import eu.javaexperience.reflect.CastTo;
import eu.javaexperience.reflect.Mirror;
import eu.javaexperience.reflect.PrimitiveTools;
import eu.javaexperience.saac.exceptions.SaacFunctionCreationException;
import eu.javaexperience.text.StringTools;

public class SaacEnv
{
	protected Map<String, PreparedFunction> functionSet;
	protected Class<?> rootAcceptType;
	
	
	public SaacEnv(Map<String, PreparedFunction> functionSet, Class<?> accept)
	{
		this.functionSet = functionSet;
		this.rootAcceptType = accept;
	}

	protected Map<Object, Object[]> path = new IdentityHashMap<>();
	
	public static interface SaacClosureInfo
	{
		public Object[] getArgs();
	}
	
	public static interface EnvAdapter
	{
		public Map<String, Object> subEnv(Map<String, Object> env);
	}
	
	public static interface SaacGetByWrapper<R, A> extends SaacClosureInfo, GetBy1<R, A>{}
	public static interface SaacSimplePublishWrapper<R> extends SaacClosureInfo, SimplePublish1<R>{}

	
	protected Object root;
	
	//TODO encapsule
	protected static ThreadLocal<Map<String, PreparedFunction>> THREAD_CONTEXT = new ThreadLocal<>();
	
	protected static class ContextFunctionSet
	{
		protected Map<String, PreparedFunction> functionSet;
		protected boolean master = false;
		
		public ContextFunctionSet(Map<String, PreparedFunction> functionSet, boolean master)
		{
			this.functionSet = functionSet;
			this.master = false;
		}
		
	}
	
	/**
	 * Getting/setting the thread function set
	 * @param functionSet2 
	 * */
	protected static ContextFunctionSet getOrAccumulateProperFunctionSet(Map<String, PreparedFunction> functionSet)
	{
		//trying to get a previous setted function set
		if(null == functionSet)
		{
			Map<String, PreparedFunction> ret = THREAD_CONTEXT.get();
			if(null == ret)
			{
				throw new RuntimeException("There's no function set available for this context.");
			}
			else
			{
				return new ContextFunctionSet(ret, false);
			}
		}
		else
		{
			//trying to accumulate
			
			if(null == THREAD_CONTEXT.get())
			{
				THREAD_CONTEXT.set(functionSet);
				
				//this will be cleaned up if the root parser returned.
				return new ContextFunctionSet(functionSet, true);
			}
			else
			{
				//returning the original one.
				return new ContextFunctionSet(functionSet, false);
			}
		}
	}
	
	protected static void cleanupFunctionSet(ContextFunctionSet funcs)
	{
		if(null != funcs)
		{
			if(funcs.master)
			{
				THREAD_CONTEXT.set(null);
			}
		}
	}
	
	public static SaacEnv create
	(
		Map<String, PreparedFunction> functionSet,
		DataObject obj,
		Class<?> accept
	)
	{
		SaacEnv env = new SaacEnv(functionSet, accept);
		env.parse(obj);
		return env;
	}
	
	public void parse
	(
		DataObject obj
	)
	{
		ContextFunctionSet fset = getOrAccumulateProperFunctionSet(functionSet);
		functionSet = fset.functionSet;
		try
		{
			root = parse(obj, rootAcceptType);
		}
		finally
		{
			cleanupFunctionSet(fset);
		}
	}

	protected Object parse(DataObject obj, Type acceptType)
	{
		Class accept = Mirror.extracClass(acceptType);
		//{"id":"","content":"","parent":null,"args":[]}
		{
			String id = extractString(obj, SaacTools.SAAC_FIELD_ID);
			if(null != id)
			{
				PreparedFunction pp = functionSet.get(id);
				if(null == pp)
				{
					SaacFunctionCreationException ex = new SaacFunctionCreationException("Function doesn't exists: "+id);
					ex.functionName = id;
					throw ex;
				}
				
				DataArray args = obj.getArray("args");
				Object[] call = new Object[pp.getArgs().length];
				Param[] ps = pp.getArgs();
				
				boolean[] paramWraps = new boolean[pp.getArgs().length];
				boolean wrapRet = false;
				
				Class retClass = Mirror.extracClass(pp.getReturning().getType());
				
				if(null != accept && null != retClass)
				{
					//if method "returning" void we don't have to cast to SimplePublish
					
					
					//if method returns anything, we need to cast with GetBy1
					//even if it's accetable, because only in that case will be wrapped with GetBy1
					
					//from the other hand this function might be wrapped if any of it's argument evaluated in runtime.
					wrapRet = !Mirror.isVoid(retClass) && !SimplePublish1.class.isAssignableFrom(accept) 
							//!accept.isAssignableFrom(retClass)
					;
				}
				
				List<Object> varargs = null;
				if(ps.length > 0 && Mirror.extracClass(ps[ps.length-1].getType()).isArray())
				{
					varargs = new ArrayList<>();
				}
				
				for(int i=0;i<args.size();++i)
				{
					DataLike dc = (DataLike) args.get(i);
					
					int acc = i;
					
					boolean inVaridaicRange = false;
					
					//is there a better way to check variadic?
					if(i >= ps.length-1 && null != varargs)
					{
						inVaridaicRange = true;
						acc = ps.length-1;
					}
					
					Class reqType = Mirror.extracClass(ps[acc].getType());
					
					Class varType = reqType;
					if(inVaridaicRange)
					{
						varType = reqType.getComponentType();
					}
					
					if(reqType.isArray() && dc instanceof DataObject)
					{
						DataObject d = (DataObject) dc;
						if(!isUseful(d.opt(SaacTools.SAAC_FIELD_ID)) && !isUseful(d.opt(SaacTools.SAAC_FIELD_CONTENT)))
						{
							dc = d.getArray("args");
						}
					}
					
					Object add = null; 
					switch (dc.getDataReprezType())
					{
						case ARRAY:
							add = parseArray
							(
								functionSet,
								(DataArray) dc,
								reqType.getComponentType()
							);
							break;
						
						case OBJECT:
						case CLASS_OBJECT:
						case RESOURCE:
							
							add = create
							(
								functionSet,
								(DataObject) dc,
								varType
							).root;
							
							break;
							
						case NULL:
						case PRIMITIVE:
						default:
							throw new UnimplementedCaseException(dc.getDataReprezType());
					}
					
					add = postWrapFilter(varType, add);
					
					if(null != varargs && i >= ps.length-1)
					{
						varargs.add(add);
					}
					else
					{
						call[i] = add;
					}
				}
				
				if(null != varargs)
				{
					if
					(
						varargs.size() == 1 &&
						null != varargs.get(0) &&
						Mirror.extracClass(ps[ps.length-1].getType()).isAssignableFrom(varargs.get(0).getClass())
					)
					{
						
						call[ps.length-1] = varargs.get(0);
					}
					else
					{
						Class<?> vr = Mirror.extracClass(ps[ps.length-1].getType());
						Object[] as = tryExtractAsRequested(vr, varargs);
						
						call[ps.length-1] = postWrapFilter(vr, as);
					}
				}
				
				boolean needWrap = 
						false;
						//wrapRet;
				
				if(!needWrap)
				{
					for(int i=0;i<paramWraps.length;++i)
					{
						needWrap |= paramWraps[i];
						if(needWrap)
						{
							break;
						}
					}
				}
				
				//accept is null only int the case of the root element
				if(needWrap && null != accept && !Mirror.isVoid(accept))
				{
					//wrapping the return value if we have to wrap any of input argument and caller requires return value
					wrapRet = true;
				}
				
				
				if(needWrap)
				{
					return wrapForRuntime(pp, wrapRet, paramWraps, call);
				}
				else
				{
					return pp.create(call);
				}
			}
		}
		
		if(null != accept)
		{
			final String content = extractString(obj, SaacTools.SAAC_FIELD_CONTENT);
			if(null != content)
			{
				if(accept.isEnum())
				{
					return EnumTools.getByName((Class) accept, content);
				}
	
				CastTo to = CastTo.getCasterRestrictlyForTargetClass(accept);
				if(null != to)
				{
					return to.cast(content);
				}
				
				if(content.startsWith("$"))
				{
					return new SaacGetByWrapper<Object, Map<String,Object>>()
					{
						@Override
						public Object getBy(Map<String, Object> a)
						{
							return a.get(content);
						}
						
						@Override
						public String toString()
						{
							return "Scope getter: "+super.toString();
						}
	
						@Override
						public Object[] getArgs()
						{
							return new Object[]{content};
						}
					};
				}
				else
				{
					return new SaacGetByWrapper<Object, Object>()
					{
						@Override
						public Object getBy(Object a)
						{
							return content;
						}
						
						@Override
						public String toString()
						{
							return "Scope getter: "+super.toString();
						}
	
						@Override
						public Object[] getArgs()
						{
							return new Object[]{content};
						}
					};
				}
			}
		}
		
		return null;
	}
	
	public static Object[] tryExtractAsRequested(Class req, List obj)
	{
		try
		{
			return obj.toArray((Object[]) Array.newInstance(req.getComponentType(), obj.size()));
		}
		catch(Exception e)
		{
			return obj.toArray();
		}
	}
	
	public static Object postWrapFilter(Class reqType, Object add)
	{
		//comaptible?
		if(null != reqType && null != add)
		{
			if(PrimitiveTools.isPrimitiveClass(reqType))
			{
				reqType = PrimitiveTools.toObjectClassType(reqType, reqType);
			}
			
			//add direct wrap: GetBy1<T,?> (or SimplePublish<T>)required and a T given
			boolean wrapParam = !reqType.isAssignableFrom(add.getClass()) || SaacSimplePublishWrapper.class.isAssignableFrom(add.getClass());
			wrap:if(wrapParam)
			{
				Object o = tryWrapSameOfArray(reqType, add);
				if(null != o)
				{
					add = o;
					break wrap;
				}
				
				o = tryWrapConstantAsSourceFunction(reqType, add);
				if(null != o)
				{
					add = o;
					//not needed to wrap in runtime, we have been done that.
					break wrap;
				}
				
				//need to extact or evaluate in runtime, or unamingously: it will be extracted in runtime
			}
		}
		
		return add;
	}
	
	
	public static Object tryWrapSameOfArray(Class reqType, Object object)
	{
		if(reqType.isArray())
		{
			Class cls = reqType.getComponentType();
			if(cls.isAssignableFrom(object.getClass()))
			{
				Object[] ret = (Object[]) Array.newInstance(cls, 1);
				ret[0] = object;
				return ret;
			}
		}
		return null;
	}

	public static Object tryWrapConstantAsSourceFunction(Type reqType, Object value)
	{
		Class req = Mirror.extracClass(reqType);
		if(req.isAssignableFrom(SimpleGet.class))
		{
			//TODO when it's raw just let them go, if Generic bounding specified
			// check them, on mismatch throw exception
			return new SimpleGet()
			{
				@Override
				public Object get()
				{
					return value;
				}
			};
		}
		else if(req.isAssignableFrom(GetBy1.class))
		{
			return new GetBy1()
			{
				@Override
				public Object getBy(Object o)
				{
					return value;
				}
			};
		}
		else if(req.isAssignableFrom(GetBy2.class))
		{
			return new GetBy2()
			{
				@Override
				public Object getBy(Object o, Object p)
				{
					return value;
				}
			};
		}
		//getBy3 so on
		
		return null;
		
	}
	
	protected static boolean isUseful(Object o)
	{
		return null != o && !StringTools.isNullOrTrimEmpty(o.toString());
	}
	
	protected static <T> Object processArray
	(
		PreparedFunction pp,
		Param p,
		Object[] at,
		Map<String, Object> env
	)
	{
		Class<T> rt = (Class<T>) Mirror.extracClass(p.getType()).getComponentType();
		ArrayList<Object> ret = new ArrayList<>();
		boolean allFits = true;
		
		for(int i=0;i<at.length;++i)
		{
			Object add = processSingleParam(pp, p, at[i], env);
			ret.add(add);
			if(null != add)
			{
				allFits &= rt.isAssignableFrom(add.getClass());
			}
		}
		
		
		if(allFits)
		{
			return ret.toArray((T[]) Array.newInstance(rt, 0));
		}
		
		return ret.toArray();
		
	}
	
	protected static Object processSingleParam
	(
		PreparedFunction pp,
		Param p,
		Object at,
		Map<String, Object> env
	)
	{
		if(null == at)
		{
			return null;
		}
		
		if(at instanceof EnvAdapter)
		{
			env = ((EnvAdapter) at).subEnv(env);
		}
		
		Class cls = Mirror.extracClass(p.getType());
		if(cls.isAssignableFrom(at.getClass()))
		{
			return at;
		}
		
		if(at instanceof SimpleGet)
		{
			at = ((SimpleGet) at).get();
		}
		else if(at instanceof	//SaacGetByWrapper)
								GetBy1)
		{
			at = ((GetBy1) at).getBy(env);
		}
		else if(at instanceof	//SaacSimplePublishWrapper)
								SimplePublish1)
		{
			((SimplePublish1) at).publish(env);
			at = null;
		}
		
		if(null == at)
		{
			return null;
		}
		
		if(at instanceof String)
		{
			String str = (String) at;
			
			if(cls.isEnum())
			{
				return EnumTools.getByName(cls, str);
			}
			else
			{
				CastTo ct = CastTo.getCasterRestrictlyForTargetClass(cls);
				if(null != ct)
				{
					return ct.cast(str);
				}
				else
				{
					return env.get(str);
				}
			}
		}
		//arrays
		else if(at.getClass().isArray())
		{
			return processArray(pp, p, (Object[]) at, env);
		}
		else
		{
			return at;
		}
	}

	protected static String extractString(DataObject obj, String key)
	{
		if(obj.has(key))
		{
			//can be a number or boolean
			Object o = obj.get(key);
			
			if(null == o)
			{
				return null;
			}
			
			String ret = null;
			
			if(o instanceof DataCommon)
			{
				byte[] b = ((DataCommon)o).toBlob();
				if(null != b)
				{
					ret = new String(b);
				}
			}
			else
			{
				ret = o.toString();
			}
			
			if(!StringTools.isNullOrTrimEmpty(ret))
			{
				return ret;
			}
		}
		return null;
	}

	
	protected static Object processArgs
	(
		final PreparedFunction pp,
		boolean wrapRet,
		boolean[] paramWraps,
		final Object[] args,
		final Map<String,Object> env
	)
	{
		Object[] cre = new Object[args.length];
		
		for(int i=0;i<args.length;++i)
		{
			if(!paramWraps[i])
			{
				cre[i] = args[i];
				continue;
			}
			
			cre[i] = processSingleParam(pp, pp.getArgs()[i], args[i], env);
		}
		
		try
		{
			return pp.create(cre);
		}
		catch(Exception e)
		{
			SaacFunctionCreationException t = new SaacFunctionCreationException("Can't create function: "+pp.getName(), e);
			t.functionName = pp.getName();
			t.arguments = cre;
			t.function = pp;
			throw t;
		}
	}
	
	public static <T> Object wrapForRuntime
	(
		final PreparedFunction pp,
		boolean wrapRet,
		boolean[] paramWraps,
		final Object[] args
	)
	{
		if(wrapRet)
		{
			return new SaacGetByWrapper<Object, Map<String, Object>>()
			{
				@Override
				public Object getBy(Map<String, Object> env)
				{
					return processArgs(pp, wrapRet, paramWraps, args, env);
				}
				
				@Override
				public String toString()
				{
					return "Runtime Scope getter (GetBy1): "+super.toString();
				}

				@Override
				public Object[] getArgs()
				{
					return new Object[]{pp, wrapRet, paramWraps, args};
				}
			};
		}
		else
		{
			return new SaacSimplePublishWrapper<Map<String,Object>>()
			{
				@Override
				public void publish(Map<String, Object> env)
				{
					processArgs(pp, wrapRet, paramWraps, args, env);
				}
				
				@Override
				public String toString()
				{
					return "Runtime Wrap (SimplePublish): "+super.toString();
				}
				
				@Override
				public Object[] getArgs()
				{
					return new Object[]{pp, wrapRet, paramWraps, args};
				}
			};
		}
	}
	
	protected static <T> Object parseArray
	(
		Map<String, PreparedFunction> functionSet,
		DataArray arr,
		Class<?> accept
	)
	{
		int size = arr.size();
		ArrayList<Object> coll = new ArrayList<>();
		
		boolean allFits = true;
		
		for(int i=0;i<size;++i)
		{
			Object add = create(functionSet, arr.getObject(i), accept).root;
			if(null != add)
			{
				allFits &= accept.isAssignableFrom(add.getClass());
			}
			coll.add(add);
		}
		
		if(allFits)
		{
			return coll.toArray((T[]) Array.newInstance(accept, 0));
		}
		
		return coll.toArray();
	}

	//TODO encapsule
	protected static ThreadLocal<Stack<Map<String, Object>>> ENV = new ThreadLocal<Stack<Map<String, Object>>>()
	{
		protected java.util.Stack<java.util.Map<String,Object>> initialValue()
		{
			return new Stack<>();
		};
	};
	
	public static synchronized Map<String, Object> getCurrentEnv()
	{
		Stack<Map<String, Object>> stack = ENV.get();
		return stack.peek();
	}
	
	public static synchronized void pushEnv(Map<String, Object> env)
	{
		Stack<Map<String, Object>> stack = ENV.get();
		stack.push(env);
	}

	public static synchronized Map<String, Object> popEnv(Map<String, Object> env)
	{
		Stack<Map<String, Object>> stack = ENV.get();
		return stack.pop();
	}

	public Object getRoot()
	{
		return root;
	}
}