/*******************************************************************************
 * Copyright (c) 2000, 2004 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.osgi.service.resolver;

import java.util.StringTokenizer;
import java.util.Vector;

/**
 * <p>
 * Version identifier for a plug-in. In its string representation, 
 * it consists of up to 4 tokens separated by a decimal point.
 * The first 3 tokens are positive integer numbers, the last token
 * is an uninterpreted string (no whitespace characters allowed).
 * For example, the following are valid version identifiers 
 * (as strings):
 * <ul>
 *   <li><code>0.0.0</code></li>
 *   <li><code>1.0.127564</code></li>
 *   <li><code>3.7.2.build-127J</code></li>
 *   <li><code>1.9</code> (interpreted as <code>1.9.0</code>)</li>
 *   <li><code>3</code> (interpreted as <code>3.0.0</code>)</li>
 * </ul>
 * </p>
 * <p>
 * The version identifier can be decomposed into a major, minor, 
 * micro level component and qualifier components. A difference
 * in the major component is interpreted as an incompatible version
 * change. A difference in the minor (and not the major) component
 * is interpreted as a compatible version change. The micro
 * level component is interpreted as a cumulative and compatible
 * micro update of the minor version component. The qualifier is
 * not interpreted, other than in version comparisons. The 
 * qualifiers are compared using lexicographical string comparison.
 * </p>
 * <p>
 * Version identifiers can be matched as perfectly equal, equivalent,
 * compatible or greaterOrEqual.
 * </p>
 * <p>
 * Clients may instantiate; not intended to be subclassed by clients.
 * </p>
 * @see org.eclipse.core.runtime.IPluginDescriptor#getVersionIdentifier
 * @see java.lang.String#compareTo
 */
public final class Version implements Comparable {

	private int major = 0;
	private int minor = 0;
	private int micro = 0;
	private String qualifier = ""; //$NON-NLS-1$
	private boolean inclusive = true;

	private static final String SEPARATOR = "."; //$NON-NLS-1$

	public static Version emptyVersion = new Version(0, 0, 0);
	public static Version maxVersion = new Version(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);

	/**
	 * Creates a plug-in version identifier from a given Version.
	 * 
	 * @param version 
	 */
	public Version(Version version) {
		this(version.major, version.minor, version.micro, version.qualifier, version.inclusive);
	}

	/**
	 * Creates a plug-in version identifier from its components.
	 * 
	 * @param major major component of the version identifier
	 * @param minor minor component of the version identifier
	 * @param micro micro update component of the version identifier
	 */
	public Version(int major, int minor, int micro) {
		this(major, minor, micro, null);
	}

	/**
	 * Creates a plug-in version identifier from its components.
	 * 
	 * @param major major component of the version identifier
	 * @param minor minor component of the version identifier
	 * @param micro micro update component of the version identifier
	 * @param qualifier qualifier component of the version identifier
	 * Qualifier characters that are not a letter or a digit are replaced.
	 */
	public Version(int major, int minor, int micro, String qualifier) throws IllegalArgumentException {
		this(major, minor, micro, qualifier, true);
	}

	/**
	 * Creates a plug-in version identifier from its components.
	 * 
	 * @param major major component of the version identifier
	 * @param minor minor component of the version identifier
	 * @param micro micro update component of the version identifier
	 * @param qualifier qualifier component of the version identifier
	 * @param inclusive inclusive property of the version identifier
	 * Qualifier characters that are not a letter or a digit are replaced.
	 */
	public Version(int major, int minor, int micro, String qualifier, boolean inclusive) throws IllegalArgumentException {

		// Do the test outside of the assert so that they 'Policy.bind' 
		// will not be evaluated each time (including cases when we would
		// have passed by the assert).

		if (major < 0)
			throw new IllegalArgumentException("Negative major"); //$NON-NLS-1$
		if (minor < 0)
			throw new IllegalArgumentException("Negative minor"); //$NON-NLS-1$
		if (micro < 0)
			throw new IllegalArgumentException("Negative micro"); //$NON-NLS-1$
		if (qualifier == null)
			qualifier = ""; //$NON-NLS-1$

		this.major = major;
		this.minor = minor;
		this.micro = micro;
		this.qualifier = verifyQualifier(qualifier);
		this.inclusive = inclusive;
	}

