package com.hp.isomorphic.client.issues;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;

import com.google.gwt.core.client.JavaScriptObject;
import com.smartgwt.client.data.AdvancedCriteria;
import com.smartgwt.client.data.Criteria;
import com.smartgwt.client.data.Criterion;
import com.smartgwt.client.data.DataSource;
import com.smartgwt.client.data.DataSourceField;
import com.smartgwt.client.data.RelativeDate;
import com.smartgwt.client.types.OperatorId;
import com.smartgwt.client.util.JSOHelper;

public class SmartGWTFilterBuilder {
	
	private static final Logger logger = Logger.getLogger(SmartGWTFilterBuilder.class.getName());

	private static final String JAVASCRIPT_OBJECT_PREFIX = "[object"; //$NON-NLS-1$
	private static final String NOW = "now"; //$NON-NLS-1$
	private static final String TODAY = "today"; //$NON-NLS-1$
	private static final String TOMORROW = "tomorrow"; //$NON-NLS-1$
	private static final String YESTERDAY = "yesterday"; //$NON-NLS-1$
	private static final String AND_OPERATOR = "and"; //$NON-NLS-1$
	private static final String OR_OPERATOR = "or"; //$NON-NLS-1$
	private static final String NOT_OPERATOR = "not"; //$NON-NLS-1$
	private static final String IS_OPERATOR = "is"; //$NON-NLS-1$

	public class AttributeNames {
		public static final String FIELD_NAME = "fieldName"; //$NON-NLS-1$
		public static final String OPERATOR = "operator"; //$NON-NLS-1$
		public static final String VALUE = "value"; //$NON-NLS-1$
		public static final String START = "start"; //$NON-NLS-1$
		public static final String END = "end"; //$NON-NLS-1$
		public static final String CRITERIA = "criteria"; //$NON-NLS-1$
		public static final String FILTER_STRING = "FILTER_STRING"; //$NON-NLS-1$
	}
	
	@SuppressWarnings("serial")
	private static final Map<OperatorId, OperatorId> alternateOps = new HashMap<OperatorId, OperatorId>() {
		{
			put(OperatorId.BETWEEN_INCLUSIVE, OperatorId.IBETWEEN_INCLUSIVE);
			put(OperatorId.IBETWEEN_INCLUSIVE, OperatorId.BETWEEN_INCLUSIVE);
			put(OperatorId.CONTAINS, OperatorId.ICONTAINS);
			put(OperatorId.ICONTAINS, OperatorId.CONTAINS);
			put(OperatorId.ENDS_WITH, OperatorId.IENDS_WITH);
			put(OperatorId.IENDS_WITH, OperatorId.ENDS_WITH);
			put(OperatorId.EQUALS, OperatorId.IEQUALS);
			put(OperatorId.IEQUALS, OperatorId.EQUALS);
			put(OperatorId.NOT_CONTAINS, OperatorId.INOT_CONTAINS);
			put(OperatorId.INOT_CONTAINS, OperatorId.NOT_CONTAINS);
			put(OperatorId.NOT_EQUAL, OperatorId.INOT_EQUAL);
			put(OperatorId.INOT_EQUAL, OperatorId.NOT_EQUAL);
			put(OperatorId.STARTS_WITH, OperatorId.ISTARTS_WITH);
			put(OperatorId.ISTARTS_WITH, OperatorId.STARTS_WITH);
			put(OperatorId.REGEXP, OperatorId.IREGEXP);
			put(OperatorId.IREGEXP, OperatorId.REGEXP);
		}
	};

	private static String[][] binaryOperators = new String[][] {
		{ OR_OPERATOR },
		{ AND_OPERATOR },
		{ "=", "!=", ">=", "<=", ">", "<", IS_OPERATOR }, //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
	};
	
	@SuppressWarnings("serial")
	private static Map<OperatorId, OperatorId> operatorToFieldOperatorMap = new HashMap<OperatorId, OperatorId>() {
		{
			put(OperatorId.EQUALS, OperatorId.EQUALS_FIELD);
			put(OperatorId.GREATER_OR_EQUAL, OperatorId.GREATER_OR_EQUAL_FIELD); 
			put(OperatorId.GREATER_THAN, OperatorId.GREATER_THAN_FIELD); 
			put(OperatorId.LESS_OR_EQUAL, OperatorId.LESS_OR_EQUAL_FIELD); 
			put(OperatorId.LESS_THAN, OperatorId.LESS_THAN_FIELD); 
			put(OperatorId.NOT_EQUAL, OperatorId.NOT_EQUAL_FIELD); 
		}
	};
	
