/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2006-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.util;

import java.io.Serializable;
import java.util.regex.Pattern;


/**
 * Holds a version number. Versions are often of the form <code>{@linkplain #getMajor
 * major}.{@linkplain #getMinor minor}.{@linkplain #getRevision revision}</code>, but
 * are not required to. For example an EPSG database version is {@code "6.11.2"}. The
 * separator character is the dot.
 * <p>
 * This class provides convenience methods for fetching the major, minor and reversion
 * numbers, and for performing comparaisons.
 *
 * @since 2.4
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/library/metadata/src/main/java/org/geotools/util/Version.java $
 * @version $Id: Version.java 37298 2011-05-25 05:16:15Z mbedward $
 * @author Martin Desruisseaux
 *
 * @see org.geotools.factory.GeoTools#getVersion
 */
public class Version implements CharSequence, Comparable<Version>, Serializable {
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = -6793384507333713770L;

    /**
     * The pattern to use for splitting version numbers.
     */
    private static final Pattern PATTERN = Pattern.compile("(\\.|\\-)");

    /**
     * The version in string form, with leading and trailing spaces removed.
     */
    private final String version;

    /**
     * The components of the version string. Will be created when first needed.
     */
    private transient String[] components;

    /**
     * The parsed components of the version string. Will be created when first needed.
     */
    private transient Comparable<?>[] parsed;

    /**
     * The hash code value. Will be computed when first needed.
     */
    private transient int hashCode;

    /**
     * Creates a new version object from the supplied string.
     *
     * @param version The version as a string.
     */
    public Version(final String version) {
        this.version = version.trim();
    }

    /**
     * Returns the specified components of this version string. For a version of the
     * {@code major.minor.revision} form, index 0 stands for the major version number,
     * 1 stands for the minor version number and 2 stands for the revision number.
     * <p>
     * The return value is an {@link Integer} if the component is parsable as an integer,
     * or a {@link String} otherwise. If there is no component at the specified index,
     * then this method returns {@code null}.
     *
     * @param  index The index of the component to fetch.
     * @return The value at the specified index, or {@code null} if none.
     * @throws IndexOutOfBoundsException if {@code index} is negative.
     */
    public synchronized Comparable<?> getComponent(final int index) {
        if (parsed == null) {
            if (components == null) {
                components = PATTERN.split(version);
            }
            parsed = new Comparable[components.length];
        }
        if (index >= parsed.length) {
            return null;
        }
        Comparable<?> candidate = parsed[index];
        if (candidate == null) {
            final String value = components[index].trim();
            try {
                candidate = Integer.valueOf(value);
            } catch (NumberFormatException e) {
                candidate = value;
            }
            parsed[index] = candidate;
        }
        return candidate;
    }

    /**
     * Get the rank of the specified object according this type.
     * This is for {@link #compareTo(Version, int)} internal only.
     */
    private static int getTypeRank(final Object value) {
        if (value instanceof CharSequence) {
            return 0;
        }
        if (value instanceof Number) {
            return 1;
        }
        throw new IllegalArgumentException(String.valueOf(value));
    }

    /**
     * Compares this version with an other version object, up to the specified limit. A limit
     * of 1 compares only the {@linkplain #getMajor major} version number. A limit of 2 compares
     * the major and {@linkplain #getMinor minor} version numbers, <cite>etc</cite>. The
     * comparaisons are performed as {@link Integer} object if possible, or as {@link String}
     * otherwise.
     *
     * @param  other The other version object to compare with.
     * @param  limit The maximum number of components to compare.
     * @return A negative value if this version is lower than the supplied version, a positive
     *         value if it is higher, or 0 if they are equal.
     */
    public int compareTo(final Version other, final int limit) {
        for (int i=0; i<limit; i++) {
            final Comparable<?> v1 =  this.getComponent(i);
            final Comparable<?> v2 = other.getComponent(i);
            if (v1 == null) {
                return (v2 == null) ? 0 : -1;
            } else if (v2 == null) {
                return +1;
            }
            final int dr = getTypeRank(v1) - getTypeRank(v2);
            if (dr != 0) {
                /*
                 * One value is a text while the other value is a number.  We could be tempted to
                 * force a comparaison by converting the number to a String and then invoking the
                 * String.compareTo(String) method, but this strategy would violate the following
                 * contract from Comparable.compareTo(Object):  "The implementor must also ensure
                 * that the relation is transitive". Use case:
                 *
                 *    A is the integer 10
                 *    B is the string "8Z"
                 *    C is the integer 5.
                 *
                 * If mismatched types are converted to String before being compared, then we
                 * would have A < B < C. Transitivity implies that A < C, but if we compare A
                 * and C directly we get A > C because they are compared as numbers.  An easy
                 * way to fix this inconsistency is to define all String as lexicographically
                 * preceding Integer, no matter their content. This is what we do here.
                 */
                return dr;
            }
            @SuppressWarnings("unchecked")
            final int c = ((Comparable) v1).compareTo(v2);
            if (c != 0) {
                return c;
            }
        }
        return 0;
    }

    /**
     * Compares this version with an other version object. This method performs the same
     * comparaison than {@link #compareTo(Version, int)} with no limit.
     *
     * @param  other The other version object to compare with.
     * @return A negative value if this version is lower than the supplied version, a positive
     *         value if it is higher, or 0 if they are equal.
     */
    public int compareTo(final Version other) {
        return compareTo(other, Integer.MAX_VALUE);
    }

    /**
     * Compare this version string with the specified object for equality. Two version are
     * considered equal if <code>{@linkplain #compareTo(Object) compareTo}(other) == 0</code>.
     *
     * @param other The object to compare with this version for equality.
     */
    @Override
    public boolean equals(final Object other) {
        if (other != null && getClass().equals(other.getClass())) {
            return compareTo((Version) other) == 0;
        }
        return false;
    }

    /**
     * Returns the length of the version string.
     */
    public int length() {
        return version.length();
    }

    /**
     * Returns the {@code char} value at the specified index.
     */
    public char charAt(final int index) {
        return version.charAt(index);
    }

    /**
     * Returns a new version string that is a subsequence of this sequence.
     */
    public CharSequence subSequence(final int start, final int end) {
        return version.subSequence(start, end);
    }

    /**
     * Returns the version string. This is the string specified at construction time.
     */
    @Override
    public String toString() {
        return version;
    }

    /**
     * Returns a hash code value for this version.
     */
    @Override
    public int hashCode() {
        if (hashCode == 0) {
            int code = (int) serialVersionUID;
            int index = 0;
            Comparable<?> component;
            while ((component = getComponent(index)) != null) {
                code = code * 37 + component.hashCode();
                index++;
            }
            hashCode = code;
        }
        return hashCode;
    }
}
