/**
 * License Agreement.
 *
 * Rich Faces - Natural Ajax for Java Server Faces (JSF)
 *
 * Copyright (C) 2007 Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * 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
 */

package org.ajax4jsf.renderkit.compiler;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.faces.FacesException;
import javax.faces.el.MethodNotFoundException;

import org.ajax4jsf.Messages;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.SAXException;


/**
 * @author asmirnov@exadel.com (latest modification by $Author: alexsmirnov $)
 * @version $Revision: 1.1.2.1 $ $Date: 2007/01/09 18:57:47 $
 *
 */
public  class MethodCallElement extends ElementBase {

	public static final String UTILS_PREFIX = "utils.";
	
	static final Log _log = LogFactory.getLog(MethodCallElement.class);

	private String _name = null;
	
	private List parameters = new ArrayList();
	
	private Invoker invoker = new Invoker();
	
	private MethodCacheState state = new MethodCacheState();
	
	
	/* (non-Javadoc)
	 * @see org.ajax4jsf.renderkit.compiler.CompiledXML#encode(javax.faces.render.Renderer, javax.faces.context.FacesContext, javax.faces.component.UIComponent)
	 */
	public void encode(TemplateContext context ) throws IOException {
			getValue(context);
	}

	/* (non-Javadoc)
	 * @see org.ajax4jsf.renderkit.compiler.ElementBase#encode(org.ajax4jsf.renderkit.compiler.TemplateContext, java.lang.String)
	 */
	public void encode(TemplateContext context, String breakPoint) throws IOException {
		// Text not contain breakpoints.
		encode(context);
	}
	
	
	public Object getValue(TemplateContext context) throws FacesException {
		// prepare method params. we attempt to call 3 signatures :
		// a) name(FacesContext,UIComponent [, param0...])
		// b) name(TempalateContext [,param0...])
		// c) name([param0...])
		state.init(parameters);
		Object[] values = state.computeParameterValues(context);
		
		InvokeData data = null;
		synchronized (state) {
			state.update(context, values, invoker);
			data = invoker.invokeMethod(context, state);
		}
		return invoker.invokeMethod(data);
		// perform childrens.
		// super.encode(renderer,context,component);
	}



	public void addParameter(MethodParameterElement parameter){
		parameters.add(parameter);
	}
	/**
	 * @return Returns the methodName.
	 */
	public String getName() {
		return _name;
	}

	/**
	 * @param methodName The methodName to set.
	 */
	public void setName(String methodName) {
		if(methodName.startsWith(UTILS_PREFIX)){
			this._name = methodName.substring(UTILS_PREFIX.length());
			this.invoker = getRendererUtilsInvoker(_name);
		} else if(methodName.indexOf('.') >= 0) {
			this._name = methodName;
			this.invoker = getStaticInvoker(_name);
		} else {
			this._name = methodName;
			this.invoker = getRendererInvoker(_name);
		}
	}

	static Map staticInvokers = new HashMap();
	
	public StaticInvoker getStaticInvoker(String methodName) {
		StaticInvoker invoker = (StaticInvoker)staticInvokers.get(methodName);
		if(invoker == null) {
			invoker = new StaticInvoker(methodName);
			staticInvokers.put(methodName, invoker);
		}
		return invoker;		
	}

	static Map rendererInvokers = new HashMap();
	
	public RendererInvoker getRendererInvoker(String methodName) {
		RendererInvoker invoker = (RendererInvoker)rendererInvokers.get(methodName);
		if(invoker == null) {
			invoker = new RendererInvoker(false, methodName);
			rendererInvokers.put(methodName, invoker);
		}
		return invoker;
	}
	
	static Map utilsInvokers = new HashMap();

	public RendererInvoker getRendererUtilsInvoker(String methodName) {
		RendererInvoker invoker = (RendererInvoker)utilsInvokers.get(methodName);
		if(invoker == null) {
			invoker = new RendererInvoker(true, methodName);
			utilsInvokers.put(methodName, invoker);
		}
		return invoker;
	}
	
