/*
 * ====================================================================
 * 
 * The ObjectStyle Group Software License, Version 1.0
 * 
 * Copyright (c) 2005 The ObjectStyle Group and individual authors of the
 * software. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * 3. The end-user documentation included with the redistribution, if any, must
 * include the following acknowlegement: "This product includes software
 * developed by the ObjectStyle Group (http://objectstyle.org/)." Alternately,
 * this acknowlegement may appear in the software itself, if and wherever such
 * third-party acknowlegements normally appear.
 * 
 * 4. The names "ObjectStyle Group" and "Cayenne" must not be used to endorse or
 * promote products derived from this software without prior written permission.
 * For written permission, please contact andrus@objectstyle.org.
 * 
 * 5. Products derived from this software may not be called "ObjectStyle" nor
 * may "ObjectStyle" appear in their names without prior written permission of
 * the ObjectStyle Group.
 * 
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * OBJECTSTYLE GROUP OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ====================================================================
 * 
 * This software consists of voluntary contributions made by many individuals on
 * behalf of the ObjectStyle Group. For more information on the ObjectStyle
 * Group, please see <http://objectstyle.org/>.
 *  
 */
package org.objectstyle.wolips.ruleeditor.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;

/**
 * @author <a href="mailto:georg@moleque.com.br">Georg von Bülow</a>
 * @author <a href="mailto:frederico@moleque.com.br">Frederico Lellis</a>
 * @author <a href="mailto:hprange@moleque.com.br">Henrique Prange</a>
 */
public class LeftHandSideParser {
	private static class RegularExpressionTokenizer implements Iterator<String> {

		private String delim;

		private final CharSequence input;

		private int lastEnd = 0;

		private String match;

		private Matcher matcher;

		private final boolean returnDelims;

		public RegularExpressionTokenizer(final CharSequence input, final String patternStr, final boolean returnDelims) {
			this.input = input;
			this.returnDelims = returnDelims;

			Pattern pattern = Pattern.compile(patternStr);
			matcher = pattern.matcher(input);
		}

		public boolean hasNext() {
			if (matcher == null) {
				return false;
			}
			if (delim != null || match != null) {
				return true;
			}
			if (matcher.find()) {
				if (returnDelims) {
					delim = input.subSequence(lastEnd, matcher.start()).toString();
				}
				match = matcher.group();
				lastEnd = matcher.end();
			} else if (returnDelims && lastEnd < input.length()) {
				delim = input.subSequence(lastEnd, input.length()).toString();
				lastEnd = input.length();

				matcher = null;
			}
			return delim != null || match != null;
		}

		public boolean isNextToken() {
			return delim == null && match != null;
		}

		public String next() {
			String result = null;

			if (delim != null) {
				result = delim;
				delim = null;
			} else if (match != null) {
				result = match;
				match = null;
			}
			return result;
		}

		public void remove() {
			throw new UnsupportedOperationException();
		}
	}

	private static final String AND_OR_NOT_PATTERN;

	private static final String INTERNAL_QUALIFIER_PREFIX = "$INTERNAL_QUALIFIER_";

	private static final String OPERATORS_PATTERN = "(=|!=|>|<|>=|<=|like|caseInsensitiveLike|[^\\s]*:){1}";

	private static final Map<String, String> UNMODIFIABLE_NULL_VALUE_MAP;

	static {
		StringBuffer buffer = new StringBuffer();

		buffer.append("^");
		buffer.append(Qualifier.NOT.getDisplayName());
		buffer.append("|(\\s+");
		buffer.append(Qualifier.AND.getDisplayName());
		buffer.append("\\s+|\\s+");
		buffer.append(Qualifier.OR.getDisplayName());
		buffer.append("\\s+)+");

		// Pattern "^not|(\s+and\s+|\s+or\s+)+"
		AND_OR_NOT_PATTERN = buffer.toString();

		Map<String, String> tempMap = new HashMap<String, String>();

		tempMap.put("class", LhsValue.NULL_VALUE_CLASS);

		UNMODIFIABLE_NULL_VALUE_MAP = Collections.unmodifiableMap(tempMap);
	}

	private static String getClassFromMap(final Map<String, Object> properties) {
		return (String) properties.get(AbstractRuleElement.CLASS_KEY);
	}

