/*
 * 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.commons.lang3.time;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.text.DateFormatSymbols;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.LocaleUtils;

/**
 * <p>
 * FastDateParser is a fast and thread-safe version of
 * {@link java.text.SimpleDateFormat}.
 * </p>
 *
 * <p>
 * To obtain a proxy to a FastDateParser, use
 * {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another
 * variation of the factory methods of {@link FastDateFormat}.
 * </p>
 *
 * <p>
 * Since FastDateParser is thread safe, you can use a static member instance:
 * </p>
 * <code>
 *     private static final DateParser DATE_PARSER = FastDateFormat.getInstance("yyyy-MM-dd");
 * </code>
 *
 * <p>
 * This class can be used as a direct replacement for {@code SimpleDateFormat}
 * in most parsing situations. This class is especially useful in multi-threaded
 * server environments. {@code SimpleDateFormat} is not thread-safe in any JDK
 * version, nor will it be as Sun has closed the <a href=
 * "http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4228335">bug</a>/RFE.
 * </p>
 *
 * <p>
 * Only parsing is supported by this class, but all patterns are compatible with
 * SimpleDateFormat.
 * </p>
 *
 * <p>
 * The class operates in lenient mode, so for example a time of 90 minutes is
 * treated as 1 hour 30 minutes.
 * </p>
 *
 * <p>
 * Timing tests indicate this class is as about as fast as SimpleDateFormat in
 * single thread applications and about 25% faster in multi-thread applications.
 * </p>
 *
 * @since 3.2
 * @see FastDatePrinter
 */
public class FastDateParser implements DateParser, Serializable {

	/**
	 * Required for serialization support.
	 *
	 * @see java.io.Serializable
	 */
	private static final long serialVersionUID = 3L;

	static final Locale JAPANESE_IMPERIAL = new Locale("ja", "JP", "JP");

	// defining fields
	private final String pattern;
	private final TimeZone timeZone;
	private final Locale locale;
	private final int century;
	private final int startYear;

	// derived fields
	private transient List<StrategyAndWidth> patterns;

	// comparator used to sort regex alternatives
	// alternatives should be ordered longer first, and shorter last. ('february'
	// before 'feb')
	// all entries must be lowercase by locale.
	private static final Comparator<String> LONGER_FIRST_LOWERCASE = Comparator.reverseOrder();