	public String getTag() {
		return HtmlCompiler.NS_PREFIX+HtmlCompiler.CALL_TAG;
	}

	
	/* (non-Javadoc)
	 * @see org.ajax4jsf.renderkit.compiler.ElementBase#setParent(org.ajax4jsf.renderkit.compiler.PreparedTemplate)
	 */
	public void setParent(PreparedTemplate parent) throws SAXException {		
		super.setParent(parent);
		if (getName()==null) {
			throw new SAXException(Messages.getMessage(Messages.NO_NAME_ATTRIBUTE_ERROR, getTag()));
		}
	}

	
/* (non-Javadoc)
	 * @see org.ajax4jsf.renderkit.compiler.ElementBase#getString(org.ajax4jsf.renderkit.compiler.TemplateContext)
	 */
	public String getString(TemplateContext context) throws FacesException {
		Object result = getValue(context);
		if (null == result || result.toString().length()==0) {
				result = "";
		}
		return result.toString();
	}

	/* (non-Javadoc)
	 * @see org.ajax4jsf.renderkit.compiler.ElementBase#getAllowedClasses()
	 */
	protected Class[] getAllowedClasses() {
		// TODO Auto-generated method stub
		return new Class[]{
				MethodParameterElement.class,
				ResourceElement.class
		};
	}

}

class InvokeData {
	TemplateContext context;
	Method method;
	Object object;
	Object[] arguments;
	InvokeData(TemplateContext context, Method method, Object object, Object[] arguments) {
		this.context = context;
		this.method = method;
		this.object = object;
		this.arguments = (Object[]) arguments.clone();
	}
}

class Invoker {
	String methodName;

	InvokeData invokeMethod(TemplateContext context, MethodCacheState state) {
		throw new FacesException(Messages.getMessage(Messages.RENDERER_METHOD_NOT_SET_ERROR));
	}

	void handleInvocationTargetException(TemplateContext context, InvocationTargetException e) {}
	void handleIllegalAccessException(TemplateContext context, IllegalAccessException e) {}
	void handleMethodNotFoundException(TemplateContext context) throws MethodNotFoundException {}

	InvokeData invokeMethod(TemplateContext context, Map methods, Class cls, Object object, MethodCacheState state) {
		Method method = provideMethod(methods, cls, object, state);
		return new InvokeData(context, method, object, state.current.arguments);
	}
	
	Object invokeMethod(InvokeData data) {
		if(data.method != null) {
			try {
				return data.method.invoke(data.object, data.arguments);
			} catch (InvocationTargetException e) {
				handleInvocationTargetException(data.context, e);
			} catch (IllegalAccessException e) {
				handleIllegalAccessException(data.context, e);
			}
		}
		handleMethodNotFoundException(data.context);
		return null;
	}
	
	public Class getInvokedClass(TemplateContext context) {
		return null;
	}
	
	private Method provideMethod(Map methods, Class cls, Object object, MethodCacheState state) {
		if(state.method != null) return state.method;

		if(methods.size() > 0) {
			for (int i = 0; i < state.signatures.length; i++) {
				state.method = (Method)methods.get(getClassesKey(state.signatures[i].arguments));
				if(state.method != null) {
					state.current = state.signatures[i];
					return state.method;
				}
			}
		}
		
		if(cls == null && object != null) cls = object.getClass();
		Method[] ms = cls.getMethods();
		for (int m = 0; m < ms.length; m++) {
			if(!ms[m].getName().equals(methodName)) continue;
			if(object == null && !Modifier.isStatic(ms[m].getModifiers())) continue;
			Class[] cs = ms[m].getParameterTypes();
			Signature s = getMatchingArguments(cs, state.signatures);
			if(s == null) continue;
			state.current = s;
			state.method = ms[m];
			methods.put(getClassesKey(s.arguments), ms[m]);
			return state.method;
		}

		return null;
	}

	private String getClassesKey(Object[] args) {
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < args.length; i++) {
			String dk = args[i] == null ? "null" : args[i].getClass().getName();
			sb.append(";").append(dk);
		}
		return sb.toString();
	}
	
	private Signature getMatchingArguments(Class[] cs, Signature[] sgs) {
		for (int i = 0; i < sgs.length; i++) {
			if(isMatching(cs, sgs[i].arguments)) return sgs[i];
		}
		return null;
	}

	static boolean isMatching(Class[] cs, Object[] args) {
		if(cs.length != args.length) return false;
		for (int i = 0; i < cs.length; i++) {
			if(args[i] != null && !cs[i].isAssignableFrom(args[i].getClass())) return false;
		}
		return true;
	}
}

