// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
// * with the License.  You may obtain a copy of the License at                                                              *
// *                                                                                                                         *
// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
// *                                                                                                                         *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
// * specific language governing permissions and limitations under the License.                                              *
// ***************************************************************************************************************************
package org.apache.juneau.utils;

import static java.util.Calendar.*;
import static org.apache.juneau.internal.StringUtils.*;

import java.lang.reflect.*;
import java.text.*;
import java.util.*;
import java.util.regex.*;

import org.apache.juneau.*;
import org.apache.juneau.internal.*;

/**
 * Designed to provide search/view/sort/paging filtering on tabular in-memory POJO models.
 * 
 * <p>
 * It can also perform just view filtering on beans/maps.
 * 
 * <p>
 * Examples of tabular POJO models:
 * <ul>
 * 	<li><tt>Collection{@code <Map>}</tt>
 * 	<li><tt>Collection{@code <Bean>}</tt>
 * 	<li><tt>Map[]</tt>
 * 	<li><tt>Bean[]</tt>
 * </ul>
 * 
 * <p>
 * Tabular POJO models can be thought of as tables of data.  For example, a list of the following beans...
 * <p class='bcode'>
 * 	<jk>public</jk> MyBean {
 * 		<jk>public int</jk> myInt;
 * 		<jk>public</jk> String myString;
 * 		<jk>public</jk> Date myDate;
 * 	}
 * <p>
 * 	... can be thought of a table containing the following columns...
 * <p>
 * <table class='styled code'>
 * 	<tr><th>myInt</th><th>myString</th><th>myDate</th></tr>
 * 	<tr><td>123</td><td>'foobar'</td><td>yyyy/MM/dd HH:mm:ss</td></tr>
 * 	<tr><td colspan=3>...</td></tr>
 * </table>
 * 
 * <p>
 * From this table, you can perform the following functions:
 * <ul class='spaced-list'>
 * 	<li>
 * 		Search - Return only rows where a search pattern matches.
 * 	<li>
 * 		View - Return only the specified subset of columns in the specified order.
 * 	<li>
 * 		Sort - Sort the table by one or more columns.
 * 	<li>
 * 		Position/limit - Only return a subset of rows.
 * </ul>
 * 
 * <h5 class='topic'>Search</h5>
 * 
 * The search capabilities allow you to filter based on query patterns against strings, dates, and numbers.
 * Queries take the form of a Map with column names as keys, and search patterns as values.
 * <br>Multiple search patterns are ANDed (i.e. all patterns must match for the row to be returned).
 * 
 * <h5 class='section'>Example:</h5>
 * <ul class='spaced-list'>
 * 	<li>
 * 		<tt>{myInt:'123'}</tt> - Return only rows where the <tt>myInt</tt> column is 123.
 * 	<li>
 * 		<tt>{myString:'foobar'}</tt> - Return only rows where the <tt>myString</tt> column is 'foobar'.
 * 	<li>
 * 		<tt>{myDate:'2001'}</tt> - Return only rows where the <tt>myDate</tt> column have dates in the year 2001.
 * </ul>
 * 
 * <h5 class='topic'>String Patterns</h5>
 * 
 * Any objects can be queried against using string patterns.
 * If the objects being searched are not strings, then the patterns are matched against whatever is return by the
 * {@code Object#toString()} method.
 * 
 * <h5 class='topic'>Example string query patterns:</h5>
 * <ul>
 * 	<li><tt>foo</tt> - The string 'foo'
 * 	<li><tt>foo bar</tt> - The string 'foo' or the string 'bar'
 * 	<li><tt>'foo bar'</tt> - The phrase 'foo bar'
 * 	<li><tt>"foo bar"</tt> - The phrase 'foo bar'
 * 	<li><tt>foo*</tt> - <tt>*</tt> matches zero-or-more characters.
 * 	<li><tt>foo?</tt> - <tt>?</tt> matches exactly one character
 * </ul>
 * 
 * <h5 class='section'>Notes:</h5>
 * <ul class='spaced-list'>
 * 	<li>
 * 		Whitespace is ignored around search patterns.
 * 	<li>
 * 		Prepend <tt>+</tt> to tokens that must match.  (e.g. <tt>+foo* +*bar</tt>)
 * 	<li>
 * 		Prepend <tt>-</tt> to tokens that must not match.  (e.g. <tt>+foo* -*bar</tt>)
 * </ul>
 * 
 * <h5 class='topic'>Numeric Patterns</h5>
 * 
 * Any object of type {@link Number} (or numeric primitives) can be searched using numeric patterns.
 * 
 * <h5 class='topic'>Example numeric query patterns:</h5>
 * <ul>
 * 	<li><tt>123</tt> - The single number 123
 * 	<li><tt>1 2 3</tt>	- 1, 2, or 3
 * 	<li><tt>1-100</tt> - Between 1 and 100
 * 	<li><tt>1 - 100</tt> - Between 1 and 100
 * 	<li><tt>1 - 100 200-300</tt> - Between 1 and 100 or between 200 and 300
 * 	<li><tt>&gt; 100</tt> - Greater than 100
 * 	<li><tt>&gt;= 100</tt> - Greater than or equal to 100
 * 	<li><tt>!123</tt> - Not 123
 * </ul>
 * 
 * <h5 class='section'>Notes:</h5>
 * <ul class='spaced-list'>
 * 	<li>
 * 		Whitespace is ignored in search patterns.
 * 	<li>
 * 		Negative numbers are supported.
 * </ul>
 * 
 * <h5 class='topic'>Date Patterns</h5>
 * 
 * Any object of type {@link Date} or {@link Calendar} can be searched using date patterns.
 * 
 * <p>
 * The default valid input timestamp formats (which can be overridden via the {@link #setValidTimestampFormats(String...)}
 * method are...
 * 
 * <ul>
 * 	<li><tt>yyyy.MM.dd.HH.mm.ss</tt>
 * 	<li><tt>yyyy.MM.dd.HH.mm</tt>
 * 	<li><tt>yyyy.MM.dd.HH</tt>
 * 	<li><tt>yyyy.MM.dd</tt>
 * 	<li><tt>yyyy.MM</tt>
 * 	<li><tt>yyyy</tt>
 * </ul>
 * 
 * <h5 class='topic'>Example date query patterns:</h5>
 * <ul>
 * 	<li><tt>2001</tt> - A specific year.
 * 	<li><tt>2001.01.01.10.50</tt> - A specific time.
 * 	<li><tt>&gt;2001</tt>	- After a specific year.
 * 	<li><tt>&gt;=2001</tt> - During or after a specific year.
 * 	<li><tt>2001 - 2003.06.30</tt>	- A date range.
 * 	<li><tt>2001 2003 2005</tt>	- Multiple date patterns are ORed.
 * </ul>
 * 
 * <h5 class='section'>Notes:</h5>
 * <ul class='spaced-list'>
 * 	<li>
 * 		Whitespace is ignored in search patterns.
 * </ul>
 * 
 * <h5 class='topic'>View</h5>
 * 
 * The view capability allows you to return only the specified subset of columns in the specified order.
 * <br>The view parameter is a list of either <tt>Strings</tt> or <tt>Maps</tt>.
 * 
 * <h5 class='topic'>Example view parameters:</h5>
 * <ul>
 * 	<li><tt>column1</tt> - Return only column 'column1'.
 * 	<li><tt>column2, column1</tt> - Return only columns 'column2' and 'column1' in that order.
 * </ul>
 * 
 * <h5 class='topic'>Sort</h5>
 * 
 * The sort capability allows you to sort values by the specified rows.
 * <br>The sort parameter is a list of strings with an optional <js>'+'</js> or <js>'-'</js> suffix representing
 * ascending and descending order accordingly.
 * 
 * <h5 class='topic'>Example sort parameters:</h5>
 * <ul>
 * 	<li><tt>column1</tt> - Sort rows by column 'column1' ascending.
 * 	<li><tt>column1+</tt> - Sort rows by column 'column1' ascending.
 * 	<li><tt>column1-</tt> - Sort rows by column 'column1' descending.
 * 	<li><tt>column1, column2-</tt> - Sort rows by column 'column1' ascending, then 'column2' descending.
 * </ul>
 * 
 * <h5 class='topic'>Paging</h5>
 * 
 * Use the <tt>position</tt> and <tt>limit</tt> parameters to specify a subset of rows to return.
 */