	/**
	 * Creates a plug-in version identifier from the given string.
	 * The string represenation consists of up to 4 tokens 
	 * separated by decimal point.
	 * For example, the following are valid version identifiers 
	 * (as strings):
	 * <ul>
	 *   <li><code>0.0.0</code></li>
	 *   <li><code>1.0.127564</code></li>
	 *   <li><code>3.7.2.build-127J</code></li>
	 *   <li><code>1.9</code> (interpreted as <code>1.9.0</code>)</li>
	 *   <li><code>3</code> (interpreted as <code>3.0.0</code>)</li>
	 * </ul>
	 * 
	 * @param versionId string representation of the version identifier. 
	 * Qualifier characters that are not a letter or a digit are replaced.
	 */
	public Version(String versionId) {
		this(versionId, true);
	}

	/**
	 * Creates a plug-in version identifier from the given string.
	 * The string represenation consists of up to 4 tokens 
	 * separated by decimal point.
	 * For example, the following are valid version identifiers 
	 * (as strings):
	 * <ul>
	 *   <li><code>0.0.0</code></li>
	 *   <li><code>1.0.127564</code></li>
	 *   <li><code>3.7.2.build-127J</code></li>
	 *   <li><code>1.9</code> (interpreted as <code>1.9.0</code>)</li>
	 *   <li><code>3</code> (interpreted as <code>3.0.0</code>)</li>
	 * </ul>
	 * 
	 * @param versionId string representation of the version identifier. 
	 * Qualifier characters that are not a letter or a digit are replaced.
	 * @param inclusive inclusive property of the version identifier
	 */
	public Version(String versionId, boolean inclusive) {
		if (versionId == null)
			versionId = "0.0.0"; //$NON-NLS-1$
		Object[] parts = parseVersion(versionId);
		this.major = ((Integer) parts[0]).intValue();
		this.minor = ((Integer) parts[1]).intValue();
		this.micro = ((Integer) parts[2]).intValue();
		this.qualifier = (String) parts[3];
		this.inclusive = inclusive;
	}

	private static Object[] parseVersion(String versionId) {
		// Do the test outside of the assert so that they 'Policy.bind' 
		// will not be evaluated each time (including cases when we would
		// have passed by the assert).
		if (versionId == null)
			throw new IllegalArgumentException("Null version string"); //$NON-NLS-1$

		String s = versionId.trim();
		if (s.equals("")) //$NON-NLS-1$
			throw new IllegalArgumentException("Empty version string"); //$NON-NLS-1$
		if (s.startsWith(SEPARATOR))
			throw new IllegalArgumentException("Invalid version format"); //$NON-NLS-1$
		if (s.endsWith(SEPARATOR))
			throw new IllegalArgumentException("Invalid version format"); //$NON-NLS-1$
		if (s.indexOf(SEPARATOR + SEPARATOR) != -1)
			throw new IllegalArgumentException("Invalid version format"); //$NON-NLS-1$

		StringTokenizer st = new StringTokenizer(s, SEPARATOR);
		Vector elements = new Vector(4);

		while (st.hasMoreTokens())
			elements.addElement(st.nextToken());

		int elementSize = elements.size();

		if (elementSize <= 0)
			throw new IllegalArgumentException("Invalid version format (no token)"); //$NON-NLS-1$
		if (elementSize > 4)
			throw new IllegalArgumentException("Invalid version format (more than 4 tokens)"); //$NON-NLS-1$

		int[] numbers = new int[3];
		try {
			numbers[0] = Integer.parseInt((String) elements.elementAt(0));
			if (numbers[0] < 0)
				throw new IllegalArgumentException("Negative major"); //$NON-NLS-1$
		} catch (NumberFormatException nfe) {
			throw new IllegalArgumentException("Invalid major"); //$NON-NLS-1$
		}

		try {
			if (elementSize >= 2) {
				numbers[1] = Integer.parseInt((String) elements.elementAt(1));
				if (numbers[1] < 0)
					throw new IllegalArgumentException("Negative minor"); //$NON-NLS-1$
			} else
				numbers[1] = 0;
		} catch (NumberFormatException nfe) {
			throw new IllegalArgumentException("Invalid minor"); //$NON-NLS-1$
		}

		try {
			if (elementSize >= 3) {
				numbers[2] = Integer.parseInt((String) elements.elementAt(2));
				if (numbers[2] < 0)
					throw new IllegalArgumentException("Invalid micro"); //$NON-NLS-1$
			} else
				numbers[2] = 0;
		} catch (NumberFormatException nfe) {
			throw new IllegalArgumentException("Invalid micro"); //$NON-NLS-1$
		}

		// "result" is a 4-element array with the major, minor, micro, and qualifier
		Object[] result = new Object[4];
		result[0] = new Integer(numbers[0]);
		result[1] = new Integer(numbers[1]);
		result[2] = new Integer(numbers[2]);
		if (elementSize >= 4)
			result[3] = verifyQualifier((String) elements.elementAt(3));
		else
			result[3] = ""; //$NON-NLS-1$
		return result;
	}

