package com.hammurapi.extract.scripting;

import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptException;

import com.hammurapi.extract.Extractor;
import com.hammurapi.extract.ExtractorBase;
import com.hammurapi.extract.ExtractorException;
import com.hammurapi.extract.ExtractorFactory;

class ScriptingExtractor<T, V, C> extends ExtractorBase<T, V, C> {

	private boolean doArgs = true;
	private Set<Integer> parameterIndices = new TreeSet<Integer>();
	private String[] parameterNames;
	private ScriptEngineFactory sef;
	private String code;
	
	public ScriptingExtractor(String code, ScriptEngineFactory sef, String[] parameterNames) {
		super(0, null); // TODO cost from the factory?
		for (String pn: parameterNames) {
			if (pn!=null) {
				doArgs = false;
			}
		}
		
		for (int i=0; i<parameterNames.length; ++i) {
			if (doArgs || parameterNames[i]!=null) {
				parameterIndices.add(i);
			}
		}
		this.parameterNames = parameterNames;
		this.sef = sef;
		this.code = code;
	}
			
	public boolean isContextDependent() {
		return true;
	}

	public Set<Integer> parameterIndices() {
		return parameterIndices;
	}
	
	ThreadLocal<ScriptEngine> setl = new ThreadLocal<ScriptEngine>() {
		
		@Override
		protected ScriptEngine initialValue() {
			return sef.getScriptEngine();
		}
	};

	@SuppressWarnings("unchecked")
	@Override
	protected V extractInternal(C context, Map<C, Map<Extractor<T, ? super V, C>, ? super V>> cache, T... obj) {				
		ScriptEngine engine = setl.get(); // Engine is requested once per thread. sef.getScriptEngine();
		if (doArgs) {
			engine.put(ExtractorFactory.ARGS_BINDING, obj);
		} else {
			for (int i=0, l=Math.min(parameterNames.length, obj.length); i<l; ++i) {
				if (parameterNames[i]!=null) {
					engine.put(parameterNames[i], obj[i]);
				}
			}
		}
		engine.put(ExtractorFactory.CONTEXT_BINDING, context);
		try {
			return (V) engine.eval(code);
		} catch (ScriptException e) {
			throw new ExtractorException("Error evaluating '"+code+"'", e);
		}
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((code == null) ? 0 : code.hashCode());
		result = prime * result + (doArgs ? 1231 : 1237);
		result = prime
				* result
				+ ((parameterIndices == null) ? 0 : parameterIndices.hashCode());
		result = prime * result + Arrays.hashCode(parameterNames);
		result = prime * result + ((sef == null) ? 0 : sef.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		
		if (obj == null)
			return false;
		
		if (getClass() != obj.getClass())
			return false;
		
		ScriptingExtractor other = (ScriptingExtractor) obj;
		
		if (code == null) {
			if (other.code != null)
				return false;
		} else if (!code.equals(other.code))
			return false;
		
		if (doArgs != other.doArgs)
			return false;
		
		if (parameterIndices == null) {
			if (other.parameterIndices != null)
				return false;
		} else if (!parameterIndices.equals(other.parameterIndices))
			return false;
		
		if (!Arrays.equals(parameterNames, other.parameterNames))
			return false;
		
		if (sef == null) {
			if (other.sef != null)
				return false;
		} else if (!sef.equals(other.sef)) {
			return sef.getClass().equals(other.sef.getClass()) 
			&& equal(sef.getEngineName(), other.sef.getEngineName()) 
			&& equal(sef.getEngineVersion(), other.sef.getEngineVersion());
		}
		return true;
	}

	private boolean equal(String s1, String s2) {
		return (s1==null && s2==null) || (s1!=null && s2!=null && s1.equals(s2));
	}
	
	@Override
	public String toString() {
		return getClass().getName()+"("+sef.getEngineName()+" v. "+sef.getEngineVersion()+", code '"+code+"', parameter indices "+parameterIndices+")";
	}
}