@SuppressWarnings({"unchecked","rawtypes"})
public final class PojoQuery {

	private Object input;
	private ClassMeta type;
	private BeanSession session;

	/**
	 * Constructor.
	 * 
	 * @param input The POJO we're going to be filtering.
	 * @param session The bean session to use to create bean maps for beans.
	 */
	public PojoQuery(Object input, BeanSession session) {
		this.input = input;
		this.type = session.getClassMetaForObject(input);
		this.session = session;
	}

	/**
	 * Filters the input object as a collection of maps.
	 * 
	 * @param args The search arguments.
	 * @return The filtered collection.
	 * Returns the unaltered input if the input is not a collection or array of objects.
	 */
	public List filter(SearchArgs args) {

		if (input == null)
			return null;

		if (! type.isCollectionOrArray())
			throw new FormattedRuntimeException("Cannot call filterCollection() on class type ''{0}''", type);

		// Create a new ObjectList
		ObjectList l = (ObjectList)replaceWithMutables(input);

		// Do the search
		CollectionFilter filter = new CollectionFilter(args.getSearch(), args.isIgnoreCase());
		filter.doQuery(l);

		// If sort or view isn't empty, then we need to make sure that all entries in the
		// list are maps.
		Map<String,Boolean> sort = args.getSort();
		List<String> view = args.getView();

		if ((! sort.isEmpty()) || (! view.isEmpty())) {
			if (! sort.isEmpty())
				doSort(l, sort);
			if (! view.isEmpty())
				doView(l, view);
		}

		// Do the paging.
		int pos = args.getPosition();
		int limit = args.getLimit();
		if (pos != 0 || limit != 0) {
			int end = (limit == 0 || limit+pos >= l.size()) ? l.size() : limit + pos;
			pos = Math.min(pos, l.size());
			ObjectList l2 = new DelegateList(((DelegateList)l).getClassMeta());
			l2.addAll(l.subList(pos, end));
			l = l2;
		}

		return l;
	}