class RendererInvoker extends Invoker {
	boolean utils = false;
	Map renderers = new HashMap();
	
	public RendererInvoker(boolean utils, String methodName) {
		this.utils = utils;
		this.methodName = methodName;
	}

	public Class getInvokedClass(TemplateContext context) {
		Object object = getInvokedObject(context);
		return (object != null) ? object.getClass() : null;
	}
	
	private Object getInvokedObject(TemplateContext context) {
		if(utils) {
			return context.getRenderer().getUtils();
		} else {
			return context.getRenderer();
		}
	}
	
	InvokeData invokeMethod(TemplateContext context, MethodCacheState state) {
		Object object = getInvokedObject(context);
		Map methods = getMethods(object);
		return invokeMethod(context, methods, null, object, state);
	}
	
	private Map getMethods(Object object) {
		if(object == null) return null;
		Map methods = (Map)renderers.get(object);
		if(methods == null) {
			methods = new HashMap();
			renderers.put(object, methods);
		}
		return methods;
	}

	void handleInvocationTargetException(TemplateContext context, InvocationTargetException e) {
		String logMessage = (utils) 
			? Messages.getMessage(Messages.METHOD_CALL_ERROR_1, methodName, context.getComponent().getId())
			: Messages.getMessage(Messages.METHOD_CALL_ERROR_1a, methodName, context.getComponent().getId());
		String excMessage = (utils)
			? Messages.getMessage(Messages.METHOD_CALL_ERROR_2, new Object[]{methodName, context.getComponent().getId(), e.getCause().getMessage()})
			: Messages.getMessage(Messages.METHOD_CALL_ERROR_2a, new Object[]{methodName, context.getComponent().getId(), e.getCause().getMessage()});
		MethodCallElement._log.error(logMessage, e);
		throw new FacesException(excMessage, e);
	}
	void handleIllegalAccessException(TemplateContext context, IllegalAccessException e) {
		String logMessage = (utils) 
			? Messages.getMessage(Messages.getMessage(Messages.METHOD_CALL_ERROR_3, methodName, context.getComponent().getId()))
			: Messages.getMessage(Messages.getMessage(Messages.METHOD_CALL_ERROR_3a, methodName, context.getComponent().getId()));
		String excMessage = (utils)
			? Messages.getMessage(Messages.getMessage(Messages.METHOD_CALL_ERROR_4, new Object[]{methodName, context.getComponent().getId(), e.getCause().getMessage()}))
			: Messages.getMessage(Messages.getMessage(Messages.METHOD_CALL_ERROR_4a, new Object[]{methodName, context.getComponent().getId(), e.getCause().getMessage()}));
		MethodCallElement._log.error(logMessage, e);
		throw new FacesException(excMessage, e);
	}
	
	void handleMethodNotFoundException(TemplateContext context) throws MethodNotFoundException {
		String logMessage = (utils) 
			? Messages.getMessage(Messages.getMessage(Messages.METHOD_CALL_ERROR_5, methodName, context.getComponent().getId()))
			: Messages.getMessage(Messages.getMessage(Messages.METHOD_CALL_ERROR_5a, methodName, context.getComponent().getId()));
		String excMessage = (utils)
			? Messages.getMessage(Messages.getMessage(Messages.METHOD_CALL_ERROR_6, methodName))
			: Messages.getMessage(Messages.getMessage(Messages.METHOD_CALL_ERROR_6a, methodName));
			MethodCallElement._log.error(logMessage);
		throw new FacesException(excMessage);
	}
}

class StaticInvoker extends Invoker {
	String className;
	Class cls;
	Map methods = new HashMap();
	
	StaticInvoker(String methodName) {
		this.methodName = methodName;
		int i = methodName.lastIndexOf('.');
		className = methodName.substring(0, i);
		this.methodName = methodName.substring(i + 1);
		try {
			cls = Thread.currentThread().getContextClassLoader().loadClass(className);
		} catch (ClassNotFoundException e) {
			//ignore, throw exception when invoking
		}
	}