	/**
	 * <p>
	 * Constructs a new FastDateParser.
	 * </p>
	 *
	 * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another
	 * variation of the factory methods of {@link FastDateFormat} to get a cached
	 * FastDateParser instance.
	 *
	 * @param pattern  non-null {@link java.text.SimpleDateFormat} compatible
	 *                 pattern
	 * @param timeZone non-null time zone to use
	 * @param locale   non-null locale
	 */
	protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) {
		this(pattern, timeZone, locale, null);
	}

	/**
	 * <p>
	 * Constructs a new FastDateParser.
	 * </p>
	 *
	 * @param pattern      non-null {@link java.text.SimpleDateFormat} compatible
	 *                     pattern
	 * @param timeZone     non-null time zone to use
	 * @param locale       non-null locale
	 * @param centuryStart The start of the century for 2 digit year parsing
	 *
	 * @since 3.5
	 */
	protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale,
			final Date centuryStart) {
		this.pattern = pattern;
		this.timeZone = timeZone;
		this.locale = LocaleUtils.toLocale(locale);

		final Calendar definingCalendar = Calendar.getInstance(timeZone, this.locale);

		final int centuryStartYear;
		if (centuryStart != null) {
			definingCalendar.setTime(centuryStart);
			centuryStartYear = definingCalendar.get(Calendar.YEAR);
		} else if (this.locale.equals(JAPANESE_IMPERIAL)) {
			centuryStartYear = 0;
		} else {
			// from 80 years ago to 20 years from now
			definingCalendar.setTime(new Date());
			centuryStartYear = definingCalendar.get(Calendar.YEAR) - 80;
		}
		century = centuryStartYear / 100 * 100;
		startYear = centuryStartYear - century;

		init(definingCalendar);
	}

	/**
	 * Initializes derived fields from defining fields. This is called from
	 * constructor and from readObject (de-serialization)
	 *
	 * @param definingCalendar the {@link java.util.Calendar} instance used to
	 *                         initialize this FastDateParser
	 */
	private void init(final Calendar definingCalendar) {
		patterns = new ArrayList<>();

		final StrategyParser fm = new StrategyParser(definingCalendar);
		for (;;) {
			final StrategyAndWidth field = fm.getNextStrategy();
			if (field == null) {
				break;
			}
			patterns.add(field);
		}
	}

	// helper classes to parse the format string
	// -----------------------------------------------------------------------

	/**
	 * Holds strategy and field width
	 */
	private static class StrategyAndWidth {

		final Strategy strategy;
		final int width;

		StrategyAndWidth(final Strategy strategy, final int width) {
			this.strategy = strategy;
			this.width = width;
		}

		int getMaxWidth(final ListIterator<StrategyAndWidth> lt) {
			if (!strategy.isNumber() || !lt.hasNext()) {
				return 0;
			}
			final Strategy nextStrategy = lt.next().strategy;
			lt.previous();
			return nextStrategy.isNumber() ? width : 0;
		}

		@Override
		public String toString() {
			return "StrategyAndWidth [strategy=" + strategy + ", width=" + width + "]";
		}
	}

	/**
	 * Parse format into Strategies
	 */
	private class StrategyParser {
		private final Calendar definingCalendar;
		private int currentIdx;

		StrategyParser(final Calendar definingCalendar) {
			this.definingCalendar = definingCalendar;
		}

		StrategyAndWidth getNextStrategy() {
			if (currentIdx >= pattern.length()) {
				return null;
			}

			final char c = pattern.charAt(currentIdx);
			if (isFormatLetter(c)) {
				return letterPattern(c);
			}
			return literal();
		}

		private StrategyAndWidth letterPattern(final char c) {
			final int begin = currentIdx;
			while (++currentIdx < pattern.length()) {
				if (pattern.charAt(currentIdx) != c) {
					break;
				}
			}

			final int width = currentIdx - begin;
			return new StrategyAndWidth(getStrategy(c, width, definingCalendar), width);
		}

		private StrategyAndWidth literal() {
			boolean activeQuote = false;

			final StringBuilder sb = new StringBuilder();
			while (currentIdx < pattern.length()) {
				final char c = pattern.charAt(currentIdx);
				if (!activeQuote && isFormatLetter(c)) {
					break;
				} else if (c == '\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\'')) {
					activeQuote = !activeQuote;
					continue;
				}
				++currentIdx;
				sb.append(c);
			}

			if (activeQuote) {
				throw new IllegalArgumentException("Unterminated quote");
			}

			final String formatField = sb.toString();
			return new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length());
		}
	}

	private static boolean isFormatLetter(final char c) {
		return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z';
	}

	// Accessors
	// -----------------------------------------------------------------------
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.commons.lang3.time.DateParser#getPattern()
	 */
	@Override
	public String getPattern() {
		return pattern;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.commons.lang3.time.DateParser#getTimeZone()
	 */
	@Override
	public TimeZone getTimeZone() {
		return timeZone;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.commons.lang3.time.DateParser#getLocale()
	 */
	@Override
	public Locale getLocale() {
		return locale;
	}

	// Basics
	// -----------------------------------------------------------------------
	/**
	 * <p>
	 * Compares another object for equality with this object.
	 * </p>
	 *
	 * @param obj the object to compare to
	 * @return {@code true}if equal to this instance
	 */
	@Override
	public boolean equals(final Object obj) {
		if (!(obj instanceof FastDateParser)) {
			return false;
		}
		final FastDateParser other = (FastDateParser) obj;
		return pattern.equals(other.pattern) && timeZone.equals(other.timeZone) && locale.equals(other.locale);
	}

	/**
	 * <p>
	 * Returns a hash code compatible with equals.
	 * </p>
	 *
	 * @return a hash code compatible with equals
	 */
	@Override
	public int hashCode() {
		return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());
	}

	/**
	 * <p>
	 * Gets a string version of this formatter.
	 * </p>
	 *
	 * @return a debugging string
	 */
	@Override
	public String toString() {
		return "FastDateParser[" + pattern + ", " + locale + ", " + timeZone.getID() + "]";
	}

	/**
	 * Converts all state of this instance to a String handy for debugging.
	 *
	 * @return a string.
	 * @since 3.12.0
	 */
	public String toStringAll() {
		return "FastDateParser [pattern=" + pattern + ", timeZone=" + timeZone + ", locale=" + locale + ", century="
				+ century + ", startYear=" + startYear + ", patterns=" + patterns + "]";
	}

	// Serializing
	/**
	 * Creates the object after serialization. This implementation reinitializes the
	 * transient properties.
	 *
	 * @param in ObjectInputStream from which the object is being deserialized.
	 * @throws IOException            if there is an IO issue.
	 * @throws ClassNotFoundException if a class cannot be found.
	 */
	private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
		in.defaultReadObject();

		final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
		init(definingCalendar);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String)
	 */
	@Override
	public Object parseObject(final String source) throws ParseException {
		return parse(source);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String)
	 */
	@Override
	public Date parse(final String source) throws ParseException {
		final ParsePosition pp = new ParsePosition(0);
		final Date date = parse(source, pp);
		if (date == null) {
			// Add a note re supported date range
			if (locale.equals(JAPANESE_IMPERIAL)) {
				throw new ParseException("(The " + locale + " locale does not support dates before 1868 AD)\n"
						+ "Unparseable date: \"" + source, pp.getErrorIndex());
			}
			throw new ParseException("Unparseable date: " + source, pp.getErrorIndex());
		}
		return date;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String,
	 * java.text.ParsePosition)
	 */
	@Override
	public Object parseObject(final String source, final ParsePosition pos) {
		return parse(source, pos);
	}

	/**
	 * This implementation updates the ParsePosition if the parse succeeds. However,
	 * it sets the error index to the position before the failed field unlike the
	 * method {@link java.text.SimpleDateFormat#parse(String, ParsePosition)} which
	 * sets the error index to after the failed field.
	 * <p>
	 * To determine if the parse has succeeded, the caller must check if the current
	 * parse position given by {@link ParsePosition#getIndex()} has been updated. If
	 * the input buffer has been fully parsed, then the index will point to just
	 * after the end of the input buffer.
	 *
	 * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String,
	 *      java.text.ParsePosition)
	 */
	@Override
	public Date parse(final String source, final ParsePosition pos) {
		// timing tests indicate getting new instance is 19% faster than cloning
		final Calendar cal = Calendar.getInstance(timeZone, locale);
		cal.clear();

		return parse(source, pos, cal) ? cal.getTime() : null;
	}

	/**
	 * Parses a formatted date string according to the format. Updates the Calendar
	 * with parsed fields. Upon success, the ParsePosition index is updated to
	 * indicate how much of the source text was consumed. Not all source text needs
	 * to be consumed. Upon parse failure, ParsePosition error index is updated to
	 * the offset of the source text which does not match the supplied format.
	 *
	 * @param source   The text to parse.
	 * @param pos      On input, the position in the source to start parsing, on
	 *                 output, updated position.
	 * @param calendar The calendar into which to set parsed fields.
	 * @return true, if source has been parsed (pos parsePosition is updated);
	 *         otherwise false (and pos errorIndex is updated)
	 * @throws IllegalArgumentException when Calendar has been set to be not
	 *                                  lenient, and a parsed field is out of range.
	 */
	@Override
	public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {
		final ListIterator<StrategyAndWidth> lt = patterns.listIterator();
		while (lt.hasNext()) {
			final StrategyAndWidth strategyAndWidth = lt.next();
			final int maxWidth = strategyAndWidth.getMaxWidth(lt);
			if (!strategyAndWidth.strategy.parse(this, calendar, source, pos, maxWidth)) {
				return false;
			}
		}
		return true;
	}

	// Support for strategies
	// -----------------------------------------------------------------------

	private static StringBuilder simpleQuote(final StringBuilder sb, final String value) {
		for (int i = 0; i < value.length(); ++i) {
			final char c = value.charAt(i);
			switch (c) {
			case '\\':
			case '^':
			case '$':
			case '.':
			case '|':
			case '?':
			case '*':
			case '+':
			case '(':
			case ')':
			case '[':
			case '{':
				sb.append('\\');
			default:
				sb.append(c);
			}
		}
		if (sb.charAt(sb.length() - 1) == '.') {
			// trailing '.' is optional
			sb.append('?');
		}
		return sb;
	}

	/**
	 * Gets the short and long values displayed for a field
	 * 
	 * @param calendar The calendar to obtain the short and long values
	 * @param locale   The locale of display names
	 * @param field    The field of interest
	 * @param regex    The regular expression to build
	 * @return The map of string display names to field values
	 */
	private static Map<String, Integer> appendDisplayNames(final Calendar calendar, Locale locale, final int field,
			final StringBuilder regex) {
		final Map<String, Integer> values = new HashMap<>();
		locale = LocaleUtils.toLocale(locale);
		final Map<String, Integer> displayNames = calendar.getDisplayNames(field, Calendar.ALL_STYLES, locale);
		final TreeSet<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
		for (final Map.Entry<String, Integer> displayName : displayNames.entrySet()) {
			final String key = displayName.getKey().toLowerCase(locale);
			if (sorted.add(key)) {
				values.put(key, displayName.getValue());
			}
		}
		for (final String symbol : sorted) {
			simpleQuote(regex, symbol).append('|');
		}
		return values;
	}

	/**
	 * Adjusts dates to be within appropriate century
	 * 
	 * @param twoDigitYear The year to adjust
	 * @return A value between centuryStart(inclusive) to
	 *         centuryStart+100(exclusive)
	 */
	private int adjustYear(final int twoDigitYear) {
		final int trial = century + twoDigitYear;
		return twoDigitYear >= startYear ? trial : trial + 100;
	}

	/**
	 * A strategy to parse a single field from the parsing pattern
	 */
	private abstract static class Strategy {

		/**
		 * Is this field a number? The default implementation returns false.
		 *
		 * @return true, if field is a number
		 */
		boolean isNumber() {
			return false;
		}

		abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos,
				int maxWidth);
	}

	/**
	 * A strategy to parse a single field from the parsing pattern
	 */
	private abstract static class PatternStrategy extends Strategy {

		Pattern pattern;

		void createPattern(final StringBuilder regex) {
			createPattern(regex.toString());
		}

		void createPattern(final String regex) {
			this.pattern = Pattern.compile(regex);
		}

		/**
		 * Is this field a number? The default implementation returns false.
		 *
		 * @return true, if field is a number
		 */
		@Override
		boolean isNumber() {
			return false;
		}

		@Override
		boolean parse(final FastDateParser parser, final Calendar calendar, final String source,
				final ParsePosition pos, final int maxWidth) {
			final Matcher matcher = pattern.matcher(source.substring(pos.getIndex()));
			if (!matcher.lookingAt()) {
				pos.setErrorIndex(pos.getIndex());
				return false;
			}
			pos.setIndex(pos.getIndex() + matcher.end(1));
			setCalendar(parser, calendar, matcher.group(1));
			return true;
		}

		abstract void setCalendar(FastDateParser parser, Calendar calendar, String value);

		/**
		 * Converts this instance to a handy debug string.
		 *
		 * @since 3.12.0
		 */
		@Override
		public String toString() {
			return getClass().getSimpleName() + " [pattern=" + pattern + "]";
		}

	}

	/**
	 * Gets a Strategy given a field from a SimpleDateFormat pattern
	 * 
	 * @param f                A sub-sequence of the SimpleDateFormat pattern
	 * @param definingCalendar The calendar to obtain the short and long values
	 * @return The Strategy that will handle parsing for the field
	 */
	private Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) {
		switch (f) {
		default:
			throw new IllegalArgumentException("Format '" + f + "' not supported");
		case 'D':
			return DAY_OF_YEAR_STRATEGY;
		case 'E':
			return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar);
		case 'F':
			return DAY_OF_WEEK_IN_MONTH_STRATEGY;
		case 'G':
			return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar);
		case 'H': // Hour in day (0-23)
			return HOUR_OF_DAY_STRATEGY;
		case 'K': // Hour in am/pm (0-11)
			return HOUR_STRATEGY;
		case 'M':
			return width >= 3 ? getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) : NUMBER_MONTH_STRATEGY;
		case 'S':
			return MILLISECOND_STRATEGY;
		case 'W':
			return WEEK_OF_MONTH_STRATEGY;
		case 'a':
			return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar);
		case 'd':
			return DAY_OF_MONTH_STRATEGY;
		case 'h': // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0
			return HOUR12_STRATEGY;
		case 'k': // Hour in day (1-24), i.e. midnight is 24, not 0
			return HOUR24_OF_DAY_STRATEGY;
		case 'm':
			return MINUTE_STRATEGY;
		case 's':
			return SECOND_STRATEGY;
		case 'u':
			return DAY_OF_WEEK_STRATEGY;
		case 'w':
			return WEEK_OF_YEAR_STRATEGY;
		case 'y':
		case 'Y':
			return width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY;
		case 'X':
			return ISO8601TimeZoneStrategy.getStrategy(width);
		case 'Z':
			if (width == 2) {
				return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY;
			}
			//$FALL-THROUGH$
		case 'z':
			return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar);
		}
	}

	@SuppressWarnings("unchecked") // OK because we are creating an array with no entries
	private static final Map<Locale, Strategy>[] caches = new Map[Calendar.FIELD_COUNT];

	/**
	 * Gets a cache of Strategies for a particular field
	 * 
	 * @param field The Calendar field
	 * @return a cache of Locale to Strategy
	 */
	private static Map<Locale, Strategy> getCache(final int field) {
		synchronized (caches) {
			if (caches[field] == null) {
				caches[field] = new HashMap<>(3);
			}
			return caches[field];
		}
	}

	/**
	 * Constructs a Strategy that parses a Text field
	 * 
	 * @param field            The Calendar field
	 * @param definingCalendar The calendar to obtain the short and long values
	 * @return a TextStrategy for the field and Locale
	 */
	private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) {
		final Map<Locale, Strategy> cache = getCache(field);
		Strategy strategy = cache.get(locale);
		if (strategy == null) {
			strategy = field == Calendar.ZONE_OFFSET ? new TimeZoneStrategy(locale)
					: new CaseInsensitiveTextStrategy(field, definingCalendar, locale);
			final Strategy inCache = cache.putIfAbsent(locale, strategy);
			if (inCache != null) {
				return inCache;
			}
		}
		return strategy;
	}

	/**
	 * A strategy that copies the static or quoted field in the parsing pattern
	 */
	private static class CopyQuotedStrategy extends Strategy {

		private final String formatField;

		/**
		 * Constructs a Strategy that ensures the formatField has literal text
		 *
		 * @param formatField The literal text to match
		 */
		CopyQuotedStrategy(final String formatField) {
			this.formatField = formatField;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		boolean isNumber() {
			return false;
		}

		@Override
		boolean parse(final FastDateParser parser, final Calendar calendar, final String source,
				final ParsePosition pos, final int maxWidth) {
			for (int idx = 0; idx < formatField.length(); ++idx) {
				final int sIdx = idx + pos.getIndex();
				if (sIdx == source.length()) {
					pos.setErrorIndex(sIdx);
					return false;
				}
				if (formatField.charAt(idx) != source.charAt(sIdx)) {
					pos.setErrorIndex(sIdx);
					return false;
				}
			}
			pos.setIndex(formatField.length() + pos.getIndex());
			return true;
		}

		/**
		 * Converts this instance to a handy debug string.
		 *
		 * @since 3.12.0
		 */
		@Override
		public String toString() {
			return "CopyQuotedStrategy [formatField=" + formatField + "]";
		}
	}

	/**
	 * A strategy that handles a text field in the parsing pattern
	 */
	private static class CaseInsensitiveTextStrategy extends PatternStrategy {
		private final int field;
		final Locale locale;
		private final Map<String, Integer> lKeyValues;

		/**
		 * Constructs a Strategy that parses a Text field
		 *
		 * @param field            The Calendar field
		 * @param definingCalendar The Calendar to use
		 * @param locale           The Locale to use
		 */
		CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) {
			this.field = field;
			this.locale = LocaleUtils.toLocale(locale);

			final StringBuilder regex = new StringBuilder();
			regex.append("((?iu)");
			lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex);
			regex.setLength(regex.length() - 1);
			regex.append(")");
			createPattern(regex);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) {
			final String lowerCase = value.toLowerCase(locale);
			Integer iVal = lKeyValues.get(lowerCase);
			if (iVal == null) {
				// match missing the optional trailing period
				iVal = lKeyValues.get(lowerCase + '.');
			}
			calendar.set(field, iVal.intValue());
		}

		/**
		 * Converts this instance to a handy debug string.
		 *
		 * @since 3.12.0
		 */
		@Override
		public String toString() {
			return "CaseInsensitiveTextStrategy [field=" + field + ", locale=" + locale + ", lKeyValues=" + lKeyValues
					+ ", pattern=" + pattern + "]";
		}
	}

	/**
	 * A strategy that handles a number field in the parsing pattern
	 */
	private static class NumberStrategy extends Strategy {

		private final int field;

		/**
		 * Constructs a Strategy that parses a Number field
		 *
		 * @param field The Calendar field
		 */
		NumberStrategy(final int field) {
			this.field = field;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		boolean isNumber() {
			return true;
		}

		@Override
		boolean parse(final FastDateParser parser, final Calendar calendar, final String source,
				final ParsePosition pos, final int maxWidth) {
			int idx = pos.getIndex();
			int last = source.length();

			if (maxWidth == 0) {
				// if no maxWidth, strip leading white space
				for (; idx < last; ++idx) {
					final char c = source.charAt(idx);
					if (!Character.isWhitespace(c)) {
						break;
					}
				}
				pos.setIndex(idx);
			} else {
				final int end = idx + maxWidth;
				if (last > end) {
					last = end;
				}
			}

			for (; idx < last; ++idx) {
				final char c = source.charAt(idx);
				if (!Character.isDigit(c)) {
					break;
				}
			}

			if (pos.getIndex() == idx) {
				pos.setErrorIndex(idx);
				return false;
			}

			final int value = Integer.parseInt(source.substring(pos.getIndex(), idx));
			pos.setIndex(idx);

			calendar.set(field, modify(parser, value));
			return true;
		}

		/**
		 * Make any modifications to parsed integer
		 *
		 * @param parser The parser
		 * @param iValue The parsed integer
		 * @return The modified value
		 */
		int modify(final FastDateParser parser, final int iValue) {
			return iValue;
		}

		/**
		 * Converts this instance to a handy debug string.
		 *
		 * @since 3.12.0
		 */
		@Override
		public String toString() {
			return "NumberStrategy [field=" + field + "]";
		}
	}

	private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) {
		/**
		 * {@inheritDoc}
		 */
		@Override
		int modify(final FastDateParser parser, final int iValue) {
			return iValue < 100 ? parser.adjustYear(iValue) : iValue;
		}
	};

	/**
	 * A strategy that handles a time zone field in the parsing pattern
	 */
	static class TimeZoneStrategy extends PatternStrategy {
		private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}";
		private static final String GMT_OPTION = TimeZones.GMT_ID + "[+-]\\d{1,2}:\\d{2}";

		private final Locale locale;
		private final Map<String, TzInfo> tzNames = new HashMap<>();

		private static class TzInfo {
			final TimeZone zone;
			final int dstOffset;

			TzInfo(final TimeZone tz, final boolean useDst) {
				zone = tz;
				dstOffset = useDst ? tz.getDSTSavings() : 0;
			}
		}

		/**
		 * Index of zone id
		 */
		private static final int ID = 0;

		/**
		 * Constructs a Strategy that parses a TimeZone
		 *
		 * @param locale The Locale
		 */
		TimeZoneStrategy(final Locale locale) {
			this.locale = LocaleUtils.toLocale(locale);

			final StringBuilder sb = new StringBuilder();
			sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION);

			final Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);

			final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings();
			for (final String[] zoneNames : zones) {
				// offset 0 is the time zone ID and is not localized
				final String tzId = zoneNames[ID];
				if (tzId.equalsIgnoreCase(TimeZones.GMT_ID)) {
					continue;
				}
				final TimeZone tz = TimeZone.getTimeZone(tzId);
				// offset 1 is long standard name
				// offset 2 is short standard name
				final TzInfo standard = new TzInfo(tz, false);
				TzInfo tzInfo = standard;
				for (int i = 1; i < zoneNames.length; ++i) {
					switch (i) {
					case 3: // offset 3 is long daylight savings (or summertime) name
							// offset 4 is the short summertime name
						tzInfo = new TzInfo(tz, true);
						break;
					case 5: // offset 5 starts additional names, probably standard time
						tzInfo = standard;
						break;
					default:
						break;
					}
					if (zoneNames[i] != null) {
						final String key = zoneNames[i].toLowerCase(locale);
						// ignore the data associated with duplicates supplied in
						// the additional names
						if (sorted.add(key)) {
							tzNames.put(key, tzInfo);
						}
					}
				}
			}
			// order the regex alternatives with longer strings first, greedy
			// match will ensure longest string will be consumed
			for (final String zoneName : sorted) {
				simpleQuote(sb.append('|'), zoneName);
			}
			sb.append(")");
			createPattern(sb);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		void setCalendar(final FastDateParser parser, final Calendar calendar, final String timeZone) {
			final TimeZone tz = FastTimeZone.getGmtTimeZone(timeZone);
			if (tz != null) {
				calendar.setTimeZone(tz);
			} else {
				final String lowerCase = timeZone.toLowerCase(locale);
				TzInfo tzInfo = tzNames.get(lowerCase);
				if (tzInfo == null) {
					// match missing the optional trailing period
					tzInfo = tzNames.get(lowerCase + '.');
				}
				calendar.set(Calendar.DST_OFFSET, tzInfo.dstOffset);
				calendar.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset());
			}
		}

		/**
		 * Converts this instance to a handy debug string.
		 *
		 * @since 3.12.0
		 */
		@Override
		public String toString() {
			return "TimeZoneStrategy [locale=" + locale + ", tzNames=" + tzNames + ", pattern=" + pattern + "]";
		}

	}

	private static class ISO8601TimeZoneStrategy extends PatternStrategy {
		// Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm

		/**
		 * Constructs a Strategy that parses a TimeZone
		 * 
		 * @param pattern The Pattern
		 */
		ISO8601TimeZoneStrategy(final String pattern) {
			createPattern(pattern);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) {
			calendar.setTimeZone(FastTimeZone.getGmtTimeZone(value));
		}

		private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))");
		private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))");
		private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy(
				"(Z|(?:[+-]\\d{2}(?::)\\d{2}))");

		/**
		 * Factory method for ISO8601TimeZoneStrategies.
		 *
		 * @param tokenLen a token indicating the length of the TimeZone String to be
		 *                 formatted.
		 * @return a ISO8601TimeZoneStrategy that can format TimeZone String of length
		 *         {@code tokenLen}. If no such strategy exists, an
		 *         IllegalArgumentException will be thrown.
		 */
		static Strategy getStrategy(final int tokenLen) {
			switch (tokenLen) {
			case 1:
				return ISO_8601_1_STRATEGY;
			case 2:
				return ISO_8601_2_STRATEGY;
			case 3:
				return ISO_8601_3_STRATEGY;
			default:
				throw new IllegalArgumentException("invalid number of X");
			}
		}
	}

	private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) {
		@Override
		int modify(final FastDateParser parser, final int iValue) {
			return iValue - 1;
		}
	};

	private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR);
	private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR);
	private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH);
	private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR);
	private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH);
	private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK) {
		@Override
		int modify(final FastDateParser parser, final int iValue) {
			return iValue == 7 ? Calendar.SUNDAY : iValue + 1;
		}
	};

	private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);
	private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY);
	private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) {
		@Override
		int modify(final FastDateParser parser, final int iValue) {
			return iValue == 24 ? 0 : iValue;
		}
	};

	private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) {
		@Override
		int modify(final FastDateParser parser, final int iValue) {
			return iValue == 12 ? 0 : iValue;
		}
	};

	private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR);
	private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE);
	private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND);
	private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND);
}