	/**
	 * Parses a filter string, like "(jobName = 'Fred') and (jobStatus = 'Ready')" into GWT criteria object(s).
	 * @param filterString algebraic expression of the filter string
	 * @return GWT criteria object(s) equivalent to the given filter string
	 */
	public static AdvancedCriteria convertToCriteria(String filterString) {
		return convertToCriteria(filterString, null);
	}
	
	/**
	 * Parses a filter string, like "(jobName = 'Fred') and (jobStatus = 'Ready')" into GWT criteria object(s).
	 * @param filterString algebraic expression of the filter string
	 * @param dataSource dataSource that this filter will be applied to (used to find out data types of fields referenced in the filter)
	 * @return GWT criteria object(s) equivalent to the given filter string
	 */
	public static AdvancedCriteria convertToCriteria(String filterString, DataSource dataSource) {
		if( filterString == null || filterString.isEmpty() )
			return null;
		
		logger.finer("FILTER String: " + filterString ); //$NON-NLS-1$
		
		Parser parser = new SmartGWTFilterBuilder().makeParser(dataSource);
		AdvancedCriteria result = (AdvancedCriteria) parser.parse(filterString);

//		String jsonCriteria = JSON.encode(result.getJsObj());
//		logger.finer("2c FILTER JSON CRITERIA: " + jsonCriteria); //$NON-NLS-1$
//		result = new AdvancedCriteria(JSON.decode(jsonCriteria));
		logger.finer("2c FILTER JSON CRITERIA2: " + result.toJSON()); //$NON-NLS-1$
//		if (result != null) {
//			result.setAttribute(AttributeNames.FILTER_STRING, filterString);
//			String crit = JSON.encode(result.getJsObj());
//			logger.finer("FILTER String -> CRITERIA: " + crit); //$NON-NLS-1$
//		}
		
		return result;
	}
	
	public static String quote(String value) {
		return quote(value, "'"); //$NON-NLS-1$
	}
	
	public static String quoteFieldName(String fieldName) {
		return quote("{{" + fieldName + "}}"); //$NON-NLS-1$ //$NON-NLS-2$
	}
	
	private static String quote(String value, String quote) {
		return quote + value.replace(quote, quote + quote) + quote;
	}

	private Parser makeParser(DataSource dataSource) {
		return new Parser(dataSource);
	}

	// JFF we know that the filter builder is returning something like this when it is called.
	// There must be some way to construct an advanced criteria object with a relative date, but there's
	// no direct constructor.
	
	//	<isc_DataSource_0 constructor="AdvancedCriteria">
	//    <operator>and</operator>
	//    <criteria>
	//    	<fieldName>date</fieldName>
	//    	<operator>equals</operator>
	//    	<value>
	//    		<_constructor>RelativeDate</_constructor>
	//    		<value>$weekAgo</value>
	//    	</value>
	//    </criteria>
	//	</isc_DataSource_0>

	class Parser extends ExpressionParser {
		private static final String NULL = "null"; //$NON-NLS-1$
		protected DataSource dataSource;
		
		public Parser(DataSource dataSource) {
			this.dataSource = dataSource;
		}
		
