/**
 * 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.templatecompiler.el;

import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.el.PropertyNotFoundException;

import org.ajax4jsf.templatecompiler.builder.CompilationContext;
import org.ajax4jsf.templatecompiler.builder.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.sun.el.parser.ArithmeticNode;
import com.sun.el.parser.AstAnd;
import com.sun.el.parser.AstBracketSuffix;
import com.sun.el.parser.AstChoice;
import com.sun.el.parser.AstDeferredExpression;
import com.sun.el.parser.AstDiv;
import com.sun.el.parser.AstDotSuffix;
import com.sun.el.parser.AstEmpty;
import com.sun.el.parser.AstEqual;
import com.sun.el.parser.AstFalse;
import com.sun.el.parser.AstFunction;
import com.sun.el.parser.AstGreaterThan;
import com.sun.el.parser.AstGreaterThanEqual;
import com.sun.el.parser.AstIdentifier;
import com.sun.el.parser.AstInteger;
import com.sun.el.parser.AstLessThan;
import com.sun.el.parser.AstLessThanEqual;
import com.sun.el.parser.AstLiteralExpression;
import com.sun.el.parser.AstMinus;
import com.sun.el.parser.AstMod;
import com.sun.el.parser.AstMult;
import com.sun.el.parser.AstNot;
import com.sun.el.parser.AstNotEqual;
import com.sun.el.parser.AstOr;
import com.sun.el.parser.AstPlus;
import com.sun.el.parser.AstString;
import com.sun.el.parser.AstTrue;
import com.sun.el.parser.AstValue;
import com.sun.el.parser.BooleanNode;
import com.sun.el.parser.ELParser;
import com.sun.el.parser.Node;

/**
 * Compiler EL-expressions.
 * 
 * @author ayukhovich@exadel.com (latest modification by $Author: alexsmirnov $)
 * @version $Revision: 1.1.2.2 $ $Date: 2007/02/21 17:17:12 $
 * 
 */
public class ELCompiler implements IELCompiler {

	private static final Log log = LogFactory.getLog(ELCompiler.class);
	
	Map<String, String> functionsMap = new HashMap<String,String>();

	static {
	}

	/**
	 * 
	 */
	public ELCompiler() {
		super();
		resetVariables();
	}

	/**
	 * 
	 */
	public void resetVariables() {
		// maps.put("context", "javax.faces.context.FacesContext" );
		// maps.put("component", "javax.faces.component.UIComponent" );
		// maps.put("a4jSkin", "org.ajax4jsf.framework.skin.Skin" );

		this.functionsMap.put("a4jSkin:getParameter",
				"org.ajax4jsf.framework.skin.getParameter");
	}

	public String compileEL(String expression, CompilationContext componentBean) {
		Node node = ELParser.parse(expression);
		StringBuffer sbMain = new StringBuffer();
		processNode(node, sbMain, componentBean);
		return sbMain.toString();
	}

	/**
	 * Processing node
	 * 
	 * @param node
	 * @param sbMain
	 * @param componentBean
	 */
	private void processNode(Node node, StringBuffer sbMain,
			CompilationContext componentBean) {
		int numChildren = node.jjtGetNumChildren();

		boolean bNeedConversion = false;
		for (int i = 0; i < numChildren; i++) {
			Node childNode = node.jjtGetChild(i);
			if (childNode instanceof AstLiteralExpression) {
				bNeedConversion = true;
				break;
			}
		}

		for (int i = 0; i < numChildren; i++) {
			Node childNode = node.jjtGetChild(i);

			if (childNode instanceof AstLiteralExpression) {
				if (childNode.getImage() != null) {
					if (i > 0) {
						sbMain.append(" + ");
					}
					sbMain.append("\"");
					sbMain.append(StringUtils.getEscapedString(childNode
							.getImage()));
					sbMain.append("\"");

					if (i < (numChildren - 1)) {
						sbMain.append(" + ");
					}
				}
			} else {
				if (bNeedConversion) {
					sbMain.append("convertToString(");
				}
				boolean processing = processingNode(childNode, sbMain,
						componentBean);
				if (!processing) {
					processNode(childNode, sbMain, componentBean);
				}
				if (bNeedConversion) {
					sbMain.append(")");
				}
			}

		}
	}

