/*******************************************************************************
 * Copyright (c) 2009, 2017 Cloudsmith Inc. and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Cloudsmith Inc. - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.metadata;

import java.io.Serializable;
import java.util.*;
import org.eclipse.equinox.internal.p2.metadata.EnumDefinition.EnumSegment;
import org.eclipse.equinox.internal.p2.metadata.VersionFormat.TreeInfo;
import org.eclipse.equinox.p2.metadata.VersionFormatException;
import org.eclipse.osgi.util.NLS;

/**
 * This is the Omni Version Format parser. It will parse a version format in string form
 * into a group of {@link VersionFormatParser.Fragment} elements. That group, wrapped in a
 * {@link VersionFormat}, becomes the parser for versions corresponding to the format.
 *
 * The class is not intended to included in a public API. Instead VersionFormats should
 * be created using {@link VersionFormat#parse(String)}
 *
 */
class VersionFormatParser {

	private static class EnumInstruction {
		private final EnumDefinition definition;
		private final boolean caseSensitive;
		private final boolean optional;
		private final boolean begins;

		EnumInstruction(EnumDefinition definition, boolean caseSensitive, boolean optional, boolean begins) {
			this.definition = definition;
			this.caseSensitive = caseSensitive;
			this.optional = optional;
			this.begins = begins;
		}

		EnumSegment getEnumSegment(RangeFragment fragment, String version, int[] posHolder, int maxPos) {
			int pos = posHolder[0];
			int len = maxPos - pos;
			int minLen = definition.getShortestLength();
			if (minLen > len)
				return null;

			int maxLen = definition.getLongestLength();
			if (maxLen < len)
				len = maxLen;

			++len;
			while (--len >= minLen) {
				int last = pos + len;
				if (!begins && last < maxPos) {
					char c = version.charAt(last);
					if (VersionParser.isLetter(c) && fragment.isAllowed(c)) {
						// We are not allowed to truncate at this point
						continue;
					}
				}
				String identifier = version.substring(pos, last);
				if (!caseSensitive)
					identifier = identifier.toLowerCase();
				int ordinal = definition.getOrdinal(identifier);
				if (ordinal >= 0) {
					posHolder[0] = pos + len;
					return definition.getSegment(ordinal);
				}
			}
			return null;
		}

		void toString(StringBuffer bld) {
			bld.append('=');
			definition.toString(bld);
			if (begins)
				bld.append('b');
			if (!caseSensitive)
				bld.append('i');
			if (optional)
				bld.append('?');
			bld.append(';');
		}

		public boolean isOptional() {
			return optional;
		}
	}

	static class Instructions {
		char[] characters = null;
		Comparable<?> defaultValue = null;
		char oppositeTranslationChar = 0;
		int oppositeTranslationRepeat = 0;
		boolean ignore = false;
		boolean inverted = false;
		Comparable<?> padValue = null;
		int rangeMax = Integer.MAX_VALUE;
		int rangeMin = 0;
		EnumInstruction enumInstruction = null;
	}

	static final Qualifier EXACT_ONE_QUALIFIER = new Qualifier(1, 1);

	static final Qualifier ONE_OR_MANY_QUALIFIER = new Qualifier(1, Integer.MAX_VALUE);

	static final Qualifier ZERO_OR_MANY_QUALIFIER = new Qualifier(0, Integer.MAX_VALUE);

	static final Qualifier ZERO_OR_ONE_QUALIFIER = new Qualifier(0, 1);

	/**
	 * Represents one fragment of a format (i.e. auto, number, string, delimiter, etc.)
	 */
	static abstract class Fragment implements Serializable {
		private static final long serialVersionUID = 4109185333058622681L;

		private final Qualifier qualifier;

		Fragment(Qualifier qualifier) {
			this.qualifier = qualifier;
		}

		@Override
		public final boolean equals(Object f) {
			return f == this || getClass().equals(f.getClass()) && qualifier.equals(((Fragment) f).qualifier);
		}

		@Override
		public final int hashCode() {
			return 11 * qualifier.hashCode();
		}

		public boolean isGroup() {
			return false;
		}

		@Override
		public String toString() {
			StringBuffer sb = new StringBuffer();
			toString(sb);
			return sb.toString();
		}

		Comparable<?> getDefaultValue() {
			return null;
		}

		Fragment getFirstLeaf() {
			return this;
		}

		Comparable<?> getPadValue() {
			return null;
		}

		Qualifier getQualifier() {
			return qualifier;
		}

		boolean parse(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
			return qualifier.parse(new Fragment[] {this}, 0, segments, version, maxPos, info);
		}

		abstract boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info);

		void setDefaults(List<Comparable<?>> segments) {
			// No-op at this level
		}

