// EXTENSION
// ISSUE: DVCORE-1078
// FIXED BY: Roman Pedchenko
// DATE: 2014.03.26
// ADOPTED FROM: https://issues.jboss.org/browse/TEIID-2141
/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 * Modified by
 * @author  Data Virtuality GmbH
 * @date    $Date$
 *  
 */

package org.teiid.query.eval;

import java.beans.BeanInfo;
import java.beans.IndexedPropertyDescriptor;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.MethodDescriptor;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Pattern;

import javax.script.AbstractScriptEngine;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptException;
import javax.script.SimpleBindings;

import org.teiid.core.util.LRUCache;
import org.teiid.core.util.ObjectConverterUtil;
import org.teiid.query.QueryPlugin;

/**
 * A simplistic script engine that supports root variable access and 0-ary methods on the subsequent objects.
 */
public final class TeiidScriptEngine extends AbstractScriptEngine implements Compilable {
	private static Map<ClassLoader, Map<Class<?>, Map<String, Method>>> properties = new WeakHashMap<ClassLoader, Map<Class<?>, Map<String, Method>>>(100);
	private static Pattern splitter = Pattern.compile("\\."); //$NON-NLS-1$
	
	@Override
	public Bindings createBindings() {
		return new SimpleBindings();
	}

	@Override
	public CompiledScript compile(String script) throws ScriptException {
		final String[] parts = splitter.split(script);
		for (int i = 1; i < parts.length; i++) {
			String string = parts[i];
			for (int j = 0; j < string.length(); j++) {
				if (!Character.isJavaIdentifierPart(string.charAt(j))) {
					throw new ScriptException(QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30431,string, string.charAt(j)));
				}
			}
		}
		return new CompiledScript() {
			
			@Override
			public ScriptEngine getEngine() {
				return TeiidScriptEngine.this;
			}
			
			@Override
			public Object eval(ScriptContext sc) throws ScriptException {
				if (sc == null) {
					throw new NullPointerException();
				}
				Object obj = null;
				if (parts.length > 0) {
					obj = sc.getAttribute(parts[0]);
				}
				if (obj == null) {
					return null;
				}
				for (int i = 1; i < parts.length; i++) {
					String part = parts[i];
					Map<String, Method> methodMap = getMethodMap(obj.getClass());
					Method m = methodMap.get(part);
					if (m == null) {
						throw new ScriptException(QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31111, part, obj.getClass()));
					}
					try {
						obj = m.invoke(obj);
					} catch (IllegalAccessException e) {
						throw new ScriptException(e);
					} catch (InvocationTargetException e) {
						if (e.getCause() instanceof Exception) {
							throw new ScriptException((Exception) e.getCause());
						}
						throw new ScriptException(e);
					}
				}
				return obj;
			}
		};
	}
	
	private Map<String, Method> getMethodMap(Class<?> clazz) throws ScriptException {
		Map<Class<?>, Map<String, Method>> clazzMaps = null;
		synchronized (properties) {
			clazzMaps = properties.get(clazz.getClassLoader());
			if (clazzMaps == null) {
				clazzMaps = Collections.synchronizedMap(new LRUCache<Class<?>, Map<String, Method>>(100));
				properties.put(clazz.getClassLoader(), clazzMaps);
			}
		}
		Map<String, Method> methodMap = clazzMaps.get(clazz);
		if (methodMap == null) {
			try {
				BeanInfo info = Introspector.getBeanInfo(clazz);
				PropertyDescriptor[] pds = info.getPropertyDescriptors();
				methodMap = new HashMap<String, Method>();
				if (pds != null) {
					for (int j = 0; j < pds.length; j++) {
						PropertyDescriptor pd = pds[j];
						if (pd.getReadMethod() == null || pd instanceof IndexedPropertyDescriptor) {
							continue;
						}
						String name = pd.getName();
						Method m = pd.getReadMethod();
						methodMap.put(name, m);
					}
				}
				MethodDescriptor[] mds = info.getMethodDescriptors();
				if (pds != null) {
					for (int j = 0; j < mds.length; j++) {
						MethodDescriptor md = mds[j];
						if (md.getMethod() == null || md.getMethod().getParameterTypes().length > 0 || md.getMethod().getReturnType() == Void.class) {
							continue;
						}
						String name = md.getName();
						Method m = md.getMethod();
						methodMap.put(name, m);
					}
				}
				clazzMaps.put(clazz, methodMap);
			} catch (IntrospectionException e) {
				throw new ScriptException(e);
			}
		}
		return methodMap;
	}

	@Override
	public CompiledScript compile(Reader script) throws ScriptException {
		try {
			return compile(ObjectConverterUtil.convertToString(script));
		} catch (IOException e) {
			throw new ScriptException(e);
		}
	}
	
	@Override
	public ScriptEngineFactory getFactory() {
		throw new UnsupportedOperationException();
	}

	public Object eval(Reader reader, ScriptContext sc)
			throws ScriptException {
		throw new UnsupportedOperationException();
	}

	@Override
	public Object eval(String script, ScriptContext sc)
			throws ScriptException {
		throw new UnsupportedOperationException();
	}

}
// END EXTENSION