	/*
	 * If there are any non-Maps in the specified list, replaces them with BeanMaps.
	 */
	private Object replaceWithMutables(Object o) {
		if (o == null)
			return null;
		ClassMeta cm = session.getClassMetaForObject(o);
		if (cm.isCollection()) {
			ObjectList l = new DelegateList(session.getClassMetaForObject(o));
			for (Object o2 : (Collection)o)
				l.add(replaceWithMutables(o2));
			return l;
		}
		if (cm.isMap() && o instanceof BeanMap) {
			BeanMap bm = (BeanMap)o;
			DelegateBeanMap dbm = new DelegateBeanMap(bm.getBean(), session);
			for (Object key : bm.keySet())
				dbm.addKey(key.toString());
			return dbm;
		}
		if (cm.isBean()) {
			BeanMap bm = session.toBeanMap(o);
			DelegateBeanMap dbm = new DelegateBeanMap(bm.getBean(), session);
			for (Object key : bm.keySet())
				dbm.addKey(key.toString());
			return dbm;
		}
		if (cm.isMap()) {
			Map m = (Map)o;
			DelegateMap dm = new DelegateMap(session.getClassMetaForObject(m));
			for (Map.Entry e : (Set<Map.Entry>)m.entrySet())
				dm.put(e.getKey().toString(), e.getValue());
			return dm;
		}
		if (cm.isArray()) {
			return replaceWithMutables(Arrays.asList((Object[])o));
		}
		return o;
	}

	/*
	 * Sorts the specified list by the sort list.
	 */
	private static void doSort(List list, Map<String,Boolean> sortList) {

		// We reverse the list and sort last to first.
		List<String> columns = new ArrayList<>(sortList.keySet());
		Collections.reverse(columns);

		for (final String c : columns) {
			final boolean isDesc = sortList.get(c);

			Comparator comp = new Comparator<Map>() {
				@Override /* Comparator */
				public int compare(Map m1, Map m2) {
					Comparable v1 = toComparable(m1.get(c)), v2 = toComparable(m2.get(c));
					if (v1 == null && v2 == null)
						return 0;
					if (v1 == null)
						return (isDesc ? -1 : 1);
					if (v2 == null)
						return (isDesc ? 1 : -1);
					return (isDesc ? v2.compareTo(v1) : v1.compareTo(v2));
				}
			};
			Collections.sort(list, comp);
		}
	}

	static final Comparable toComparable(Object o) {
		if (o == null)
			return null;
		if (o instanceof Comparable)
			return (Comparable)o;
		if (o instanceof Map)
			return ((Map)o).size();
		if (o.getClass().isArray())
			return Array.getLength(o);
		return o.toString();
	}

	/*
	 * Filters all but the specified view columns on all entries in the specified list.
	 */
	private static void doView(List list, List<String> view) {
		for (ListIterator i = list.listIterator(); i.hasNext();) {
			Object o = i.next();
			Map m = (Map)o;
			doView(m, view);
		}
	}

	/*
	 * Creates a new Map with only the entries specified in the view list.
	 */
	private static Map doView(Map m, List<String> view) {
		if (m instanceof DelegateMap)
			((DelegateMap)m).filterKeys(view);
		else
			((DelegateBeanMap)m).filterKeys(view);
		return m;
	}


	//====================================================================================================
	// CollectionFilter
	//====================================================================================================
	private class CollectionFilter {
		IMatcher entryMatcher;

		public CollectionFilter(Map query, boolean ignoreCase) {
			if (query != null && ! query.isEmpty())
				entryMatcher = new MapMatcher(query, ignoreCase);
		}

		public void doQuery(List in) {
			if (in == null || entryMatcher == null)
				return;
			for (Iterator i = in.iterator(); i.hasNext();) {
				Object o = i.next();
				if (! entryMatcher.matches(o))
					i.remove();
			}
		}
	}

	//====================================================================================================
	// IMatcher
	//====================================================================================================
	private interface IMatcher<E> {
		public boolean matches(E o);
	}

	//====================================================================================================
	// MapMatcher
	//====================================================================================================
	/*
	 * Matches on a Map only if all specified entry matchers match.
	 */
	private class MapMatcher implements IMatcher<Map> {

		Map<String,IMatcher> entryMatchers = new HashMap<>();

		public MapMatcher(Map query, boolean ignoreCase) {
			for (Map.Entry e : (Set<Map.Entry>)query.entrySet())
				if (e.getKey() != null && e.getValue() != null)
					entryMatchers.put(e.getKey().toString(), new ObjectMatcher(e.getValue().toString(), ignoreCase));
		}