	/**
	 * 
	 * @param node
	 * @param sbMain
	 * @param componentBean
	 * @param cMain
	 * @return
	 */
	public boolean processingNode(Node node, StringBuffer sbMain,
			CompilationContext componentBean) {
		boolean returnValue = false;
		if (node instanceof ArithmeticNode) {
			returnValue = processingArithmeticNode((ArithmeticNode) node,
					sbMain, componentBean);
		} else if (node instanceof AstIdentifier) {
			returnValue = processingIdentifier((AstIdentifier) node, sbMain,
					componentBean);
		} else if (node instanceof AstValue) {
			returnValue = processingValue((AstValue) node, sbMain,
					componentBean);
		} else if (node instanceof AstInteger) {
			returnValue = processingInteger((AstInteger) node, sbMain);
		} else if (node instanceof AstString) {
			returnValue = processingString((AstString) node, sbMain);
		} else if (node instanceof AstFunction) {
			returnValue = processingFunction((AstFunction) node, sbMain,
					componentBean);
		} else if (node instanceof AstDeferredExpression) {

		} else if (node instanceof BooleanNode) {
			returnValue = processingBooleanNode((BooleanNode) node,
					sbMain, componentBean);
		} else if (node instanceof AstNot) {
			returnValue = processingNot((AstNot) node,
					sbMain, componentBean);
		} else if (node instanceof AstChoice) {
			returnValue = processingChoice((AstChoice) node,
					sbMain, componentBean);
		} else if (node instanceof AstEmpty) {
			returnValue = processingEmpty((AstEmpty) node,
					sbMain, componentBean);
		} else {
			StringBuffer sb = new StringBuffer();
			sb.append(node.toString());
			sb.append(" (");
			sb.append(node.getClass().getName());
			sb.append(")");
			log.debug(sb.toString());
		}

		return returnValue;
	}
	
	/**
	 * Processing 'empty' node
	 * 
	 * @param node
	 * @param sb
	 * @param cMain
	 * @return
	 */
	private boolean processingEmpty(AstEmpty node, StringBuffer sbMain,
			CompilationContext componentBean) {
		boolean returnValue = false;
		StringBuffer sb1 = new StringBuffer();
		
		Node node1 = node.jjtGetChild(0);

		if (null != node1) {
			if (!(returnValue = processingNode(node1, sb1,
					componentBean))) {
				log.error("Error processing node1: "
						+ node1.getImage());
			}
		}
		
		if (returnValue) {
			sbMain.append(" getUtils().isEmpty( ");
			sbMain.append(sb1);
			sbMain.append(" ) ");
		}
		
		return returnValue;
	}

	/**
	 * Processing 'choice' node
	 * 
	 * @param node
	 * @param sb
	 * @param cMain
	 * @return
	 */
	private boolean processingChoice(AstChoice node, StringBuffer sbMain,
			CompilationContext componentBean) {
		boolean returnValue = true;
		
		StringBuffer sb1 = new StringBuffer();
		StringBuffer sb2 = new StringBuffer();
		StringBuffer sb3 = new StringBuffer();
		
		Node node1 = node.jjtGetChild(0);

		if (node1 != null) {
			if (!(returnValue &= processingNode(node1, sb1,
					componentBean))) {
				log.error("Error processing node1: "
						+ node1.getImage());
			}
		}
		
		Node node2 = node.jjtGetChild(1);

		if (null != node2) {
			if (!(returnValue &= processingNode(node2, sb2,
					componentBean))) {
				log.error("Error processing node2: "
						+ node2.getImage());
			}
		}
		
		Node node3 = node.jjtGetChild(2);

		if (null != node3) {
			if (!(returnValue &= processingNode(node3, sb3,
					componentBean))) {
				log.error("Error processing node3: "
						+ node3.getImage());
			}
		}
		
		if (returnValue) {
			sbMain.append(" ( ");
			sbMain.append(sb1);
			sbMain.append(" ? ");
			sbMain.append(sb2);
			sbMain.append(" : ");
			sbMain.append(sb3);
			sbMain.append(" ) ");
		}
		
		return returnValue;
	}