		@Override
		protected Object makeBinaryExpression(String operator, Object lhs, Object rhs) {
			OperatorId op = getOperatorId(operator);
			
			if (op == OperatorId.AND || op == OperatorId.OR) {
			    return new AdvancedCriteria(getOperatorId(operator), new Criterion[] { (Criterion) lhs, (Criterion) rhs });
			} 
			
			String name = getStringRep(lhs);
			String value = getStringRep(rhs);
			
			if (name.startsWith("{{") && name.endsWith("}}")) { //$NON-NLS-1$ //$NON-NLS-2$
				name = name.substring(2, name.length() - 2);
			}
			
			if (value.startsWith("{{") && value.endsWith("}}")) { //$NON-NLS-1$ //$NON-NLS-2$
				value = value.substring(2, value.length() - 2);
				op = operatorToFieldOperatorMap.get(op);
			}
			
			boolean isRelativeDate = isRelativeDate(value);
			
			if (op == OperatorId.EQUALS && !isRelativeDate) {
				if (value.contains("*")) { //$NON-NLS-1$
					if (value.startsWith("*") && value.endsWith("*")) { //$NON-NLS-1$ //$NON-NLS-2$
						return createPropertyCriteria(name, getSupportedOperator(name, OperatorId.ICONTAINS), value.substring(1, value.length() - 1));
					}
					if (value.startsWith("*")) { //$NON-NLS-1$
						return createPropertyCriteria(name, getSupportedOperator(name, OperatorId.IENDS_WITH), value.substring(1, value.length()));
					}
					if (value.endsWith("*")) { //$NON-NLS-1$
						return createPropertyCriteria(name, getSupportedOperator(name, OperatorId.ISTARTS_WITH), value.substring(0, value.length() - 1));			
					}
					return createPropertyCriteria(name, getSupportedOperator(name, OperatorId.IEQUALS), value);
				}
				else if (value.equalsIgnoreCase("null")) { //$NON-NLS-1$
					return new AdvancedCriteria(name, getSupportedOperator(name, OperatorId.IS_NULL));
				}
				
				return createPropertyCriteria(name, getSupportedOperator(name, OperatorId.IEQUALS), value);
			}

			if (op == OperatorId.NOT_EQUAL && !isRelativeDate) {
				if (value.equalsIgnoreCase(NULL)) {
					return new AdvancedCriteria(name, getSupportedOperator(name, OperatorId.NOT_NULL));
				}

				return createPropertyCriteria(name, getSupportedOperator(name, OperatorId.INOT_EQUAL), value);
			}
			
			if (((op == OperatorId.IS_NULL) || (op == OperatorId.NOT_NULL)) && value.equalsIgnoreCase(NULL)) {
				return new AdvancedCriteria(name, getSupportedOperator(name, op));
			}

			if (isRelativeDate) {
				return makeRelativeDateCriteria(name, getSupportedOperator(name, op), value);
			}

			return createPropertyCriteria(name, getSupportedOperator(name, op), value);
		}
		
		private String getStringRep(Object val) {
			String strVal = val.toString();
			if (JSOHelper.isJSO(val) && val.toString().startsWith(JAVASCRIPT_OBJECT_PREFIX)) {
				JavaScriptObject jso = (JavaScriptObject) val;
				Object o = JSOHelper.convertToJava(jso);
				strVal = getStringRep(o);
			} 
			return fixupString(strVal);
		}
		