		void toString(StringBuffer sb) {
			if (!(qualifier == VersionFormatParser.EXACT_ONE_QUALIFIER || (qualifier == VersionFormatParser.ZERO_OR_ONE_QUALIFIER && this.isGroup())))
				qualifier.toString(sb);
		}
	}

	/**
	 * Specifies the min and max occurrences of a fragment
	 */
	static class Qualifier implements Serializable {
		private static final long serialVersionUID = 7494021832824671685L;

		private final int max;
		private final int min;

		Qualifier(int min, int max) {
			this.min = min;
			this.max = max;
		}

		@Override
		public boolean equals(Object o) {
			if (o == this)
				return true;
			if (!(o instanceof Qualifier))
				return false;
			Qualifier oq = (Qualifier) o;
			return min == oq.min && max == oq.max;
		}

		@Override
		public int hashCode() {
			return 31 * min + 67 * max;
		}

		@Override
		public String toString() {
			StringBuffer sb = new StringBuffer();
			toString(sb);
			return sb.toString();
		}

		int getMax() {
			return max;
		}

		int getMin() {
			return min;
		}

		boolean parse(Fragment[] fragments, int fragIdx, List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
			Fragment fragment = fragments[fragIdx++];
			int idx = 0;

			// Do the required parsing. I.e. iterate this fragment
			// min number of times.
			//
			for (; idx < min; ++idx)
				if (!fragment.parseOne(segments, version, maxPos, info))
					return false;

			for (; idx < max; ++idx) {
				// We are greedy. Continue parsing until we get an exception
				// and remember the state before each parse is performed.
				//
				info.pushState(segments.size(), fragment);
				if (!fragment.parseOne(segments, version, maxPos, info)) {
					info.popState(segments, fragment);
					break;
				}
			}
			int maxParsed = idx;

			for (;;) {
				// Pad with default values unless the max is unbounded
				//
				if (idx < max) {
					if (max != Integer.MAX_VALUE) {
						for (; idx < max; ++idx)
							fragment.setDefaults(segments);
					}
				} else {
					if (fragment instanceof StringFragment) {
						// Check for translations if we default to for MINS or MAXS
						StringFragment stringFrag = (StringFragment) fragment;
						Comparable<?> opposite = stringFrag.getOppositeDefaultValue();
						if (opposite != null) {
							idx = segments.size() - 1;
							if (stringFrag.isOppositeTranslation(segments.get(idx)))
								segments.set(idx, opposite);
						}
					}
				}

				if (fragIdx == fragments.length)
					// We are the last segment
					//
					return true;

				// Try to parse the next segment. If it fails, pop the state of
				// this segment (or a child thereof) and try again
				//
				if (fragments[fragIdx].getQualifier().parse(fragments, fragIdx, segments, version, maxPos, info))
					return true;

				// Be less greedy, step back one position and try again.
				//
				if (maxParsed <= min)
					// We have no more states to pop. Tell previous that we failed.
					//
					return false;

				info.popState(segments, fragment);
				idx = --maxParsed; // segments now have room for one more default value
			}
		}

		void toString(StringBuffer sb) {
			if (min == 0) {
				switch (max) {
					case 1:
						sb.append('?');
						break;
					case Integer.MAX_VALUE:
						sb.append('*');
						break;
					default:
						sb.append('{');
						sb.append(min);
						sb.append(',');
						sb.append(max);
						sb.append('}');
						break;
				}
			} else if (max == Integer.MAX_VALUE) {
				if (min == 1)
					sb.append('+');
				else {
					sb.append('{');
					sb.append(min);
					sb.append(",}"); //$NON-NLS-1$
				}
			} else {
				sb.append('{');
				sb.append(min);
				if (min != max) {
					sb.append(',');
					sb.append(max);
				}
				sb.append('}');
			}
		}

		// Preserve singleton when deserialized
		private Object readResolve() {
			Qualifier q = this;
			if (min == 0) {
				if (max == 1)
					q = VersionFormatParser.ZERO_OR_ONE_QUALIFIER;
				else if (max == Integer.MAX_VALUE)
					q = VersionFormatParser.ZERO_OR_MANY_QUALIFIER;
			} else if (min == 1) {
				if (max == 1)
					q = VersionFormatParser.EXACT_ONE_QUALIFIER;
				else if (max == Integer.MAX_VALUE)
					q = VersionFormatParser.ONE_OR_MANY_QUALIFIER;
			}
			return q;
		}
	}

	private static class AutoFragment extends RangeFragment {
		private static final long serialVersionUID = -1016534328164247755L;

		AutoFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
			super(instr, qualifier);
		}

		@Override
		boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
			int pos = info.getPosition();
			maxPos = checkRange(pos, maxPos);
			if (maxPos < 0)
				return false;

			char c = version.charAt(pos);
			if (VersionParser.isDigit(c) && isAllowed(c) && (enumInstruction == null || enumInstruction.isOptional())) {
				// Parse to next non-digit
				//
				int start = pos;
				int value = c - '0';
				while (++pos < maxPos) {
					c = version.charAt(pos);
					if (!(VersionParser.isDigit(c) && isAllowed(c)))
						break;
					value *= 10;
					value += (c - '0');
				}
				int len = pos - start;
				if (rangeMin > len || len > rangeMax)
					return false;

				if (!isIgnored())
					segments.add(VersionParser.valueOf(value));
				info.setPosition(pos);
				return true;
			}

			int start = pos;
			if (enumInstruction != null) {
				int[] posHolder = new int[] {pos};
				EnumSegment es = enumInstruction.getEnumSegment(this, version, posHolder, maxPos);
				if (es != null) {
					pos = posHolder[0];
					int len = pos - start;
					if (rangeMin > len || len > rangeMax)
						return false;
					if (!isIgnored())
						segments.add(es);
					info.setPosition(pos);
					return true;
				}
				if (!enumInstruction.isOptional())
					return false;
			}

			if (!(VersionParser.isLetter(c) && isAllowed(c)))
				return false;

			// Parse to next non-letter or next delimiter
			//
			for (++pos; pos < maxPos; ++pos) {
				c = version.charAt(pos);
				if (!(VersionParser.isLetter(c) && isAllowed(c)))
					break;
			}
			int len = pos - start;
			if (rangeMin > len || len > rangeMax)
				return false;

			if (!isIgnored())
				segments.add(version.substring(start, pos));
			info.setPosition(pos);
			return true;
		}

		@Override
		void toString(StringBuffer sb) {
			sb.append('a');
			super.toString(sb);
		}
	}

	private static class DelimiterFragment extends Fragment {
		private static final long serialVersionUID = 8173654376143370605L;
		private final char[] delimChars;
		private final boolean inverted;

		DelimiterFragment(VersionFormatParser.Instructions ep, Qualifier qualifier) {
			super(qualifier);
			if (ep == null) {
				delimChars = null;
				inverted = false;
			} else {
				inverted = ep.inverted;
				delimChars = ep.characters;
			}
		}

		boolean isMatch(String version, int pos) {
			char c = version.charAt(pos);
			if (delimChars != null) {
				for (char delimChar : delimChars)
					if (c == delimChar)
						return !inverted;
				return inverted;
			} else if (VersionParser.isLetterOrDigit(c))
				return false;

			return true;
		}

		@Override
		boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
			int pos = info.getPosition();
			if (pos < maxPos && isMatch(version, pos)) {
				// Just swallow, a delimiter does not contribute to the vector.
				//
				info.setPosition(pos + 1);
				return true;
			}
			return false;
		}

		@Override
		void toString(StringBuffer sb) {
			sb.append('d');
			if (delimChars != null)
				appendCharacterRange(sb, delimChars, inverted);
			super.toString(sb);
		}
	}

	static void appendCharacterRange(StringBuffer sb, char[] range, boolean inverted) {
		sb.append('=');
		sb.append('[');
		if (inverted)
			sb.append('^');
		int top = range.length;
		for (int idx = 0; idx < top; ++idx) {
			char b = range[idx];
			if (b == '\\' || b == ']' || (b == '-' && idx + 1 < top))
				sb.append('\\');

			sb.append(b);
			int ndx = idx + 1;
			if (ndx + 2 < top) {
				char c = b;
				for (; ndx < top; ++ndx) {
					char n = range[ndx];
					if (c + 1 != n)
						break;
					c = n;
				}
				if (ndx <= idx + 3)
					continue;

				sb.append('-');
				if (c == '\\' || c == ']' || (c == '-' && idx + 1 < top))
					sb.append('\\');
				sb.append(c);
				idx = ndx - 1;
			}
		}
		sb.append(']');
		sb.append(';');
	}

	static Fragment createAutoFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
		return new AutoFragment(instr, qualifier);
	}

	static Fragment createDelimiterFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
		return new DelimiterFragment(instr, qualifier);
	}

	static Fragment createGroupFragment(VersionFormatParser.Instructions instr, Qualifier qualifier, Fragment[] fragments, boolean array) {
		return new GroupFragment(instr, qualifier, fragments, array);
	}

	static Fragment createLiteralFragment(Qualifier qualifier, String literal) {
		return new LiteralFragment(qualifier, literal);
	}

	static Fragment createNumberFragment(VersionFormatParser.Instructions instr, Qualifier qualifier, boolean signed) {
		return new NumberFragment(instr, qualifier, signed);
	}

	static Fragment createPadFragment(Qualifier qualifier) {
		return new PadFragment(qualifier);
	}

	static Fragment createQuotedFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
		return new QuotedFragment(instr, qualifier);
	}

	static Fragment createRawFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
		return new RawFragment(instr, qualifier);
	}

	static Fragment createStringFragment(VersionFormatParser.Instructions instr, Qualifier qualifier, boolean unbound) {
		return new StringFragment(instr, qualifier, unbound);
	}

	static boolean equalsAllowNull(Object a, Object b) {
		return (a == null) ? (b == null) : (b != null && a.equals(b));
	}

	private static abstract class ElementFragment extends Fragment {
		private static final long serialVersionUID = -6834591415456539713L;
		private final Comparable<?> defaultValue;
		private final boolean ignored;
		private final Comparable<?> padValue;

		ElementFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
			super(qualifier);
			if (instr != null) {
				ignored = instr.ignore;
				defaultValue = instr.defaultValue;
				padValue = instr.padValue;
			} else {
				ignored = false;
				defaultValue = null;
				padValue = null;
			}
		}

		@Override
		Comparable<?> getDefaultValue() {
			return defaultValue;
		}

		@Override
		Comparable<?> getPadValue() {
			return padValue;
		}

		boolean isIgnored() {
			return ignored;
		}

		@Override
		void setDefaults(List<Comparable<?>> segments) {
			Comparable<?> defaultVal = getDefaultValue();
			if (defaultVal != null)
				segments.add(defaultVal);
		}

		@Override
		void toString(StringBuffer sb) {
			if (ignored) {
				sb.append('=');
				sb.append('!');
				sb.append(';');
			}
			if (defaultValue != null) {
				sb.append('=');
				VersionFormat.rawToString(sb, false, defaultValue);
				sb.append(';');
			}
			if (padValue != null) {
				sb.append('=');
				sb.append('p');
				VersionFormat.rawToString(sb, false, padValue);
				sb.append(';');
			}
			super.toString(sb);
		}
	}

	private static class GroupFragment extends ElementFragment {
		private static final long serialVersionUID = 9219978678087669699L;
		private final boolean array;
		private final Fragment[] fragments;

		GroupFragment(VersionFormatParser.Instructions instr, Qualifier qualifier, Fragment[] fragments, boolean array) {
			super(instr, qualifier);
			this.fragments = fragments;
			this.array = array;
		}

		@Override
		public boolean isGroup() {
			return !array;
		}

		@Override
		Fragment getFirstLeaf() {
			return fragments[0].getFirstLeaf();
		}

		@Override
		boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
			if (array) {
				ArrayList<Comparable<?>> subSegs = new ArrayList<>();
				boolean success = fragments[0].getQualifier().parse(fragments, 0, subSegs, version, maxPos, info);
				if (!success || subSegs.isEmpty())
					return false;

				Comparable<?> padValue = info.getPadValue();
				if (padValue != null)
					info.setPadValue(null); // Prevent outer group from getting this.
				else
					padValue = getPadValue();

				padValue = VersionParser.removeRedundantTrail(segments, padValue);
				segments.add(new VersionVector(subSegs.toArray(new Comparable[subSegs.size()]), padValue));
				return true;
			}

			if (fragments[0].getQualifier().parse(fragments, 0, segments, version, maxPos, info)) {
				Comparable<?> padValue = getPadValue();
				if (padValue != null)
					info.setPadValue(padValue);
				return true;
			}
			return false;
		}

		@Override
		void setDefaults(List<Comparable<?>> segments) {
			Comparable<?> dflt = getDefaultValue();
			if (dflt != null) {
				// A group default overrides any defaults within the
				// group fragments
				super.setDefaults(segments);
			} else {
				// Assign defaults for all fragments
				for (Fragment fragment : fragments)
					fragment.setDefaults(segments);
			}
		}

		@Override
		void toString(StringBuffer sb) {
			if (array) {
				sb.append('<');
				for (Fragment fragment : fragments)
					fragment.toString(sb);
				sb.append('>');
			} else {
				if (getQualifier() == VersionFormatParser.ZERO_OR_ONE_QUALIFIER) {
					sb.append('[');
					for (Fragment fragment : fragments)
						fragment.toString(sb);
					sb.append(']');
				} else {
					sb.append('(');
					for (Fragment fragment : fragments)
						fragment.toString(sb);
					sb.append(')');
				}
			}
			super.toString(sb);
		}
	}

	private static class LiteralFragment extends Fragment {
		private static final long serialVersionUID = 6210696245839471802L;
		private final String string;

		LiteralFragment(Qualifier qualifier, String string) {
			super(qualifier);
			this.string = string;
		}

		@Override
		boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
			int pos = info.getPosition();
			int litLen = string.length();
			if (pos + litLen > maxPos)
				return false;

			for (int idx = 0; idx < litLen; ++idx, ++pos) {
				if (string.charAt(idx) != version.charAt(pos))
					return false;
			}
			info.setPosition(pos);
			return true;
		}

		@Override
		void toString(StringBuffer sb) {
			String str = string;
			if (str.length() != 1) {
				sb.append('\'');
				VersionFormatParser.toStringEscaped(sb, str, "\'"); //$NON-NLS-1$
				sb.append('\'');
			} else {
				char c = str.charAt(0);
				switch (c) {
					case '\'' :
					case '\\' :
					case '<' :
					case '[' :
					case '(' :
					case '{' :
					case '?' :
					case '*' :
					case '+' :
					case '=' :
						sb.append('\\');
						sb.append(c);
						break;
					default :
						if (VersionParser.isLetterOrDigit(c)) {
							sb.append('\\');
							sb.append(c);
						} else
							sb.append(c);
				}
			}
			super.toString(sb);
		}
	}

	private static class NumberFragment extends RangeFragment {
		private static final long serialVersionUID = -8552754381106711507L;
		private final boolean signed;

		NumberFragment(VersionFormatParser.Instructions instr, Qualifier qualifier, boolean signed) {
			super(instr, qualifier);
			this.signed = signed;
		}

		@Override
		boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
			int pos = info.getPosition();
			maxPos = checkRange(pos, maxPos);
			if (maxPos < 0)
				return false;

			// Parse to next non-digit
			//
			int start = pos;
			int value;

			char c = version.charAt(pos);
			if (signed || characters != null) {
				boolean negate = false;
				if (signed && c == '-' && pos + 1 < maxPos) {
					negate = true;
					c = version.charAt(++pos);
				}

				if (!(c >= '0' && c <= '9' && isAllowed(c)))
					return false;

				// Parse to next non-digit
				//
				value = c - '0';
				while (++pos < maxPos) {
					c = version.charAt(pos);
					if (!(c >= '0' && c <= '9' && isAllowed(c)))
						break;
					value *= 10;
					value += (c - '0');
				}
				if (negate)
					value = -value;
			} else {
				if (c < '0' || c > '9')
					return false;

				// Parse to next non-digit
				//
				value = c - '0';
				while (++pos < maxPos) {
					c = version.charAt(pos);
					if (c < '0' || c > '9')
						break;
					value *= 10;
					value += (c - '0');
				}
			}

			int len = pos - start;
			if (rangeMin > len || len > rangeMax)
				return false;

			if (!isIgnored())
				segments.add(VersionParser.valueOf(value));
			info.setPosition(pos);
			return true;
		}

		@Override
		void toString(StringBuffer sb) {
			sb.append(signed ? 'N' : 'n');
			super.toString(sb);
		}
	}

	private static class PadFragment extends ElementFragment {
		private static final long serialVersionUID = 5052010199974380170L;

		PadFragment(Qualifier qualifier) {
			super(null, qualifier);
		}

		@Override
		boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
			int pos = info.getPosition();
			if (pos >= maxPos || version.charAt(pos) != 'p')
				return false;

			int[] position = new int[] {++pos};
			Comparable<?> v = VersionParser.parseRawElement(version, position, maxPos);
			if (v == null)
				return false;

			if (!isIgnored())
				info.setPadValue(v);
			info.setPosition(position[0]);
			return true;
		}

		@Override
		void toString(StringBuffer sb) {
			sb.append('p');
			super.toString(sb);
		}
	}

	private static class QuotedFragment extends RangeFragment {
		private static final long serialVersionUID = 6057751133533608969L;

		QuotedFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
			super(instr, qualifier);
		}

		@Override
		boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
			int pos = info.getPosition();
			if (pos >= maxPos)
				return false;

			char endQuote;
			char quote = version.charAt(pos);
			switch (quote) {
				case '<' :
					endQuote = '>';
					break;
				case '{' :
					endQuote = '}';
					break;
				case '(' :
					endQuote = ')';
					break;
				case '[' :
					endQuote = ']';
					break;
				case '>' :
					endQuote = '<';
					break;
				case '}' :
					endQuote = '{';
					break;
				case ')' :
					endQuote = '(';
					break;
				case ']' :
					endQuote = '[';
					break;
				default :
					if (VersionParser.isLetterOrDigit(quote))
						return false;
					endQuote = quote;
			}
			int start = ++pos;
			char c = version.charAt(pos);
			while (c != endQuote && isAllowed(c) && ++pos < maxPos)
				c = version.charAt(pos);

			if (c != endQuote || rangeMin > pos - start)
				// End quote not found
				return false;

			int len = pos - start;
			if (rangeMin > len || len > rangeMax)
				return false;

			if (!isIgnored())
				segments.add(version.substring(start, pos));
			info.setPosition(++pos); // Skip quote
			return true;
		}

		@Override
		void toString(StringBuffer sb) {
			sb.append('q');
			super.toString(sb);
		}
	}

	private static abstract class RangeFragment extends ElementFragment {
		private static final long serialVersionUID = -6680402803630334708L;
		final char[] characters;
		final boolean inverted;
		final int rangeMax;
		final int rangeMin;
		final EnumInstruction enumInstruction;

		RangeFragment(VersionFormatParser.Instructions instr, Qualifier qualifier) {
			super(instr, qualifier);
			if (instr == null) {
				characters = null;
				inverted = false;
				rangeMin = 0;
				rangeMax = Integer.MAX_VALUE;
				enumInstruction = null;
			} else {
				characters = instr.characters;
				inverted = instr.inverted;
				rangeMin = instr.rangeMin;
				rangeMax = instr.rangeMax;
				enumInstruction = instr.enumInstruction;
			}
		}

		/**
		 * Checks that pos is at a valid character position, that we
		 * have at least the required minimum characters left, and
		 * if a maximum number of characters is set, limits the
		 * returned value to a maxPos that reflects that maximum.
		 * @param pos the current position
		 * @param maxPos the current maxPos
		 * @return maxPos, possibly limited by rangeMax
		 */
		int checkRange(int pos, int maxPos) {
			int check = pos;
			if (rangeMin == 0)
				check++; // Verify one character
			else
				check += rangeMin;

			if (check > maxPos)
				// Less then min characters left
				maxPos = -1;
			else {
				if (rangeMax != Integer.MAX_VALUE) {
					check = pos + rangeMax;
					if (check < maxPos)
						maxPos = check;
				}
			}
			return maxPos;
		}

		boolean isAllowed(char c) {
			char[] crs = characters;
			if (crs != null) {
				int idx = crs.length;
				while (--idx >= 0)
					if (c == crs[idx])
						return !inverted;
				return inverted;
			}
			return true;
		}

		@Override
		void toString(StringBuffer sb) {
			if (characters != null)
				appendCharacterRange(sb, characters, inverted);
			if (rangeMin != 0 || rangeMax != Integer.MAX_VALUE) {
				sb.append('=');
				sb.append('{');
				sb.append(rangeMin);
				if (rangeMin != rangeMax) {
					sb.append(',');
					if (rangeMax != Integer.MAX_VALUE)
						sb.append(rangeMax);
				}
				sb.append('}');
				sb.append(';');
			}
			if (enumInstruction != null)
				enumInstruction.toString(sb);
			super.toString(sb);
		}
	}

	private static class RawFragment extends ElementFragment {
		private static final long serialVersionUID = 4107448125256042602L;

		RawFragment(VersionFormatParser.Instructions processing, Qualifier qualifier) {
			super(processing, qualifier);
		}

		@Override
		boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
			int[] position = new int[] {info.getPosition()};
			Comparable<?> v = VersionParser.parseRawElement(version, position, maxPos);
			if (v == null)
				return false;

			if (!isIgnored())
				segments.add(v);
			info.setPosition(position[0]);
			return true;
		}

		@Override
		void toString(StringBuffer sb) {
			sb.append('r');
			super.toString(sb);
		}
	}

	private static class StringFragment extends RangeFragment {
		private static final long serialVersionUID = -2265924553606430164L;
		final boolean anyChar;
		private final char oppositeTranslationChar;
		private final int oppositeTranslationRepeat;

		StringFragment(VersionFormatParser.Instructions instr, Qualifier qualifier, boolean noLimit) {
			super(instr, qualifier);
			anyChar = noLimit;
			char otc = 0;
			int otr = 0;
			if (instr != null) {
				otc = instr.oppositeTranslationChar;
				otr = instr.oppositeTranslationRepeat;
				if (instr.defaultValue == VersionVector.MINS_VALUE) {
					if (otc == 0)
						otc = 'z';
					if (otr == 0)
						otr = 3;
				} else if (instr.defaultValue == VersionVector.MAXS_VALUE) {
					if (otc == 0)
						otc = '-';
					otr = 1;
				}
			}
			oppositeTranslationChar = otc;
			oppositeTranslationRepeat = otr;
		}

		Comparable<?> getOppositeDefaultValue() {
			Comparable<?> dflt = getDefaultValue();
			return dflt == VersionVector.MAXS_VALUE ? VersionVector.MINS_VALUE : (dflt == VersionVector.MINS_VALUE ? VersionVector.MAXS_VALUE : null);
		}

		public boolean isOppositeTranslation(Object val) {
			if (val instanceof String) {
				String str = (String) val;
				int idx = oppositeTranslationRepeat;
				if (str.length() == idx) {
					while (--idx >= 0)
						if (str.charAt(idx) != oppositeTranslationChar)
							break;
					return idx < 0;
				}
			}
			return false;
		}

		@Override
		boolean parseOne(List<Comparable<?>> segments, String version, int maxPos, TreeInfo info) {
			int pos = info.getPosition();
			maxPos = checkRange(pos, maxPos);
			if (maxPos < 0)
				return false;

			int start = pos;
			if (enumInstruction != null) {
				int[] posHolder = new int[] {pos};
				EnumSegment es = enumInstruction.getEnumSegment(this, version, posHolder, maxPos);
				if (es != null) {
					pos = posHolder[0];
					int len = pos - start;
					if (rangeMin > len || len > rangeMax)
						return false;
					if (!isIgnored())
						segments.add(es);
					info.setPosition(pos);
					return true;
				}
				if (!enumInstruction.isOptional())
					return false;
			}

			// Parse to next delimiter or end of string
			//
			if (characters != null) {
				if (anyChar) {
					// Swallow everything that matches the allowed characters
					for (; pos < maxPos; ++pos) {
						if (!isAllowed(version.charAt(pos)))
							break;
					}
				} else {
					// Swallow letters that matches the allowed characters
					for (; pos < maxPos; ++pos) {
						char c = version.charAt(pos);
						if (!(VersionParser.isLetter(c) && isAllowed(c)))
							break;
					}
				}
			} else {
				if (anyChar)
					// Swallow all characters
					pos = maxPos;
				else {
					// Swallow all letters
					for (; pos < maxPos; ++pos) {
						if (!VersionParser.isLetter(version.charAt(pos)))
							break;
					}
				}
			}
			int len = pos - start;
			if (len == 0 || rangeMin > len || len > rangeMax)
				return false;

			if (!isIgnored())
				segments.add(version.substring(start, pos));
			info.setPosition(pos);
			return true;
		}

		@Override
		void toString(StringBuffer sb) {
			sb.append(anyChar ? 'S' : 's');
			super.toString(sb);
		}
	}

	private int current;

	private List<Fragment> currentList;

	private int eos;

	private String format;

	private int start;

	Fragment compile(String fmt, int pos, int maxPos) throws VersionFormatException {
		format = fmt;
		if (start >= maxPos)
			throw new VersionFormatException(Messages.format_is_empty);

		start = pos;
		current = pos;
		eos = maxPos;
		currentList = new ArrayList<>();
		while (current < eos)
			parseFragment();

		Fragment topFrag;
		switch (currentList.size()) {
			case 0 :
				throw new VersionFormatException(Messages.format_is_empty);
			case 1 :
				Fragment frag = currentList.get(0);
				if (frag.isGroup()) {
					topFrag = frag;
					break;
				}
				// Fall through to default
			default :
				topFrag = createGroupFragment(null, EXACT_ONE_QUALIFIER, currentList.toArray(new Fragment[currentList.size()]), false);
		}
		currentList = null;
		return topFrag;
	}

	private void assertChar(char expected) throws VersionFormatException {
		if (current >= eos)
			throw formatException(NLS.bind(Messages.premature_end_of_format_expected_0, new String(new char[] {expected})));

		char c = format.charAt(current);
		if (c != expected)
			throw formatException(c, new String(new char[] {expected}));
		++current;
	}

	private VersionFormatException formatException(char found, String expected) {
		return formatException(new String(new char[] {found}), expected);
	}

	private VersionFormatException formatException(String message) {
		return new VersionFormatException(NLS.bind(Messages.syntax_error_in_version_format_0_1_2, new Object[] {format.substring(start, eos), Integer.valueOf(current), message}));
	}

	private VersionFormatException formatException(String found, String expected) {
		return new VersionFormatException(NLS.bind(Messages.syntax_error_in_version_format_0_1_found_2_expected_3, new Object[] {format.substring(start, eos), Integer.valueOf(current), found, expected}));
	}

	private VersionFormatException illegalControlCharacter(char c) {
		return formatException(NLS.bind(Messages.illegal_character_encountered_ascii_0, VersionParser.valueOf(c)));
	}

	private String parseAndConsiderEscapeUntil(char endChar) throws VersionFormatException {
		StringBuilder sb = new StringBuilder();
		while (current < eos) {
			char c = format.charAt(current++);
			if (c == endChar)
				break;

			if (c < 32)
				throw illegalControlCharacter(c);

			if (c == '\\') {
				if (current == eos)
					throw formatException(Messages.EOS_after_escape);
				c = format.charAt(current++);
				if (c < 32)
					throw illegalControlCharacter(c);
			}
			sb.append(c);
		}
		return sb.toString();
	}

	private void parseAuto() throws VersionFormatException {
		VersionFormatParser.Instructions ep = parseProcessing();
		if (ep != null) {
			if (ep.padValue != null)
				throw formatException(Messages.auto_can_not_have_pad_value);
		}
		currentList.add(createAutoFragment(ep, parseQualifier()));
	}

	private void parseBracketGroup() throws VersionFormatException {
		List<Fragment> saveList = currentList;
		currentList = new ArrayList<>();
		while (current < eos && format.charAt(current) != ']')
			parseFragment();

		if (current == eos)
			throw formatException(NLS.bind(Messages.premature_end_of_format_expected_0, "]")); //$NON-NLS-1$

		++current;
		VersionFormatParser.Instructions ep = parseProcessing();
		saveList.add(createGroupFragment(ep, ZERO_OR_ONE_QUALIFIER, currentList.toArray(new Fragment[currentList.size()]), false));
		currentList = saveList;
	}

	private void parseCharacterGroup(VersionFormatParser.Instructions ep) throws VersionFormatException {
		assertChar('[');

		StringBuilder sb = new StringBuilder();
		outer: for (; current < eos; ++current) {
			char c = format.charAt(current);
			switch (c) {
				case '\\' :
					if (current + 1 < eos) {
						sb.append(format.charAt(++current));
						continue;
					}
					throw formatException(Messages.premature_end_of_format);
				case '^' :
					if (sb.length() == 0)
						ep.inverted = true;
					else
						sb.append(c);
					continue;
				case ']' :
					break outer;
				case '-' :
					if (sb.length() > 0 && current + 1 < eos) {
						char rangeEnd = format.charAt(++current);
						if (rangeEnd == ']') {
							// Use dash verbatim when last in range
							sb.append(c);
							break outer;
						}

						char rangeStart = sb.charAt(sb.length() - 1);
						if (rangeEnd < rangeStart)
							throw formatException(Messages.negative_character_range);
						while (++rangeStart <= rangeEnd)
							sb.append(rangeStart);
						continue;
					}
					// Fall through to default
				default :
					if (c < 32)
						throw illegalControlCharacter(c);
					sb.append(c);
			}
		}
		assertChar(']');
		int top = sb.length();
		char[] chars = new char[top];
		sb.getChars(0, top, chars, 0);
		ep.characters = chars;
	}

	private void parseDelimiter() throws VersionFormatException {
		VersionFormatParser.Instructions ep = parseProcessing();
		if (ep != null) {
			if (ep.rangeMin != 0 || ep.rangeMax != Integer.MAX_VALUE)
				throw formatException(Messages.delimiter_can_not_have_range);
			if (ep.ignore)
				throw formatException(Messages.delimiter_can_not_be_ignored);
			if (ep.defaultValue != null)
				throw formatException(Messages.delimiter_can_not_have_default_value);
			if (ep.padValue != null)
				throw formatException(Messages.delimiter_can_not_have_pad_value);
		}
		currentList.add(createDelimiterFragment(ep, parseQualifier()));
	}

	private void parseEnum(Instructions processing) throws VersionFormatException {
		++current;
		ArrayList<List<String>> identifiers = new ArrayList<>();
		ArrayList<String> idents = new ArrayList<>();
		StringBuilder sb = new StringBuilder();
		for (;;) {
			if (current >= eos)
				throw formatException(Messages.bad_enum_definition);

			char c = format.charAt(current++);
			while (c != '}' && c != ',' && c != '=') {
				if (current >= eos || c <= ' ')
					throw formatException(Messages.bad_enum_definition);
				if (c == '\\') {
					c = format.charAt(current++);
					if (current >= eos)
						throw formatException(Messages.bad_enum_definition);
				}
				sb.append(c);
				c = format.charAt(current++);
			}
			idents.add(sb.toString());
			sb.setLength(0);
			if (c == '=')
				continue;

			identifiers.add(idents);
			if (c == '}')
				break;

			// c must be ',' at this point
			idents = new ArrayList<>();
		}

		boolean enumCaseSensitive = true;
		boolean enumOptional = false;
		boolean enumBegins = false;
	    OUTER:
	    while (current < eos) {
		char c = format.charAt(current);
		switch (c) {
			case 'i':
				enumCaseSensitive = false;
				current++;
				break;
			case 'b':
				enumBegins = true;
				current++;
				break;
			case '?':
				enumOptional = true;
				current++;
				break;
			default:
				break OUTER;
		}
	    }

		// Ensure that all identifiers are unique and make them
		// lower case if necessary
		HashSet<String> unique = new HashSet<>();
		int ordinal = identifiers.size();
		while (--ordinal >= 0) {
			List<String> ids = identifiers.get(ordinal);
			int idx = ids.size();
			while (--idx >= 0) {
				String id = ids.get(idx);
				if (!enumCaseSensitive)
					id = id.toLowerCase();
				if (!unique.add(id))
					throw formatException(Messages.bad_enum_definition);
				ids.set(idx, id);
			}
		}
		EnumDefinition enumDefinition = EnumDefinition.getEnumDefinition(identifiers);
		processing.enumInstruction = new EnumInstruction(enumDefinition, enumCaseSensitive, enumOptional, enumBegins);
	}

	private void parseFragment() throws VersionFormatException {
		if (current == eos)
			throw formatException(Messages.premature_end_of_format);
		char c = format.charAt(current++);
		switch (c) {
			case '(' :
				parseGroup(false);
				break;
			case '<' :
				parseGroup(true);
				break;
			case '[' :
				parseBracketGroup();
				break;
			case 'a' :
				parseAuto();
				break;
			case 'r' :
				parseRaw();
				break;
			case 'n' :
				parseNumber(false);
				break;
			case 'N' :
				parseNumber(true);
				break;
			case 's' :
				parseString(false);
				break;
			case 'S' :
				parseString(true);
				break;
			case 'd' :
				parseDelimiter();
				break;
			case 'q' :
				parseQuotedString();
				break;
			case 'p' :
				parsePad();
				break;
			default :
				parseLiteral(c);
		}
	}

	private void parseGroup(boolean array) throws VersionFormatException {
		List<Fragment> saveList = currentList;
		currentList = new ArrayList<>();
		char expectedEnd = array ? '>' : ')';
		while (current < eos && format.charAt(current) != expectedEnd)
			parseFragment();
		assertChar(expectedEnd);

		VersionFormatParser.Instructions ep = parseProcessing();
		if (ep != null) {
			if (ep.characters != null)
				throw formatException(Messages.array_can_not_have_character_group);
			if (ep.rangeMax != Integer.MAX_VALUE && ep.padValue != null)
				throw formatException(Messages.cannot_combine_range_upper_bound_with_pad_value);
			if (ep.enumInstruction != null)
				throw formatException(Messages.array_can_not_have_enum);
		}

		if (currentList.isEmpty())
			throw formatException(array ? Messages.array_can_not_be_empty : Messages.group_can_not_be_empty);
		saveList.add(createGroupFragment(ep, parseQualifier(), currentList.toArray(new Fragment[currentList.size()]), array));
		currentList = saveList;
	}

	private int parseIntegerLiteral() throws VersionFormatException {
		if (current == eos)
			throw formatException(NLS.bind(Messages.premature_end_of_format_expected_0, "<integer>")); //$NON-NLS-1$

		char c = format.charAt(current);
		if (!VersionParser.isDigit(c))
			throw formatException(c, "<integer>"); //$NON-NLS-1$

		int value = c - '0';
		while (++current < eos) {
			c = format.charAt(current);
			if (!VersionParser.isDigit(c))
				break;
			value *= 10;
			value += (c - '0');
		}
		return value;
	}

	private void parseLiteral(char c) throws VersionFormatException {
		String value;
		switch (c) {
			case '\'' :
				value = parseAndConsiderEscapeUntil(c);
				break;
			case ')' :
			case ']' :
			case '{' :
			case '}' :
			case '?' :
			case '*' :
				throw formatException(c, "<literal>"); //$NON-NLS-1$
			default :
				if (VersionParser.isLetterOrDigit(c))
					throw formatException(c, "<literal>"); //$NON-NLS-1$

				if (c < 32)
					throw illegalControlCharacter(c);

				if (c == '\\') {
					if (current == eos)
						throw formatException(Messages.EOS_after_escape);
					c = format.charAt(current++);
					if (c < 32)
						throw illegalControlCharacter(c);
				}
				value = new String(new char[] {c});
		}
		currentList.add(createLiteralFragment(parseQualifier(), value));
	}

	private int[] parseMinMax() throws VersionFormatException {

		int max = Integer.MAX_VALUE;
		++current;
		int min = parseIntegerLiteral();
		char c = format.charAt(current);
		if (c == '}') {
			max = min;
			if (max == 0)
				throw formatException(Messages.range_max_cannot_be_zero);
			++current;
		} else if (c == ',' && current + 1 < eos) {
			if (format.charAt(++current) != '}') {
				max = parseIntegerLiteral();
				if (max == 0)
					throw formatException(Messages.range_max_cannot_be_zero);
				if (max < min)
					throw formatException(Messages.range_max_cannot_be_less_then_range_min);
			}
			assertChar('}');
		} else
			throw formatException(c, "},"); //$NON-NLS-1$
		return new int[] {min, max};
	}

	private void parseNumber(boolean signed) throws VersionFormatException {
		VersionFormatParser.Instructions ep = parseProcessing();
		if (ep != null) {
			if (ep.padValue != null)
				throw formatException(Messages.number_can_not_have_pad_value);
		}
		currentList.add(createNumberFragment(ep, parseQualifier(), signed));
	}

	private void parsePad() throws VersionFormatException {
		currentList.add(createPadFragment(parseQualifier()));
	}

	private VersionFormatParser.Instructions parseProcessing() throws VersionFormatException {
		if (current >= eos)
			return null;

		char c = format.charAt(current);
		if (c != '=')
			return null;

		VersionFormatParser.Instructions ep = new VersionFormatParser.Instructions();
		do {
			current++;
			parseProcessingInstruction(ep);
		} while (current < eos && format.charAt(current) == '=');
		return ep;
	}

	private void parseProcessingInstruction(VersionFormatParser.Instructions processing) throws VersionFormatException {
		if (current == eos)
			throw formatException(Messages.premature_end_of_format);

		char c = format.charAt(current);
		switch (c) {
			case 'p':
				// =pad(<raw-element>);
				//
				if (processing.padValue != null)
					throw formatException(Messages.pad_defined_more_then_once);
				if (processing.ignore)
					throw formatException(Messages.cannot_combine_ignore_with_other_instruction);
				++current;
				processing.padValue = parseRawElement();
				break;
			case '!':
				// =ignore;
				//
				if (processing.ignore)
					throw formatException(Messages.ignore_defined_more_then_once);
				if (processing.padValue != null || processing.characters != null || processing.rangeMin != 0 || processing.rangeMax != Integer.MAX_VALUE || processing.defaultValue != null)
					throw formatException(Messages.cannot_combine_ignore_with_other_instruction);
				++current;
				processing.ignore = true;
				break;
			case '[':
				// =[<character group];
				//
				if (processing.characters != null)
					throw formatException(Messages.character_group_defined_more_then_once);
				if (processing.ignore)
					throw formatException(Messages.cannot_combine_ignore_with_other_instruction);
				parseCharacterGroup(processing);
				break;
			case '{':
				if (current + 1 == eos)
					throw formatException(Messages.premature_end_of_format);
				if (VersionParser.isDigit(format.charAt(current + 1))) {
					// ={min,max};
					//
					if (processing.rangeMin != 0 || processing.rangeMax != Integer.MAX_VALUE)
						throw formatException(Messages.range_defined_more_then_once);
					if (processing.ignore)
						throw formatException(Messages.cannot_combine_ignore_with_other_instruction);
					int[] minMax = parseMinMax();
					processing.rangeMin = minMax[0];
					processing.rangeMax = minMax[1];
				} else {
					// ={enum1,enum2,...};
					//
					if (processing.enumInstruction != null)
						throw formatException(Messages.enum_defined_more_then_once);
					parseEnum(processing);
				}
				break;
			default:
				// =<raw-element>;
				if (processing.defaultValue != null)
					throw formatException(Messages.default_defined_more_then_once);
				if (processing.ignore)
					throw formatException(Messages.cannot_combine_ignore_with_other_instruction);
				Comparable<?> dflt = parseRawElement();
				processing.defaultValue = dflt;
				if (current < eos && format.charAt(current) == '{') {
					// =m{<translated min char>}
					// =''{<translated max char>,<max char repeat>}
					if (++current == eos)
						throw formatException(Messages.premature_end_of_format);
					processing.oppositeTranslationChar = format.charAt(current++);
					if (current == eos)
						throw formatException(Messages.premature_end_of_format);

					if (dflt == VersionVector.MINS_VALUE) {
						processing.oppositeTranslationRepeat = 3;
						if (format.charAt(current) == ',') {
							++current;
							processing.oppositeTranslationRepeat = parseIntegerLiteral();
						}
					} else if (dflt != VersionVector.MAXS_VALUE) {
						current -= 2;
						throw formatException(Messages.only_max_and_empty_string_defaults_can_have_translations);
					}
					assertChar('}');
				}
				break;
		}
		assertChar(';');
	}

	private Qualifier parseQualifier() throws VersionFormatException {
		if (current >= eos)
			return EXACT_ONE_QUALIFIER;

		char c = format.charAt(current);
		if (c == '?') {
			++current;
			return ZERO_OR_ONE_QUALIFIER;
		}

		if (c == '*') {
			++current;
			return ZERO_OR_MANY_QUALIFIER;
		}

		if (c == '+') {
			++current;
			return ONE_OR_MANY_QUALIFIER;
		}

		if (c != '{')
			return EXACT_ONE_QUALIFIER;

		int[] minMax = parseMinMax();
		int min = minMax[0];
		int max = minMax[1];

		// Use singletons for commonly used ranges
		//
		if (min == 0) {
			if (max == 1)
				return ZERO_OR_ONE_QUALIFIER;
			if (max == Integer.MAX_VALUE)
				return ZERO_OR_MANY_QUALIFIER;
		} else if (min == 1) {
			if (max == 1)
				return EXACT_ONE_QUALIFIER;
			if (max == Integer.MAX_VALUE)
				return ONE_OR_MANY_QUALIFIER;
		}
		return new Qualifier(min, max);
	}

	private void parseQuotedString() throws VersionFormatException {
		VersionFormatParser.Instructions ep = parseProcessing();
		if (ep != null) {
			if (ep.padValue != null)
				throw formatException(Messages.string_can_not_have_pad_value);
		}
		currentList.add(createQuotedFragment(ep, parseQualifier()));
	}

	private void parseRaw() throws VersionFormatException {
		VersionFormatParser.Instructions ep = parseProcessing();
		if (ep != null) {
			if (ep.padValue != null)
				throw formatException(Messages.raw_element_can_not_have_pad_value);
		}
		currentList.add(createRawFragment(ep, parseQualifier()));
	}

	private Comparable<?> parseRawElement() throws VersionFormatException {
		int[] position = new int[] {current};
		Comparable<?> v = VersionParser.parseRawElement(format, position, eos);
		if (v == null)
			throw new VersionFormatException(NLS.bind(Messages.raw_element_expected_0, format));
		current = position[0];
		return v;
	}

	private void parseString(boolean unlimited) throws VersionFormatException {
		VersionFormatParser.Instructions ep = parseProcessing();
		if (ep != null) {
			if (ep.padValue != null)
				throw formatException(Messages.string_can_not_have_pad_value);
		}
		currentList.add(createStringFragment(ep, parseQualifier(), unlimited));
	}

	static void toStringEscaped(StringBuffer sb, String value, String escapes) {
		for (int idx = 0; idx < value.length(); ++idx) {
			char c = value.charAt(idx);
			if (c == '\\' || escapes.indexOf(c) >= 0)
				sb.append('\\');
			sb.append(c);
		}
	}
}