	private static Collection<QualifierElement> getQualifiersFromMap(final Map<String, Object> map) {
		return (Collection<QualifierElement>) map.get(AbstractQualifierElement.QUALIFIERS_KEY);
	}

	private static Map<String, Object> propertiesForOneQualifier(final String expression) {
		// Handle one qualifier
		Iterator<String> tokenizer = new RegularExpressionTokenizer(expression, OPERATORS_PATTERN, true);

		tokenizer.hasNext();
		String key = tokenizer.next().trim().replaceAll("\\(", "");

		tokenizer.hasNext();
		String operator = tokenizer.next();

		tokenizer.hasNext();
		String rawValue = tokenizer.next();
		String value = rawValue.replaceAll("(\'|\"|\\))", "").trim();
		boolean isKeyComparison = !"null".equals(value) 
				&& Character.isAlphabetic(rawValue.trim().charAt(0));

		Map<String, Object> properties = new HashMap<String, Object>();

		properties.put(AbstractRuleElement.CLASS_KEY, isKeyComparison ? Qualifier.KEY_COMPARISON.getClassName() : Qualifier.KEY_VALUE.getClassName());
		properties.put(isKeyComparison ? AbstractQualifierElement.LEFT_KEY_KEY : AbstractQualifierElement.KEY_KEY, key);
		Selector sel = Selector.forOperator(operator);
		String selectorName = sel == null ? operator.substring(0, operator.length() - 1) : sel.getSelectorName();
		properties.put(AbstractQualifierElement.SELECTOR_NAME_KEY, selectorName);
		properties.put(isKeyComparison ? AbstractQualifierElement.RIGHT_KEY_KEY : AbstractQualifierElement.VALUE_KEY, valueRepresentation(value));

		return properties;
	}

	private static void putQualifiersIntoMap(final Map<String, Object> map, final Collection<QualifierElement> qualifiers) {
		map.put(AbstractQualifierElement.QUALIFIERS_KEY, qualifiers);
	}

	private static Object valueRepresentation(final String value) {
		if ("null".equals(value)) {
			return UNMODIFIABLE_NULL_VALUE_MAP;
		}

		return value;
	}

	private int count = 0;

	private final Map<String, String> qualifierSubstitutionMap = new HashMap<String, String>();

	private Map<String, Object> handleNotQualifier(final String conditionsToParse) {
		if (hasNotQualifier(conditionsToParse)) {
			String notPattern = "not\\s*\\(.*\\)";

			Iterator<String> tokenizer = new RegularExpressionTokenizer(conditionsToParse, notPattern, true);

			tokenizer.hasNext();
			String notText = tokenizer.next();

			if (tokenizer.hasNext()) {
				notText = tokenizer.next().trim();

				notText = notText.substring(3);

				Map<String, Object> qualifier = parse(notText);

				Map<String, Object> properties = new HashMap<String, Object>();

				properties.put(AbstractRuleElement.CLASS_KEY, Qualifier.NOT.getClassName());

				properties.put(AbstractQualifierElement.QUALIFIER_KEY, qualifier);

				return properties;
			}
		}

		return null;
	}

	private String handleParenthesis(final String expression) {
		String result = expression;

		for (; shouldHandleParenthesis(result); count++) {
			if (result.startsWith("(") && result.endsWith(")")) {
				result = result.substring(1, result.length() - 1);
			}

			// Handle parenthesis
			String parenthesisPattern = "\\([^\\(\\)]+\\)";

			Iterator<String> tokenizer = new RegularExpressionTokenizer(result, parenthesisPattern, false);

			tokenizer.hasNext();

			String internalQualifier = tokenizer.next();

			result = StringUtils.replace(result, internalQualifier, INTERNAL_QUALIFIER_PREFIX + count + "$");

			qualifierSubstitutionMap.put(INTERNAL_QUALIFIER_PREFIX + count + "$", internalQualifier);
		}

		return result;
	}

	private boolean hasMoreThanOneQualifier(final String expression) {
		Pattern pattern = Pattern.compile(AND_OR_NOT_PATTERN);
		Matcher matcher = pattern.matcher(expression);

		return matcher.find();
	}