		private OperatorId getSupportedOperator(String name, OperatorId op) {
			logger.finest("SupportedOperator: " + name + " " + op); //$NON-NLS-1$ //$NON-NLS-2$
			if (dataSource != null) {
				logger.finest("SupportedOperator: found dataSource(" + dataSource.getID() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
				DataSourceField field = getDataSourceField(name);
				if (field != null) {
					logger.finest("SupportedOperator: found dataSourceField"); //$NON-NLS-1$
					OperatorId[] validOps = field.getValidOperators();
					if (validOps != null) {
						logger.finest("SupportedOperator: found validOps[" + validOps.length + "]"); //$NON-NLS-1$ //$NON-NLS-2$
						if (!isValidOp(validOps, op)) {
							logger.finest("SupportedOperator: not a valid op"); //$NON-NLS-1$
							OperatorId alternateOp = alternateOps.get(op);
							if (alternateOp != null) {
								logger.finest("SupportedOperator: found alternateOp(" + alternateOp + ")"); //$NON-NLS-1$ //$NON-NLS-2$
								if (isValidOp(validOps, alternateOp)) {
									logger.finest("SupportedOperator: using alternateOp"); //$NON-NLS-1$
									return alternateOp;
								}
							}
						}
					}
				}
			}
			return op;
		}

		// HACK-POG: Live mode fails to find field by name even though it is in there!
		private DataSourceField getDataSourceField(String fieldName) {
			DataSourceField field = dataSource.getField(fieldName);
			if (field == null) {
				logger.finest("SupportedOperator: did not find dataSourceField"); //$NON-NLS-1$
				for (DataSourceField f: dataSource.getFields()) {
					String name = f.getName();
					logger.finest("SupportedOperator: Field(" + name + ")"); //$NON-NLS-1$ //$NON-NLS-2$
					if (name.equals(fieldName)) {
						field = f;
						break;
					}
				}
			}
			
			return field;
		}
		
		private boolean isValidOp(OperatorId[] validOps, OperatorId op) {
			for (OperatorId validOp: validOps) {
				if (validOp.equals(op)) {
					return true;
				}
			}
			return false;
		}
		
		/*******
		 * This is a bug fix for an issue we are seeing with smartgwt.
		 * This behavior was only seen in production code (e.g. not development mode in eclipse).  
		 * Value's were not being wrapped in quotes in the json criteria were returning
		 * [Object object during subsequent parsing.  Example below should have job_priority 
		 *  and 25 wrapped in quotes (e.g. "job_priority" and "25").
		 * "criteria":[
         * {
         *  "fieldName":job_priority, 	- should be "job_priority"
         *  "operator":"greaterThan", 
         *  "value":25					- should be "25"
         * }, 
         * This simply forces into a new string which seems to resolve the issue.  Not sure if this is 
         * something unique to the particular nightly build we are using or a general design issue.
		 * @param value
		 * @return
		 */
		private String fixupString(String value) {
			//HACK: bugfix / workaround for smartgwt criteria string value's not being wrapped in quotes.
			//Some string values seem to be optimized out but this does the trick.  
			return "" + value; //$NON-NLS-1$
		}

		private Criteria createPropertyCriteria(String propName, OperatorId operator, String value) {
			Criteria criteria = null;
			
			if (dataSource != null) {
				DataSourceField field = dataSource.getField(propName);
				//HACK - try forcing quotes around propname
				propName = fixupString(propName);
				value = fixupString(value);
				if (field != null) {
					switch (field.getType()) {
					case BOOLEAN:
						logger.finer("Create advanced criteria for: " + propName + " " + operator.toString() + " " + value); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
						// FIXME-POG: HACK: For some strange reason the grid is not able to evaluate boolean fields against boolean data, but it can if the criteria
						// is against a string value of 'true'.  When we get from the server with a filter on the initial grid get, everything is fine, but if we
						// update/add data locally on the client in response to changes after the initial get, all values seem to filter out of the grid if we
						// set up the criteria against a boolean value instead of a string value.
						//criteria = new AdvancedCriteria(propName, operator, Boolean.parseBoolean(value));
						criteria = new AdvancedCriteria(propName, operator, value);
						break;
					case DATE:
					case DATETIME:
						try {
							Date date = DateUtils.parse(value);
							logger.finer("Create advanced criteria for: " + propName + " " + operator.toString() + " " + value); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
							criteria = new AdvancedCriteria(propName, operator, date);
						}
						catch (Exception e) {
							logger.fine("Failed to parse Date, setting using string. " + value); //$NON-NLS-1$
							criteria = new AdvancedCriteria(propName, operator, value);
						}
						break;
					case ENUM:
					case IMAGE:
					case LINK:
					case TEXT:
						logger.finer("Create advanced criteria for: " + propName + " " + operator.toString() + " " + value); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
						criteria = new AdvancedCriteria(propName, operator, value);
						break;
					case FLOAT:
						try {
							logger.finer("Create advanced criteria for: " + propName + " " + operator.toString() + " " + value); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
							criteria = new AdvancedCriteria(propName, operator, Float.parseFloat(value));
						}
						catch (NumberFormatException e) {
							criteria = new AdvancedCriteria(propName, operator, value);
						}
						break;
					case INTEGER:
					case INTENUM:
						try {
							logger.finer("Create advanced criteria for: " + propName + " " + operator.toString() + " " + value); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
							criteria = new AdvancedCriteria(propName, operator, Integer.parseInt(value));
						}
						catch (NumberFormatException e) {
							criteria = new AdvancedCriteria(propName, operator, value);
						}
						break;
					default:
						break;
					}
				}
			}
			
			if (criteria == null) {
				logger.finer("Create advanced criteria for: " + propName + " " + operator.toString() + " " + value); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				criteria = new AdvancedCriteria(propName, operator, value);
			}
			
			return criteria;
		}

		private boolean isRelativeDatePreset(String value) {
			if (value == null) {
				return false;
			}
			return value.startsWith("$") || value.equals(NOW) || value.equals(TODAY) || value.equals(YESTERDAY) || value.equals(TOMORROW); //$NON-NLS-1$
		}

		private boolean isRelativeDateOffset(String value) {
			if (value == null) {
				return false;
			}
			return value.startsWith("-") || value.startsWith("+"); //$NON-NLS-1$ //$NON-NLS-2$
		}
		
		private boolean isRelativeDate(String value) {
			if (value == null) {
				return false;
			}
			// FIX-JFF should be smarter than this, but this'll do for now.
			return isRelativeDatePreset(value) || isRelativeDateOffset(value);
		}
		
		private Criteria makeRelativeDateCriteria(String fieldName, OperatorId operator, String relativeDate) {
			//hack - try to quote name
			fieldName = fixupString(fieldName);
			if (isRelativeDatePreset(relativeDate)) {
				if (relativeDate.endsWith(NOW)) {
					return makeCriteria(fieldName, operator, RelativeDate.NOW);
				}

				if (operator == OperatorId.EQUALS) {
					return new AdvancedCriteria(OperatorId.AND, new Criterion[] {
							makeCriteria(fieldName, OperatorId.GREATER_OR_EQUAL, makeStartOf(relativeDate)), 
							makeCriteria(fieldName, OperatorId.LESS_OR_EQUAL, makeEndOf(relativeDate)) });
				}
				
				if (operator == OperatorId.NOT_EQUAL) {
					return new AdvancedCriteria(OperatorId.AND, new Criterion[] {
							makeCriteria(fieldName, OperatorId.LESS_THAN, makeStartOf(relativeDate)), 
							makeCriteria(fieldName, OperatorId.GREATER_OR_EQUAL, makeEndOf(relativeDate)) });
				}
	
				if (operator == OperatorId.LESS_THAN) {
					return makeCriteria(fieldName, operator, makeStartOf(relativeDate));
				}
	
				if (operator == OperatorId.LESS_OR_EQUAL) {
					return makeCriteria(fieldName, operator, makeEndOf(relativeDate));
				}
				
				if (operator == OperatorId.GREATER_THAN) {
					return makeCriteria(fieldName, operator, makeEndOf(relativeDate));
				}
	
				if (operator == OperatorId.GREATER_OR_EQUAL) {
					return makeCriteria(fieldName, operator, makeStartOf(relativeDate));
				}
			}
			
			return makeCriteria(fieldName, operator, new RelativeDate(relativeDate));
		}
		
		private RelativeDate makeStartOf(String relativeDate) {
			if (relativeDate.equals(TODAY) || relativeDate.equals("$" + TODAY)) { //$NON-NLS-1$
				return RelativeDate.START_OF_TODAY;
			}

			if (relativeDate.equals(YESTERDAY) || relativeDate.equals("$" + YESTERDAY)) { //$NON-NLS-1$
				return RelativeDate.START_OF_YESTERDAY;
			}

			if (relativeDate.equals(TOMORROW) || relativeDate.equals("$" + TOMORROW)) { //$NON-NLS-1$
				return RelativeDate.START_OF_TOMORROW;
			}

			return new RelativeDate(relativeDate);
		}
		
		private RelativeDate makeEndOf(String relativeDate) {
			if (relativeDate.equals(TODAY) || relativeDate.equals("$" + TODAY)) { //$NON-NLS-1$
//				return RelativeDate.END_OF_TODAY;
				// HACK: This is to work around the gwt filter builder having problems converting $endOfToday to the correct value (converts to same value as $startOfToday)
				return new RelativeDate("-0s[+0D]"); // +0D has same problem as $endOfToday, but 0 seconds before "end of today" seems to work //$NON-NLS-1$
			}

			if (relativeDate.equals(YESTERDAY) || relativeDate.equals("$" + YESTERDAY)) { //$NON-NLS-1$
//				return RelativeDate.END_OF_YESTERDAY;
				// HACK: This is to work around the gwt filter builder having problems converting $endOfYesterday to the correct value (converts to same value as $startOfYesterday)
				return new RelativeDate("-1ms[-0D]"); // 1 millisecond before $startOfToday //$NON-NLS-1$
			}

			if (relativeDate.equals(TOMORROW) || relativeDate.equals("$" + TOMORROW)) { //$NON-NLS-1$
//				return RelativeDate.END_OF_TOMORROW;
				// HACK: This is to work around the gwt filter builder having problems converting $endOfYesterday to the correct value (converts to same value as $startOfTomorrow)
				return new RelativeDate("-0s[+1D]"); // +1D has same problem as $endOfTomorrow, but 0 seconds before "end of tomorrow" seems to work //$NON-NLS-1$
			}
			
			return new RelativeDate(relativeDate);
		}
		
		private AdvancedCriteria makeCriteria(String fieldName, OperatorId operator, RelativeDate relativeDate) {
			AdvancedCriteria result = new AdvancedCriteria();
			result.setAttribute("fieldName", fieldName); //$NON-NLS-1$
			result.setAttribute("operator", operator); //$NON-NLS-1$
			result.setAttribute("value", relativeDate); //$NON-NLS-1$
			return result;
		}

		@Override
		protected Object makeUnaryExpression(String operator, Object expr) {
			return new AdvancedCriteria(getOperatorId(operator), new Criterion[] { (Criterion) expr });
		}

		@Override
		protected Object makeAtomicExpression(String token) {
			return token;
		}
 
		@Override
		protected String[][] getBinaryOperators() {
			return binaryOperators;
		}

		@Override
		protected String[] getUnaryOperators() {
			return new String[] { NOT_OPERATOR };
		}
	}