		@Override /* IMatcher */
		public boolean matches(Map m) {
			if (m == null)
				return false;
			for (Map.Entry<String,IMatcher> e : entryMatchers.entrySet()) {
				String key = e.getKey();
				Object val = null;
				if (m instanceof BeanMap) {
					val = ((BeanMap)m).getRaw(key);
				} else {
					val = m.get(key);
				}
				if (! e.getValue().matches(val))
					return false;
			}
			return true;
		}
	}

	//====================================================================================================
	// ObjectMatcher
	//====================================================================================================
	/*
	 * Matcher that uses the correct matcher based on object type.
	 * Used for objects when we can't determine the object type beforehand.
	 */
	private class ObjectMatcher implements IMatcher<Object> {

		String searchPattern;
		boolean ignoreCase;
		DateMatcher dateMatcher;
		NumberMatcher numberMatcher;
		StringMatcher stringMatcher;

		ObjectMatcher(String searchPattern, boolean ignoreCase) {
			this.searchPattern = searchPattern;
			this.ignoreCase = ignoreCase;
		}

		@Override /* IMatcher */
		public boolean matches(Object o) {
			if (o instanceof Collection) {
				for (Object o2 : (Collection)o)
					if (matches(o2))
						return true;
				return false;
			}
			if (o != null && o.getClass().isArray()) {
				for (int i = 0; i < Array.getLength(o); i++)
					if (matches(Array.get(o, i)))
						return true;
				return false;
			}
			if (o instanceof Map) {
				for (Object o2 : ((Map)o).values())
					if (matches(o2))
						return true;
				return false;
			}
			if (o instanceof Number)
				return getNumberMatcher().matches(o);
			if (o instanceof Date || o instanceof Calendar)
				return getDateMatcher().matches(o);
			return getStringMatcher().matches(o);
		}

		private IMatcher getNumberMatcher() {
			if (numberMatcher == null)
				numberMatcher = new NumberMatcher(searchPattern);
			return numberMatcher;
		}

		private IMatcher getStringMatcher() {
			if (stringMatcher == null)
				stringMatcher = new StringMatcher(searchPattern, ignoreCase);
			return stringMatcher;
		}

		private IMatcher getDateMatcher() {
			if (dateMatcher == null)
				dateMatcher = new DateMatcher(searchPattern);
			return dateMatcher;
		}
	}

	//====================================================================================================
	// NumberMatcher
	//====================================================================================================
	private static class NumberMatcher implements IMatcher<Number> {

		private NumberPattern[] numberPatterns;

		/**
		 * Construct a number matcher for the given search pattern.
		 * 
		 * @param searchPattern A date search paattern.  See class usage for a description.
		 */
		public NumberMatcher(String searchPattern) {
			numberPatterns = new NumberPattern[1];
			numberPatterns[0] = new NumberPattern(searchPattern);

		}

		/**
		 * Returns 'true' if this integer matches the pattern(s).
		 */
		@Override /* IMatcher */
		public boolean matches(Number in) {
			for (int i = 0; i < numberPatterns.length; i++) {
				if (! numberPatterns[i].matches(in))
					return false;
			}
			return true;
		}

	}

	/**
	 * A construct representing a single search pattern.
	 */
	private static class NumberPattern {
		NumberRange[] numberRanges;

		public NumberPattern(String searchPattern) {

			List<NumberRange> l = new LinkedList<>();

			for (String s : breakUpTokens(searchPattern)) {
				boolean isNot = (s.charAt(0) == '!');
				String token = s.substring(1);
				Pattern p = Pattern.compile("(([<>]=?)?)(-?\\d+)(-?(-?\\d+)?)");

				// Possible patterns:
				// 123, >123, <123, >=123, <=123, >-123, >=-123, 123-456, -123--456
				// Regular expression used:  (([<>]=?)?)(-?\d+)(-??(-?\d+))
				Matcher m = p.matcher(token);

				// If a non-numeric value was passed in for a numeric value, just set the value to '0'.
				// (I think this might resolve a workaround in custom queries).
				if (! m.matches())
					throw new FormattedRuntimeException("Numeric value didn't match pattern:  ''{0}''", token);
					//m = numericPattern.matcher("0");

				String arg1 = m.group(1);
				String start = m.group(3);
				String end = m.group(5);

				l.add(new NumberRange(arg1, start, end, isNot));
			}

			numberRanges = l.toArray(new NumberRange[l.size()]);
		}

		private static List<String> breakUpTokens(String s) {
			// Get rid of whitespace in "123 - 456"
			s = s.replaceAll("(-?\\d+)\\s*-\\s*(-?\\d+)", "$1-$2");
			// Get rid of whitespace in ">= 123"
			s = s.replaceAll("([<>]=?)\\s+(-?\\d+)", "$1$2");
			// Get rid of whitespace in "! 123"
			s = s.replaceAll("(!)\\s+(-?\\d+)", "$1$2");

			// Replace all commas with whitespace
			// Allows for alternate notation of: 123,456...
			s = s.replaceAll(",", " ");

			String[] s2 = s.split("\\s+");

			// Make all tokens 'ORed'.  There is no way to AND numeric tokens.
			for (int i = 0; i < s2.length; i++)
				if (! startsWith(s2[i], '!'))
					s2[i] = "^"+s2[i];

			List<String> l = new LinkedList<>();
			l.addAll(Arrays.asList(s2));
			return l;
		}