	private boolean hasNotQualifier(final String expression) {
		int indexOfNot = expression.indexOf(Qualifier.NOT.getDisplayName());
		int indexOfOr = expression.indexOf(Qualifier.OR.getDisplayName());
		int indexOfAnd = expression.indexOf(Qualifier.AND.getDisplayName());

		if (indexOfNot < 0) {
			return false;
		}

		if (indexOfAnd >= 0 || indexOfOr >= 0) {
			return false;
		}

		return true;

		// if (indexOfAnd >= 0) {
		// return indexOfNot < indexOfAnd;
		// }
		//
		// if (indexOfOr >= 0) {
		// return indexOfNot < indexOfOr;
		// }
		//
		// return true;
		// if (indexOfAnd >= 0) {
		// if (indexOfNot < indexOfAnd) {
		// if (indexOfOr >= 0) {
		// return indexOfNot < indexOfOr;
		// }
		// }
		//
		// return false;
		// }
		//
		// if (indexOfOr >= 0) {
		// return indexOfNot < indexOfOr;
		// }
	}

	private boolean isGroupQualifier(final String expression) {
		return Qualifier.AND.getDisplayName().equals(expression) || Qualifier.OR.getDisplayName().equals(expression);
	}

	public Map<String, Object> parse(final String conditions) {
		if (conditions == null) {
			return Collections.emptyMap();
		}

		String trimmedConditions = conditions.trim();

		if ("".equals(trimmedConditions)) {
			return Collections.emptyMap();
		}

		String conditionsToParse = qualifierSubstitutionMap.get(trimmedConditions);

		if (conditionsToParse == null) {
			conditionsToParse = trimmedConditions;
		}

		if (!hasMoreThanOneQualifier(conditionsToParse)) {
			return propertiesForOneQualifier(conditionsToParse);
		}

		Map<String, Object> properties = handleNotQualifier(conditionsToParse);

		if (properties != null) {
			return properties;
		}

		conditionsToParse = handleParenthesis(conditionsToParse);

		return propertiesForManyQualifiers(conditionsToParse);

	}

	private Map<String, Object> propertiesForManyQualifiers(final String conditionsToParse) {
		Map<String, Object> properties = new HashMap<String, Object>();

		Iterator<String> tokenizer = new RegularExpressionTokenizer(conditionsToParse, AND_OR_NOT_PATTERN, true);

		Collection<QualifierElement> qualifiers = new ArrayList<QualifierElement>();

		putQualifiersIntoMap(properties, qualifiers);

		while (tokenizer.hasNext()) {
			String expression = tokenizer.next().trim();

			Map<String, Object> returnedMap;

			if (isGroupQualifier(expression)) {
				String groupQualifierClassName = Qualifier.AND.getDisplayName().equals(expression)
						? Qualifier.AND.getClassName()
						: Qualifier.OR.getClassName();

				tokenizer.hasNext();
				expression = tokenizer.next();

				String assignmentClassName = getClassFromMap(properties);

				if (assignmentClassName != null && !assignmentClassName.equals(groupQualifierClassName)) {
					returnedMap = parse(expression);

					QualifierElement qualifier = new QualifierElement(returnedMap);

					QualifierElement wrapperQualifier = new QualifierElement(properties);

					properties = new HashMap<String, Object>();

					putQualifiersIntoMap(properties, new ArrayList<QualifierElement>());

					properties.put(AbstractRuleElement.CLASS_KEY, groupQualifierClassName);

					getQualifiersFromMap(properties).add(wrapperQualifier);
					getQualifiersFromMap(properties).add(qualifier);
				} else {
					properties.put(AbstractRuleElement.CLASS_KEY, groupQualifierClassName);

					returnedMap = parse(expression);

					QualifierElement qualifier = new QualifierElement(returnedMap);

					getQualifiersFromMap(properties).add(qualifier);

				}
			} else {
				returnedMap = parse(expression);

				QualifierElement qualifier = new QualifierElement(returnedMap);

				getQualifiersFromMap(properties).add(qualifier);
			}
		}

		return properties;
	}

	private boolean shouldHandleParenthesis(final String expression) {
		Pattern pattern = Pattern.compile(".*\\(.*\\(");
		Matcher matcher = pattern.matcher(expression);

		return matcher.find();
	}
}