	InvokeData invokeMethod(TemplateContext context, MethodCacheState state) {
		if(cls == null) throw new FacesException(className, new ClassNotFoundException(className));
		return invokeMethod(context, methods, cls, null, state);
	}

	void handleInvocationTargetException(TemplateContext context, InvocationTargetException e) {
		MethodCallElement._log.error(Messages.getMessage(Messages.METHOD_CALL_ERROR_1a, methodName, context.getComponent().getId()), e);
		throw new FacesException(Messages.getMessage(Messages.METHOD_CALL_ERROR_2a, new Object[]{methodName, context.getComponent().getId(), e.getCause().getMessage()}), e);
	}
	void handleIllegalAccessException(TemplateContext context, IllegalAccessException e) {
		MethodCallElement._log.error(Messages.getMessage(Messages.METHOD_CALL_ERROR_3a, methodName, context.getComponent().getId()), e);
		throw new FacesException(Messages.getMessage(Messages.METHOD_CALL_ERROR_4a, new Object[]{methodName, context.getComponent().getId(), e.getCause().getMessage()}), e);
	}		
	void handleMethodNotFoundException(TemplateContext context) throws MethodNotFoundException {
		MethodCallElement._log.error(Messages.getMessage(Messages.METHOD_CALL_ERROR_5a, methodName, context.getComponent().getId()));
		throw new MethodNotFoundException(Messages.getMessage(Messages.METHOD_CALL_ERROR_6a, methodName));
	}
}

class MethodCacheState {
	private MethodParameterElement[] elements;
	private Class[] parameterClasses;
	private Object[] parameterValues;
	private Class lastCallingClass = null;
	public Method method;
	public Signature current;
	public Signature[] signatures;
	
	public void init(List parameters) {
		if(elements != null) return;
		synchronized (this) {
			if(elements != null) return;
			int size = parameters.size();
			parameterClasses = new Class[size];
			parameterValues = new Object[size];
			signatures = new Signature[3];
			signatures[0] = new Signature2(size);
			signatures[1] = new Signature1(size);
			signatures[2] = new Signature0(size);		
			elements = (MethodParameterElement[])parameters.toArray(new MethodParameterElement[0]);
		}
	}
	
	public Object[] computeParameterValues(TemplateContext context) {
		Object[] ps = new Object[elements.length];
		for (int i = 0; i < elements.length; i++) {
			ps[i] = elements[i].valueGetter.getValueOrDefault(context);
		}
		return ps;
	}
	
	public void update(TemplateContext context, Object[] values, Invoker invoker) {
		boolean changed = false;
		for (int i = 0; i < elements.length; i++) {
			Object parametr = values[i];
			Class c = (parametr == null) ? null : parametr.getClass();
			if(c != parameterClasses[i]) {
				parameterClasses[i] = c;
				changed = true;
			}
			parameterValues[i] = parametr;			
		}
		if(method != null && !changed && lastCallingClass != invoker.getInvokedClass(context)) {
			lastCallingClass = invoker.getInvokedClass(context);
			changed = true;
		}
		if(changed || current == null) {
			for (int i = 0; i < signatures.length; i++) {
				signatures[i].update(context, parameterValues);
			}
			method = null;
			current = null;
		} else {
			current.update(context, parameterValues);
		}
	}	
	
}

abstract class Signature {
	Object[] arguments;	

	Signature() {}

	void update(TemplateContext context, Object[] parameters) {}
}

class Signature0 extends Signature {
	Signature0(int size) {
		arguments = new Object[size];
	}

	void update(TemplateContext context, Object[] parameters) {
		System.arraycopy(parameters, 0, arguments, 0, parameters.length);
	}
	
}

class Signature1 extends Signature {
	Signature1(int size) {
		arguments = new Object[size + 1];
	}
	void update(TemplateContext context, Object[] parameters) {
		arguments[0] = context;
		System.arraycopy(parameters, 0, arguments, 1, parameters.length);
	}
	
}

class Signature2 extends Signature {
	Signature2(int size) {
		arguments = new Object[size + 2];
	}
	void update(TemplateContext context, Object[] parameters) {
		arguments[0] = context.getFacesContext();
		arguments[1] = context.getComponent();
		System.arraycopy(parameters, 0, arguments, 2, parameters.length);
	}	
}