		public boolean matches(Number number) {
			if (numberRanges.length == 0) return true;
			for (int i = 0; i < numberRanges.length; i++)
				if (numberRanges[i].matches(number))
					return true;
			return false;
		}
	}

	/**
	 * A construct representing a single search range in a single search pattern.
	 * All possible forms of search patterns are boiled down to these number ranges.
	 */
	private static class NumberRange {
		int start;
		int end;
		boolean isNot;

		public NumberRange(String arg, String start, String end, boolean isNot) {

			this.isNot = isNot;

			// 123, >123, <123, >=123, <=123, >-123, >=-123, 123-456, -123--456
			if (arg.equals("") && end == null) { // 123
				this.start = Integer.parseInt(start);
				this.end = this.start;
			} else if (arg.equals(">")) {
				this.start = Integer.parseInt(start)+1;
				this.end = Integer.MAX_VALUE;
			} else if (arg.equals(">=")) {
				this.start = Integer.parseInt(start);
				this.end = Integer.MAX_VALUE;
			} else if (arg.equals("<")) {
				this.start = Integer.MIN_VALUE;
				this.end = Integer.parseInt(start)-1;
			} else if (arg.equals("<=")) {
				this.start = Integer.MIN_VALUE;
				this.end = Integer.parseInt(start);
			} else {
				this.start = Integer.parseInt(start);
				this.end = Integer.parseInt(end);
			}
		}

		public boolean matches(Number n) {
			long i = n.longValue();
			boolean b = (i>=start && i<=end);
			if (isNot) b = !b;
			return b;
		}
	}

	//====================================================================================================
	// DateMatcher
	//====================================================================================================
	/** The list of all valid timestamp formats */
	private SimpleDateFormat[] validTimestampFormats = new SimpleDateFormat[0];
	{
		setValidTimestampFormats("yyyy.MM.dd.HH.mm.ss","yyyy.MM.dd.HH.mm","yyyy.MM.dd.HH","yyyy.MM.dd","yyyy.MM","yyyy");
	}

	/**
	 * Use this method to override the allowed search patterns when used in locales where time formats are different.
	 * 
	 * @param s A comma-delimited list of valid time formats.
	 */
	public void setValidTimestampFormats(String...s) {
		validTimestampFormats = new SimpleDateFormat[s.length];
		for (int i = 0; i < s.length; i++)
			validTimestampFormats[i] = new SimpleDateFormat(s[i]);
	}

	private class DateMatcher implements IMatcher<Object> {

		private TimestampPattern[] patterns;

		/**
		 * Construct a timestamp matcher for the given search pattern.
		 * 
		 * @param searchPattern The search pattern.
		 */
		DateMatcher(String searchPattern) {
			patterns = new TimestampPattern[1];
			patterns[0] = new TimestampPattern(searchPattern);

		}

		/**
		 * Returns <jk>true</jk> if the specified date matches the pattern passed in through the constructor.
		 * 
		 * <p>
		 * <br>The Object can be of type {@link Date} or {@link Calendar}.
		 * <br>Always returns <jk>false</jk> on <jk>null</jk> input.
		 */
		@Override /* IMatcher */
		public boolean matches(Object in) {
			if (in == null) return false;

			Calendar c = null;
			if (in instanceof Calendar)
				c = (Calendar)in;
			else if (in instanceof Date) {
				c = Calendar.getInstance();
				c.setTime((Date)in);
			} else {
				return false;
			}
			for (int i = 0; i < patterns.length; i++) {
				if (! patterns[i].matches(c))
					return false;
			}
			return true;
		}
	}

	/**
	 * A construct representing a single search pattern.
	 */
	private class TimestampPattern {
		TimestampRange[] ranges;
		List<TimestampRange> l = new LinkedList<>();