	private static Map<String, String> symbolFromOperatorNameMap = new HashMap<>();
	static {
		symbolFromOperatorNameMap.put(OperatorId.EQUALS.getValue(), "="); //$NON-NLS-1$
		symbolFromOperatorNameMap.put(OperatorId.IEQUALS.getValue(), "="); //$NON-NLS-1$
		symbolFromOperatorNameMap.put(OperatorId.NOT_EQUAL.getValue(), "!="); //$NON-NLS-1$
		symbolFromOperatorNameMap.put(OperatorId.INOT_EQUAL.getValue(), "!="); //$NON-NLS-1$
		symbolFromOperatorNameMap.put(OperatorId.GREATER_THAN.getValue(), ">"); //$NON-NLS-1$
		symbolFromOperatorNameMap.put(OperatorId.LESS_THAN.getValue(), "<"); //$NON-NLS-1$
		symbolFromOperatorNameMap.put(OperatorId.GREATER_OR_EQUAL.getValue(), ">="); //$NON-NLS-1$
		symbolFromOperatorNameMap.put(OperatorId.LESS_OR_EQUAL.getValue(), "<="); //$NON-NLS-1$
		symbolFromOperatorNameMap.put(OperatorId.AND.getValue(), AND_OPERATOR);
		symbolFromOperatorNameMap.put(OperatorId.OR.getValue(), OR_OPERATOR);
		symbolFromOperatorNameMap.put(OperatorId.NOT.getValue(), NOT_OPERATOR);
	}