	/**
	 * Processing node containing 'not' expression
	 * 
	 * @param node
	 * @param sb
	 * @param cMain
	 * @return
	 */
	private boolean processingNot(AstNot node, StringBuffer sbMain,
			CompilationContext componentBean) {
		boolean returnValue = false;
		StringBuffer sb1 = new StringBuffer();
		
		Node node1 = node.jjtGetChild(0);

		if (null != node1) {
			if (!(returnValue = processingNode(node1, sb1,
					componentBean))) {
				log.error("Error processing node1: "
						+ node1.getImage());
			}
		}
		
		if (returnValue) {
			sbMain.append(" ( ! ");
			sbMain.append(sb1);
			sbMain.append(" ) ");
		}
		
		return returnValue;
	}

	/**
	 * Processing boolean node
	 * 
	 * @param node
	 * @param sb
	 * @param cMain
	 * @return
	 */
	private boolean processingBooleanNode(BooleanNode node,
			StringBuffer sb, CompilationContext componentBean) {
		boolean returnValue = true;
		
		StringBuffer sb1 = new StringBuffer();
		StringBuffer sb2 = new StringBuffer();
		
		if (node instanceof AstFalse) {
			sb.append(" false ");
			return returnValue;
		}
		if (node instanceof AstTrue) {
			sb.append(" true ");
			return returnValue;
		}
		
		Node node1 = node.jjtGetChild(0);

		if (node1 != null) {
			if (!(returnValue &= processingNode(node1, sb1,
					componentBean))) {
				log.error("Error processing node1: "
						+ node1.getImage());
			}
		}
		
		Node node2 = node.jjtGetChild(1);

		if (null != node2) {
			if (!(returnValue &= processingNode(node2, sb2,
					componentBean))) {
				log.error("Error processing node2: "
						+ node2.getImage());
			}
		}
		
		if (returnValue) {
			sb.append(" ( ");
			sb.append(sb1);
			
			if (node instanceof AstAnd) {
				sb.append(" && ");
			} else if (node instanceof AstEqual) {
				sb.append(" == ");
			} else if (node instanceof AstGreaterThan) {
				sb.append(" > ");
			} else if (node instanceof AstGreaterThanEqual) {
				sb.append(" >= ");
			} else if (node instanceof AstLessThan) {
				sb.append(" < ");
			} else if (node instanceof AstLessThanEqual) {
				sb.append(" <= ");
			} else if (node instanceof AstNotEqual) {
				sb.append(" != ");
			} else if (node instanceof AstOr) {
				sb.append(" || ");
			}
			sb.append(sb2);			
			sb.append(" ) ");
		}
		
		return returnValue;
	}


	/**
	 * Processing arithmetic node
	 * 
	 * @param node
	 * @param sb
	 * @param cMain
	 * @return
	 */
	private boolean processingArithmeticNode(ArithmeticNode node,
			StringBuffer sb, CompilationContext componentBean) {
		StringBuffer sb1 = new StringBuffer();
		StringBuffer sb2 = new StringBuffer();

		boolean returnValue = true;

		Node node1 = node.jjtGetChild(0);

		if (node1 != null) {
			if (!(returnValue &= processingNode(node1, sb1,
					componentBean))) {
				log.error("Error processing node1: "
						+ node1.getImage());
			}
		}
		
		Node node2 = node.jjtGetChild(1);

		if (null != node2) {
			if (!(returnValue &= processingNode(node2, sb2,
					componentBean))) {
				log.error("Error processing node2: "
						+ node2.getImage());
			}
		}

		if (returnValue) {
			sb.append(" ( ");
			sb.append(sb1);

			if (node instanceof AstDiv) {
				sb.append(" / ");
			} else if (node instanceof AstMult) {
				sb.append(" * ");
			} else if (node instanceof AstMod) {
				sb.append(" % ");
			} else if (node instanceof AstPlus) {
				sb.append(" + ");
			} else if (node instanceof AstMinus) {
				sb.append(" - ");
			}

			sb.append(sb2);
			sb.append(" ) ");
		}
		return returnValue;
	}