	/**
	 * Compare version identifiers for equality. Identifiers are
	 * equal if all of their components are equal.
	 *
	 * @param object an object to compare
	 * @return whehter or not the two objects are equal
	 */
	public boolean equals(Object object) {
		if (!(object instanceof Version))
			return false;
		Version v = (Version) object;
		return v.getMajorComponent() == major && v.getMinorComponent() == minor && v.getMicroComponent() == micro && v.getQualifierComponent().equals(qualifier);
	}

	/**
	 * Returns a hash code value for the object. 
	 *
	 * @return an integer which is a hash code value for this object.
	 */
	public int hashCode() {
		int code = major + minor + micro; // R1.0 result
		if (qualifier.equals("")) //$NON-NLS-1$
			return code;
		else
			return code + qualifier.hashCode();
	}

	/**
	 * Returns the major (incompatible) component of this 
	 * version identifier.
	 *
	 * @return the major version
	 */
	public int getMajorComponent() {
		return major;
	}

	/**
	 * Returns the minor (compatible) component of this 
	 * version identifier.
	 *
	 * @return the minor version
	 */
	public int getMinorComponent() {
		return minor;
	}

	/**
	 * Returns the micro level component of this 
	 * version identifier.
	 *
	 * @return the micro level
	 */
	public int getMicroComponent() {
		return micro;
	}

	/**
	 * Returns the qualifier component of this 
	 * version identifier.
	 *
	 * @return the qualifier
	 */
	public String getQualifierComponent() {
		return qualifier;
	}

	/**
	 * Returns whether this Version is inclusive when used as a
	 * VersionRange minimum or maximum.
	 * @return <code>true</code> if this Version is inclusive, 
	 * <code>false</code> otherwise
	 */
	public boolean isInclusive() {
		return inclusive;
	}

	/**
	 * Compares two version identifiers to see if this one is
	 * greater than or equal to the argument.
	 * <p>
	 * A version identifier is considered to be greater than or equal
	 * if its major component is greater than the argument major 
	 * component, or the major components are equal and its minor component
	 * is greater than the argument minor component, or the
	 * major and minor components are equal and its micro component is
	 * greater than the argument micro component, or the major, minor and
	 * micro components are equal and the qualifier component is
	 * greated than the argument qualifier component (using lexicographic
	 * string comparison), or all components are equal.
	 * </p>
	 * @deprecated use {@link VersionRange}
	 * @param id the other version identifier
	 * @return <code>true</code> is this version identifier
	 *    is compatible with the given version identifier, and
	 *    <code>false</code> otherwise
	 */
	public boolean matchGreaterOrEqualTo(Version id) {
		if (id == null)
			return false;
		if (major > id.getMajorComponent())
			return true;
		if ((major == id.getMajorComponent()) && (minor > id.getMinorComponent()))
			return true;
		if ((major == id.getMajorComponent()) && (minor == id.getMinorComponent()) && (micro > id.getMicroComponent()))
			return true;
		if ((major == id.getMajorComponent()) && (minor == id.getMinorComponent()) && (micro == id.getMicroComponent()) && (qualifier.compareTo(id.getQualifierComponent()) >= 0))
			return true;
		else
			return false;
	}

	/**
	 * Compares two version identifiers for major match.
	 * <p>
	 * A version identifier is considered to match on major if its major 
	 * component equals to the argument major component, and its minor component
	 * is greater than or equal to the argument minor component.
	 * If the minor components are equal, than the micro level of the
	 * version identifier must be greater than or equal to the micro level
	 * of the argument identifier. If the micro levels are equal, the two 
	 * version identifiers are considered to be equivalent if this qualifier is 
	 * greated or equal to the qualifier of the argument (using lexicographic
	 * string comparison).
	 * </p>
	 * @deprecated use {@link VersionRange}
	 * @param id the other version identifier
	 * @return <code>true</code> is this version identifier
	 *    is compatible with the given version identifier, and
	 *    <code>false</code> otherwise
	 */
	public boolean matchMajor(Version id) {
		if (id == null)
			return false;
		if (major != id.getMajorComponent())
			return false;
		if (minor > id.getMinorComponent())
			return true;
		if (minor < id.getMinorComponent())
			return false;
		if (micro > id.getMicroComponent())
			return true;
		if (micro < id.getMicroComponent())
			return false;
		if (qualifier.compareTo(id.getQualifierComponent()) >= 0)
			return true;
		else
			return false;
	}