	private static TreeMap<String, OperatorId> operatorIdFromSymbolMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
	static {
		operatorIdFromSymbolMap.put("=", OperatorId.EQUALS); //$NON-NLS-1$
		operatorIdFromSymbolMap.put("!=", OperatorId.NOT_EQUAL); //$NON-NLS-1$
		operatorIdFromSymbolMap.put(">", OperatorId.GREATER_THAN); //$NON-NLS-1$
		operatorIdFromSymbolMap.put(">=", OperatorId.GREATER_OR_EQUAL); //$NON-NLS-1$
		operatorIdFromSymbolMap.put("<", OperatorId.LESS_THAN); //$NON-NLS-1$
		operatorIdFromSymbolMap.put("<=", OperatorId.LESS_OR_EQUAL); //$NON-NLS-1$
		operatorIdFromSymbolMap.put(IS_OPERATOR, OperatorId.IS_NULL);
		operatorIdFromSymbolMap.put(AND_OPERATOR, OperatorId.AND);
		operatorIdFromSymbolMap.put(OR_OPERATOR, OperatorId.OR);
		operatorIdFromSymbolMap.put(NOT_OPERATOR, OperatorId.NOT);
	}
	private static OperatorId getOperatorId(String operator) {
		return operatorIdFromSymbolMap.get(operator);
	}
}

abstract class ExpressionParser {
	private String expression;
	
	public Object parse(String expression) {
		if (isBlank(expression)) return null;
		if (!isExpr(expression)) throw new IllegalArgumentException("Not a valid expression: '" + expression + "'"); //$NON-NLS-1$ //$NON-NLS-2$
		this.expression = expression;
		Object expr = eval();
		if (this.expression.trim().equals("")) //$NON-NLS-1$
			return expr;
		throw new IllegalArgumentException("Unexpected token " + this.expression + " found after parsing"); //$NON-NLS-1$ //$NON-NLS-2$
	}
	