	/**
	 * Processing node contain integer
	 * 
	 * @param node
	 * @param sb
	 * @return
	 */
	private boolean processingInteger(AstInteger node, StringBuffer sb) {
		sb.append(node.getImage());
		return true;
	}

	/**
	 * Processing node contain string
	 * 
	 * @param node
	 * @param sb
	 * @return
	 */
	private boolean processingString(AstString node, StringBuffer sb) {
		sb.append("\"");
		sb.append(node.getString());
		sb.append("\"");
		return true;
	}

	/**
	 * 
	 * @param node
	 * @param sb
	 * @return
	 */
	private boolean processingFunction(AstFunction node, StringBuffer sb,
			CompilationContext componentBean) {

		log.debug("Processing function : " + node.getPrefix());
		log.debug("Processing function : " + node.getLocalName());
		log.debug("Processing function : " + node.getOutputName());

		String prefix = node.getPrefix();
		boolean isThis = prefix.equals("this");
		boolean isUtils = prefix.equals("utils");
		
		if (isThis || isUtils) {
			if (isUtils) {
				sb.append("getUtils().");
			}
			
			sb.append(node.getLocalName());
			sb.append("(");
			int numChildren = node.jjtGetNumChildren();
			for (int i = 0; i < numChildren; i++) {
				Node childNode = node.jjtGetChild(i);
				StringBuffer buf = new StringBuffer();
				processingNode(childNode, buf, componentBean);
				if (i != 0) {
					sb.append(",");
				}
				sb.append(buf);
			}
			sb.append(")");
		} else {
			String functionName = node.getOutputName();
			if (this.functionsMap.containsKey(functionName)) {
				sb.append(this.functionsMap.get(functionName));
				sb.append("(");
				int numChildren = node.jjtGetNumChildren();
				for (int i = 0; i < numChildren; i++) {
					Node childNode = node.jjtGetChild(i);
					StringBuffer buf = new StringBuffer();
					processingNode(childNode, buf, componentBean);
					if (i != 0) {
						sb.append(",");
					}
					sb.append(buf);
				}
				sb.append(")");
			} // if
		} // else
		return true;
	}

	/**
	 * 
	 * @param node
	 * @param sb
	 * @return
	 */
	private boolean processingIdentifier(AstIdentifier node, StringBuffer sb,
			CompilationContext componentBean) {
		String variableName = node.getImage();
		if (componentBean.containsVariable(variableName)) {
			sb.append(variableName);
		} else {
			sb.append("variables.getVariable(\"");
			sb.append(variableName);
			sb.append("\")");
		}

		return true;
	}

	/**
	 * 
	 * @param basketSuffix
	 * @return
	 */
	private String processingBracketSuffix(AstBracketSuffix basketSuffix,
			CompilationContext componentBean) {
		StringBuffer sb = new StringBuffer();
		Node node = basketSuffix.jjtGetChild(0);
		if (node instanceof AstIdentifier) {
			processingIdentifier((AstIdentifier) node, sb, componentBean);
		} else if (node instanceof AstInteger) {
			// sb.append("new Integer(");
			sb.append(node.getImage());
			// sb.append(")");
		} else if (node instanceof AstString) {
			AstString stringNode = (AstString) node;
			sb.append("\"");
			sb.append(stringNode.getString());
			sb.append("\"");
		} else {
			sb.append("\"");
			sb.append(node.getImage());
			sb.append("\"");
		}
		return sb.toString();
	}
	