		public TimestampPattern(String s) {

			// Handle special case where timestamp is enclosed in quotes.
			// This can occur on hyperlinks created by group-by queries.
			// e.g. '2007/01/29 04:17:43 PM'
			if (s.charAt(0) == '\'' && s.charAt(s.length()-1) == '\'')
				s = s.substring(1, s.length()-1);

			// Pattern for finding <,>,<=,>=
			Pattern p1 = Pattern.compile("^\\s*([<>](?:=)?)\\s*(\\S+.*)$");
			// Pattern for finding range dash (e.g. xxx - yyy)
			Pattern p2 = Pattern.compile("^(\\s*-\\s*)(\\S+.*)$");

			// States are...
			// 1 - Looking for <,>,<=,>=
			// 2 - Looking for single date.
			// 3 - Looking for start date.
			// 4 - Looking for -
			// 5 - Looking for end date.
			int state = 1;

			String op = null;
			CalendarP startDate = null;

			ParsePosition pp = new ParsePosition(0);
			Matcher m = null;
			String seg = s;

			while (! seg.equals("") || state != 1) {
				if (state == 1) {
					m = p1.matcher(seg);
					if (m.matches()) {
						op = m.group(1);
						seg = m.group(2);
						state = 2;
					} else {
						state = 3;
					}
				} else if (state == 2) {
					l.add(new TimestampRange(op, parseDate(seg, pp)));
					//tokens.add("^"+op + parseTimestamp(seg, pp));
					seg = seg.substring(pp.getIndex()).trim();
					pp.setIndex(0);
					state = 1;
				} else if (state == 3) {
					startDate = parseDate(seg, pp);
					seg = seg.substring(pp.getIndex()).trim();
					pp.setIndex(0);
					state = 4;
				} else if (state == 4) {
					// Look for '-'
					m = p2.matcher(seg);
					if (m.matches()) {
						state = 5;
						seg = m.group(2);
					} else {
						// This is a single date (e.g. 2002/01/01)
						l.add(new TimestampRange(startDate));
						state = 1;
					}
				} else if (state == 5) {
					l.add(new TimestampRange(startDate, parseDate(seg, pp)));
					seg = seg.substring(pp.getIndex()).trim();
					pp.setIndex(0);
					state = 1;
				}
			}

			ranges = l.toArray(new TimestampRange[l.size()]);
		}

		public boolean matches(Calendar c) {
			if (ranges.length == 0) return true;
			for (int i = 0; i < ranges.length; i++)
				if (ranges[i].matches(c))
					return true;
			return false;
		}
	}

	/**
	 * A construct representing a single search range in a single search pattern.
	 * All possible forms of search patterns are boiled down to these timestamp ranges.
	 */
	private static class TimestampRange {
		Calendar start;
		Calendar end;

		public TimestampRange(CalendarP start, CalendarP end) {
			this.start = start.copy().roll(MILLISECOND, -1).getCalendar();
			this.end = end.roll(1).getCalendar();
		}

		public TimestampRange(CalendarP singleDate) {
			this.start = singleDate.copy().roll(MILLISECOND, -1).getCalendar();
			this.end = singleDate.roll(1).getCalendar();
		}

		public TimestampRange(String op, CalendarP singleDate) {
			if (op.equals(">")) {
				this.start = singleDate.roll(1).roll(MILLISECOND, -1).getCalendar();
				this.end = new CalendarP(new Date(Long.MAX_VALUE), 0).getCalendar();
			} else if (op.equals("<")) {
				this.start = new CalendarP(new Date(0), 0).getCalendar();
				this.end = singleDate.getCalendar();
			} else if (op.equals(">=")) {
				this.start = singleDate.roll(MILLISECOND, -1).getCalendar();
				this.end = new CalendarP(new Date(Long.MAX_VALUE), 0).getCalendar();
			} else if (op.equals("<=")) {
				this.start = new CalendarP(new Date(0), 0).getCalendar();
				this.end = singleDate.roll(1).getCalendar();
			}
		}

		public boolean matches(Calendar c) {
			boolean b = (c.after(start) && c.before(end));
			return b;
		}
	}

	private static int getPrecisionField(String pattern) {
		if (pattern.indexOf('s') != -1)
			return SECOND;
		if (pattern.indexOf('m') != -1)
			return MINUTE;
		if (pattern.indexOf('H') != -1)
			return HOUR_OF_DAY;
		if (pattern.indexOf('d') != -1)
			return DAY_OF_MONTH;
		if (pattern.indexOf('M') != -1)
			return MONTH;
		if (pattern.indexOf('y') != -1)
			return YEAR;
		return Calendar.MILLISECOND;
	}


	/**
	 * Parses a timestamp string off the beginning of the string segment 'seg'.
	 * Goes through each possible valid timestamp format until it finds a match.
	 * The position where the parsing left off is stored in pp.
	 * 
	 * @param seg The string segment being parsed.
	 * @param pp Where parsing last left off.
	 * @return An object representing a timestamp.
	 */
	CalendarP parseDate(String seg, ParsePosition pp) {

		CalendarP cal = null;

		for (int i = 0; i < validTimestampFormats.length && cal == null; i++) {
			pp.setIndex(0);
			SimpleDateFormat f = validTimestampFormats[i];
			Date d = f.parse(seg, pp);
			int idx = pp.getIndex();
			if (idx != 0) {
				// it only counts if the next character is '-', 'space', or end-of-string.
				char c = (seg.length() == idx ? 0 : seg.charAt(idx));
				if (c == 0 || c == '-' || Character.isWhitespace(c))
					cal = new CalendarP(d, getPrecisionField(f.toPattern()));
			}
		}

		if (cal == null)
			throw new FormattedRuntimeException("Invalid date encountered:  ''{0}''", seg);

		return cal;
	}

	/**
	 * Combines a Calendar with a precision identifier.
	 */
	private static class CalendarP {
		public Calendar c;
		public int precision;

		public CalendarP(Date date, int precision) {
			c = Calendar.getInstance();
			c.setTime(date);
			this.precision = precision;
		}

		public CalendarP copy() {
			return new CalendarP(c.getTime(), precision);
		}