	protected abstract Object makeBinaryExpression(String operator, Object lhs, Object rhs);
	protected abstract Object makeUnaryExpression(String operator, Object expr);
	protected abstract Object makeAtomicExpression(String token);
	protected abstract String[][] getBinaryOperators();
	protected abstract String[] getUnaryOperators();
	
	private Object eval() {
		return eval(0);
	}
	
	private Object eval(int i) {
		String[][] operators = getBinaryOperators();
		if (i >= operators.length) {
			return constk();
		}

		Object left = eval(i + 1);
		for (String op = match(operators[i]); op != null; op = match(operators[i])) {
			left = makeBinaryExpression(op, left, eval(i + 1));
		}
		return left;
	}

	private Object constk() {
		String op = match(getUnaryOperators());
		if (op != null) {
			return makeUnaryExpression(op, constk());
		}

        if (match("(") != null) { //$NON-NLS-1$
        	Object expr = eval();
        	if (match(")") == null) { //$NON-NLS-1$
        		throw new IllegalArgumentException("Mismatched parenthesis in expression: '" + expression + "'"); //$NON-NLS-1$ //$NON-NLS-2$
        	}
        	return expr;
        }

        if (Character.isLetterOrDigit(expression.charAt(0))) {
        	String token = getToken();
        	return makeAtomicExpression(token);
        }

        if ((expression.charAt(0) == '"') || (expression.charAt(0) == '\'')) {
        	String token = getQuoted();
        	return makeAtomicExpression(token);
        }
        
       	throw new IllegalArgumentException("Invalid expression: '" + expression + "'"); //$NON-NLS-1$ //$NON-NLS-2$
	}

	private String match(String[] tokens) {
		expression = trimStart(expression, " \t\r\n"); //$NON-NLS-1$
		if (tokens != null) {
			for (String tok : tokens) {
				if (tok != null) {
					if (expression.length() >= tok.length()) {
						String start = expression.substring(0, tok.length());
						if (start.equalsIgnoreCase(tok)) {
							expression = expression.substring(tok.length());
							return tok;
						}
					}
				}
			}
		}
		return null;
	}
	
	private String getToken() {
		int length = 0;
		for (int i = 0; i < expression.length() && isTokenChar(expression.charAt(i)); i++) {
			length++;
		}
		String token = expression.substring(0, length);
		expression = expression.substring(token.length());
		return token;
	}
	
	private String getQuoted() {
		char quote = expression.charAt(0);
		int length = 0;
		for (int i = 1; i < expression.length(); i++) {
			if (expression.charAt(i) == quote) {
				// Look for escaped embedded quote marks in the string
				if (i + 1 < expression.length() && expression.charAt(i + 1) == quote) {
					length += 2;
					i++;
				}
				else {
					break;
				}
			}
			else {
				length++;
			}
		}
		String token = expression.substring(1, length + 1);
		expression = expression.substring(token.length() + 2);
		return token.replace("" + quote + quote, "" + quote); //$NON-NLS-1$ //$NON-NLS-2$
	}
	
	private String match(String tok) {
		return match(new String[] { tok });
	}
	
	private static boolean isExpr(String s) {
        String starters = "(.-!'\"_ \t"; //$NON-NLS-1$
        if (isNullOrEmpty(s)) return false;
        return Character.isLetterOrDigit(s.charAt(0)) || starters.indexOf(s.charAt(0)) >= 0;		
	}
	
	private static boolean isBlank(String s) {
		return (s == null) || s.trim().isEmpty();
	}
	
	private static boolean isNullOrEmpty(String s) {
		return (s == null || s.trim().isEmpty());
	}
	
	private static String trimStart(String s, String trimChars) {
		for (int i = 0; i < s.length(); i++) {
			if (trimChars.indexOf(s.charAt(i)) >= 0) continue;
			return s.substring(i);
		}
		return s;
	}
		
	private static boolean isTokenChar(char ch) {
		if (Character.isLetterOrDigit(ch)) {
			return true;
		}
        
		if (ch == '.') { // Symbols can contain dots, like USLetter.Mono.SimplexImpressions
			return true;
		}
        
		if (ch == '_') { // They can also contain underscores, like businessCard_2point16x3point58in
			return true;
		}
        
		return false;
	}
}