	/**
	 * 
	 * @param clazz
	 * @param propertyName
	 * @return
	 */
	private PropertyDescriptor getPropertyDescriptor(Class<?> clazz,
			String propertyName, CompilationContext compilationContext) {
		return compilationContext.getPropertyDescriptor(clazz, propertyName);
	}

	private boolean processingValue(AstValue node, StringBuffer sb,
			CompilationContext componentBean) {
		String lastIndexValue = "null";
		String lastVariableType = null;
		List<String> names = new ArrayList<String>();

		for (int i = 0; i < node.jjtGetNumChildren(); i++) {
			StringBuffer sb1 = new StringBuffer();
			Node subChild = node.jjtGetChild(i);

			if (subChild instanceof AstIdentifier) {
				String variableName = subChild.getImage();
				if (componentBean.containsVariable(variableName)) {
					lastVariableType = componentBean.getVariableType(
							variableName).getName();
					names.add(variableName);
				} else {
					processingIdentifier((AstIdentifier) subChild, sb1,
							componentBean);
				}
			} else if (subChild instanceof AstDotSuffix) {
				String propertyName = subChild.getImage();
				log.debug("Object: " + lastVariableType
						+ ", property: " + propertyName);

				if (lastVariableType != null) {
					try {

						Class<?> clazz = componentBean.loadClass(lastVariableType);

						PropertyDescriptor propertyDescriptor = getPropertyDescriptor(
								clazz, propertyName, componentBean);

						if (propertyDescriptor == null) {
							throw new PropertyNotFoundException("property: "
									+ propertyName + " not found in class: "
									+ lastVariableType);
						}

						log.debug("propertyObject: "
								+ propertyDescriptor.getPropertyType()
										.getName());
						StringBuffer tmpbuf = new StringBuffer();
						tmpbuf.append(propertyDescriptor.getReadMethod()
								.getName());
						tmpbuf.append("()");
						names.add(tmpbuf.toString());

						lastVariableType = propertyDescriptor.getPropertyType()
								.getName();
					} catch (ClassNotFoundException e) {
					    log.error(e.getLocalizedMessage(), e);
					}

				} else {

					sb1.append("getProperty(");
					sb1.append(lastIndexValue);
					sb1.append(",");
					sb1.append("\"");
					sb1.append(subChild.getImage());
					sb1.append("\")");
				}
			} else if (subChild instanceof AstBracketSuffix) {
				String bracketSuffix = processingBracketSuffix(
						(AstBracketSuffix) subChild, componentBean);

				if (lastVariableType != null) {
					StringBuffer tmpbuf = new StringBuffer();
					if (lastVariableType.startsWith("[L")) {
						tmpbuf.append("[");
						tmpbuf.append(bracketSuffix);
						tmpbuf.append("]");
						names.add(tmpbuf.toString());
					}

					if ((lastVariableType.compareTo("java.util.List") == 0)
							|| (lastVariableType.compareTo("java.util.Map") == 0)) {
						tmpbuf.append("get(");
						tmpbuf.append(bracketSuffix);
						tmpbuf.append(")");
						names.add(tmpbuf.toString());
					}
				} else {

					sb1.append("getElelmentByIndex(");
					sb1.append(lastIndexValue);
					sb1.append(",");
					sb1.append(bracketSuffix);
					sb1.append(")");
				}

			}

		}

		if (names.size() != 0) {
			StringBuffer tmpbuf = new StringBuffer();
			for (String element : names) {
				if (tmpbuf.length() != 0) {
					tmpbuf.append(".");
				}
				tmpbuf.append(element);
			}
			sb.append(tmpbuf.toString());
		} else {
			sb.append(lastIndexValue);
		}

		return true;
	}

}