		public CalendarP roll(int field, int amount) {
			c.add(field, amount);
			return this;
		}

		public CalendarP roll(int amount) {
			return roll(precision, amount);
		}

		public Calendar getCalendar() {
			return c;
		}
	}

	//====================================================================================================
	// StringMatcher
	//====================================================================================================
	private static class StringMatcher implements IMatcher<Object> {

		private SearchPattern[] searchPatterns;

		/**
		 * Construct a string matcher for the given search pattern.
		 * 
		 * @param searchPattern The search pattern.  See class usage for details.
		 * @param ignoreCase If <jk>true</jk>, use case-insensitive matching.
		 */
		public StringMatcher(String searchPattern, boolean ignoreCase) {
			this.searchPatterns = new SearchPattern[1];
			this.searchPatterns[0] = new SearchPattern(searchPattern, ignoreCase);
		}

		/**
		 * Returns 'true' if this string matches the pattern(s).
		 * Always returns false on null input.
		 */
		@Override /* IMatcher */
		public boolean matches(Object in) {
			if (in == null) return false;
			for (int i = 0; i < searchPatterns.length; i++) {
				if (! searchPatterns[i].matches(in.toString()))
					return false;
			}
			return true;
		}

	}
	/**
	 * A construct representing a single search pattern.
	 */
	private static class SearchPattern {
		Pattern[] orPatterns, andPatterns, notPatterns;

		public SearchPattern(String searchPattern, boolean ignoreCase) {

			List<Pattern> ors = new LinkedList<>();
			List<Pattern> ands = new LinkedList<>();
			List<Pattern> nots = new LinkedList<>();

			for (String arg : breakUpTokens(searchPattern)) {
				char prefix = arg.charAt(0);
				String token = arg.substring(1);

				token = token.replaceAll("([\\?\\*\\+\\\\\\[\\]\\{\\}\\(\\)\\^\\$\\.])", "\\\\$1");
				token = token.replace("\u9997", ".*");
				token = token.replace("\u9996", ".?");

				if (! token.startsWith(".*"))
					token = "^" + token;
				if (! token.endsWith(".*"))
					token = token + "$";

				int flags = Pattern.DOTALL;
				if (ignoreCase)
					flags |= Pattern.CASE_INSENSITIVE;

				Pattern p = Pattern.compile(token, flags);

				if (prefix == '^')
					ors.add(p);
				else if (prefix == '+')
					ands.add(p);
				else if (prefix == '-')
					nots.add(p);
			}
			orPatterns = ors.toArray(new Pattern[ors.size()]);
			andPatterns = ands.toArray(new Pattern[ands.size()]);
			notPatterns = nots.toArray(new Pattern[nots.size()]);
		}

		/**
		 * Break up search pattern into separate tokens.
		 */
		private static List<String> breakUpTokens(String s) {

			// If the string is null or all whitespace, return an empty vector.
			if (s == null || s.trim().length() == 0)
				return Collections.emptyList();

			// Pad with spaces.
			s = " " + s + " ";

			// Replace instances of [+] and [-] inside single and double quotes with
			// \u2001 and \u2002 for later replacement.
			int escapeCount = 0;
			boolean inSingleQuote = false;
			boolean inDoubleQuote = false;
			char[] ca = s.toCharArray();
			for (int i = 0; i < ca.length; i++) {
				if (ca[i] == '\\') escapeCount++;
				else if (escapeCount % 2 == 0) {
					if (ca[i] == '\'') inSingleQuote = ! inSingleQuote;
					else if (ca[i] == '"') inDoubleQuote = ! inDoubleQuote;
					else if (ca[i] == '+' && (inSingleQuote || inDoubleQuote)) ca[i] = '\u9999';
					else if (ca[i] == '-' && (inSingleQuote || inDoubleQuote)) ca[i] = '\u9998';
				}
				if (ca[i] != '\\') escapeCount = 0;
			}
			s = new String(ca);

			// Remove spaces between '+' or '-' and the keyword.
			//s = perl5Util.substitute("s/([\\+\\-])\\s+/$1/g", s);
			s = s.replaceAll("([\\+\\-])\\s+", "$1");

			// Replace:  [*]->[\u3001] as placeholder for '%', ignore escaped.
			s = replace(s, '*', '\u9997', true);
			// Replace:  [?]->[\u3002] as placeholder for '_', ignore escaped.
			s = replace(s, '?', '\u9996', true);
			// Replace:  [\*]->[*], [\?]->[?]
			s = unEscapeChars(s, new char[]{'*','?'});

			// Remove spaces
			s = s.trim();

			// Re-replace the [+] and [-] characters inside quotes.
			s = s.replace('\u9999', '+');
			s = s.replace('\u9998', '-');

			String[] sa = splitQuoted(s, ' ');
			List<String> l = new ArrayList<>(sa.length);
			int numOrs = 0;
			for (int i = 0; i < sa.length; i++) {
				String token = sa[i];
				int len = token.length();
				if (len > 0) {
					char c = token.charAt(0);
					String s2 = null;
					if ((c == '+' || c == '-') && len > 1)
						s2 = token.substring(1);
					else {
						s2 = token;
						c = '^';
						numOrs++;
					}
					// Trim off leading and trailing single and double quotes.
					if (s2.matches("\".*\"") || s2.matches("'.*'"))
						s2 = s2.substring(1, s2.length()-1);

					// Replace:  [\"]->["]
					s2 = unEscapeChars(s2, new char[]{'"','\''});

					// Un-escape remaining escaped backslashes.
					s2 = unEscapeChars(s2, new char[]{'\\'});

					l.add(c + s2);
				}
			}

			// If there's a single OR clause, turn it into an AND clause (makes the SQL cleaner).
			if (numOrs == 1) {
				int ii = l.size();
				for (int i = 0; i < ii; i++) {
					String x = l.get(i);
					if (x.charAt(0) == '^')
						l.set(i, '+'+x.substring(1));
				}
			}
			return l;
		}