	/**
	 * Compares two version identifiers for equivalency.
	 * <p>
	 * Two version identifiers are considered to be equivalent if their major 
	 * and minor component equal and are at least at the same micro level 
	 * as the argument. If the micro levels are equal, the two version
	 * identifiers are considered to be equivalent if this qualifier is 
	 * greated or equal to the qualifier of the argument (using lexicographic
	 * string comparison).
	 * </p>
	 * @deprecated use {@link VersionRange}
	 * @param id the other version identifier
	 * @return <code>true</code> is this version identifier
	 *    is equivalent to the given version identifier, and
	 *    <code>false</code> otherwise
	 */
	public boolean matchMinor(Version id) {
		if (id == null)
			return false;
		if (major != id.getMajorComponent())
			return false;
		if (minor != id.getMinorComponent())
			return false;
		if (micro > id.getMicroComponent())
			return true;
		if (micro < id.getMicroComponent())
			return false;
		if (qualifier.compareTo(id.getQualifierComponent()) >= 0)
			return true;
		else
			return false;
	}

	/**
	 * Compares two version identifiers for micro match.
	 * <p>
	 * Two version identifiers are considered to micro match if their major, 
	 * minor and micro components equal and this qualifier is 
	 * greated or equal to the qualifier of the argument (using lexicographic
	 * string comparison).
	 * </p>
	 * @deprecated use {@link VersionRange}
	 * @param id the other version identifier
	 * @return <code>true</code> is this version identifier
	 *    matches on micro to the given version identifier, and
	 *    <code>false</code> otherwise
	 */
	public boolean matchMicro(Version id) {
		if (id == null)
			return false;
		if (major != id.getMajorComponent() || minor != id.getMinorComponent() || micro != id.getMicroComponent())
			return false;
		if (qualifier.compareTo(id.getQualifierComponent()) >= 0)
			return true;
		else
			return false;
	}

	/**
	 * Compares two version identifiers for perfect equality.
	 * <p>
	 * Two version identifiers are considered to be perfectly equal if their
	 * major, minor, micro and qualifier components are equal
	 * </p>
	 * @deprecated use {@link VersionRange}
	 * @param id the other version identifier
	 * @return <code>true</code> is this version identifier
	 *    is perfectly equal to the given version identifier, and
	 *    <code>false</code> otherwise
	 */
	public boolean matchQualifier(Version id) {
		return equals(id);
	}

	/**
	 * Compares two version identifiers for order using multi-decimal
	 * comparison. 
	 *
	 * @param id the other version identifier
	 * @return <code>true</code> is this version identifier
	 *    is greater than the given version identifier, and
	 *    <code>false</code> otherwise
	 */
	public boolean isGreaterThan(Version id) {

		if (id == null) {
			if (major == 0 && minor == 0 && micro == 0 && qualifier.equals("")) //$NON-NLS-1$
				return false;
			else
				return true;
		}

		if (major > id.getMajorComponent())
			return true;
		if (major < id.getMajorComponent())
			return false;
		if (minor > id.getMinorComponent())
			return true;
		if (minor < id.getMinorComponent())
			return false;
		if (micro > id.getMicroComponent())
			return true;
		if (micro < id.getMicroComponent())
			return false;
		if (qualifier.compareTo(id.getQualifierComponent()) > 0)
			return true;
		else
			return false;

	}

	/**
	 * Returns the string representation of this version identifier. 
	 * The result satisfies
	 * <code>vi.equals(new PluginVersionIdentifier(vi.toString()))</code>.
	 *
	 * @return the string representation of this plug-in version identifier
	 */
	public String toString() {
		String base = major + SEPARATOR + minor + SEPARATOR + micro;
		// R1.0 result
		if (qualifier.equals("")) //$NON-NLS-1$
			return base;
		else
			return base + SEPARATOR + qualifier;
	}

	private static String verifyQualifier(String s) {
		char[] chars = s.trim().toCharArray();
		boolean whitespace = false;
		for (int i = 0; i < chars.length; i++) {
			char c = chars[i];
			if (!(Character.isLetter(c) || Character.isDigit(c))) {
				chars[i] = '-';
				whitespace = true;
			}
		}
		return whitespace ? new String(chars) : s;
	}

	/**
	 * Compares this Version object with the specified Version object for order.  
	 * Returns a negative integer, zero, or a positive integer as this object is less
	 * than, equal to, or greater than the specified object.<p>
	 *
	 * @param   o the Version object to be compared.
	 * @return  a negative integer, zero, or a positive integer as this object
	 *		is less than, equal to, or greater than the specified Version object.
	 *
	 * @throws ClassCastException if the specified object's type
	 *         is not Version.
	 */
	public int compareTo(Object o) {
		if (!(o instanceof Version))
			throw new ClassCastException();

		if (equals(o))
			return 0;

		if (isGreaterThan((Version) o))
			return 1;

		return -1;
	}
}