		public boolean matches(String input) {
			if (input == null) return false;
			for (int i = 0; i < andPatterns.length; i++)
				if (! andPatterns[i].matcher(input).matches())
					return false;
			for (int i = 0; i < notPatterns.length; i++)
				if (notPatterns[i].matcher(input).matches())
					return false;
			for (int i = 0; i < orPatterns.length; i++)
				if (orPatterns[i].matcher(input).matches())
					return true;
			return orPatterns.length == 0;
		}

	}

	/*
	 * Same as split(String, char), but does not split on characters inside
	 * single quotes.
	 * Does not split on escaped delimiters, and escaped quotes are also ignored.
	 * Example:
	 * split("a,b,c",',') -> {"a","b","c"}
	 * split("a,'b,b,b',c",',') -> {"a","'b,b,b'","c"}
	 */
	static final String[] splitQuoted(String s, char c) {

		if (s == null || s.matches("\\s*"))
			return new String[0];

		List<String> l = new LinkedList<>();
		char[] sArray = s.toCharArray();
		int x1 = 0;
		int escapeCount = 0;
		boolean inSingleQuote = false;
		boolean inDoubleQuote = false;
		for (int i = 0; i < sArray.length; i++) {
			if (sArray[i] == '\\') escapeCount++;
			else if (escapeCount % 2 == 0) {
				if (sArray[i] == '\'' && ! inDoubleQuote) inSingleQuote = ! inSingleQuote;
				else if (sArray[i] == '"' && ! inSingleQuote) inDoubleQuote = ! inDoubleQuote;
				else if (sArray[i] == c && ! inSingleQuote && ! inDoubleQuote) {
					String s2 = new String(sArray, x1, i-x1).trim();
					l.add(s2);
					x1 = i+1;
				}
			}
			if (sArray[i] != '\\') escapeCount = 0;
		}
		String s2 = new String(sArray, x1, sArray.length-x1).trim();
		l.add(s2);

		return l.toArray(new String[l.size()]);
	}

	/**
	 * Replaces tokens in a string with a different token.
	 * 
	 * <p>
	 * replace("A and B and C", "and", "or") -> "A or B or C"
	 * replace("andandand", "and", "or") -> "ororor"
	 * replace(null, "and", "or") -> null
	 * replace("andandand", null, "or") -> "andandand"
	 * replace("andandand", "", "or") -> "andandand"
	 * replace("A and B and C", "and", null) -> "A  B  C"
	 * @param ignoreEscapedChars Specify 'true' if escaped 'from' characters should be ignored.
	 */
	static String replace(String s, char from, char to, boolean ignoreEscapedChars) {
		if (s == null) return null;

		char[] sArray = s.toCharArray();

		int escapeCount = 0;
		int singleQuoteCount = 0;
		int doubleQuoteCount = 0;
		for (int i = 0; i < sArray.length; i++) {
			char c = sArray[i];
			if (c == '\\' && ignoreEscapedChars)
				escapeCount++;
			else if (escapeCount % 2 == 0) {
				if (c == from && singleQuoteCount % 2 == 0 && doubleQuoteCount % 2 == 0)
				sArray[i] = to;
			}
			if (sArray[i] != '\\') escapeCount = 0;
		}
		return new String(sArray);
	}

	/**
	 * Removes escape characters (specified by escapeChar) from the specified characters.
	 */
	static String unEscapeChars(String s, char[] toEscape) {
		char escapeChar = '\\';
		if (s == null) return null;
		if (s.length() == 0) return s;
		StringBuffer sb = new StringBuffer(s.length());
		char[] sArray = s.toCharArray();
		for (int i = 0; i < sArray.length; i++) {
			char c = sArray[i];

			if (c == escapeChar) {
				if (i+1 != sArray.length) {
					char c2 = sArray[i+1];
					boolean isOneOf = false;
					for (int j = 0; j < toEscape.length && ! isOneOf; j++)
						isOneOf = (c2 == toEscape[j]);
					if (isOneOf) {
						i++;
					} else if (c2 == escapeChar) {
						sb.append(escapeChar);
						i++;
					}
				}
			}
			sb.append(sArray[i]);
		}
		return sb.toString();
	}
}


