/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2004-2006, Geotools Project Managment Committee (PMC)
 *    (C) 2004, Institut de Recherche pour le Développement
 *
 *    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; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    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.
 *
 *    This package contains documentation from OpenGIS specifications.
 *    OpenGIS consortium's work is fully acknowledged here.
 */
package org.geotools.util;

// J2SE dependencies
import java.io.Serializable;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

// OpenGIS dependencies
import org.opengis.util.NameSpace;
import org.opengis.util.LocalName;
import org.opengis.util.ScopedName;
import org.opengis.util.InternationalString;

// Geotools dependencies
import org.geotools.resources.Utilities;


/**
 * Base class for {@linkplain ScopedName generic scoped} and
 * {@linkplain LocalName local name} structure for type and attribute
 * name in the context of name spaces.
 *
 * <P>Note: this class has a natural ordering that is inconsistent with equals.
 *    The natural ordering may be case-insensitive and ignore the character
 *    separator between name elements.</P>
 *
 * @since 2.1
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/metadata/src/main/java/org/geotools/util/GenericName.java $
 * @version $Id: GenericName.java 23632 2006-12-29 22:13:51Z desruisseaux $
 * @author Martin Desruisseaux
 *
 * @see NameFactory
 */
public abstract class GenericName implements org.opengis.util.GenericName, Serializable {
    /**
     * Serial number for interoperability with different versions.
     */
    private static final long serialVersionUID = 8685047583179337259L;

    /**
     * The default separator character.
     */
    public static final char DEFAULT_SEPARATOR = ':';

    /**
     * Creates a new instance of generic name.
     */
    protected GenericName() {
    }

    /**
     * Ensures that the given name is a {@link String} or an {@link InternationalString}.
     * This is used for subclass constructors.
     */
    static CharSequence validate(final CharSequence name) {
        return (name==null || name instanceof InternationalString) ? name : name.toString();
    }
    
    /**
     * Returns the scope (name space) in which this name is local. The scope is set on creation
     * and is not modifiable. The scope of a name determines where a name "starts". For instance,
     * if a name has a {@linkplain #depth depth} of two ({@code "util.GenericName"}) and is
     * associated with a {@linkplain NameSpace name space} having the name {@code "org.opengis"},
     * then the fully qualified name would be {@code "org.opengis.util.GenericName"}.
     *
     * @since 2.3
     */
    public abstract NameSpace scope();

    /**
     * Returns the depth of this name within the namespace hierarchy.  This indicates the number
     * of levels specified by this name.  For any {@link LocalName}, it is always one.  For a
     * {@link ScopedName} it is some number greater than or equal to 2.
     * <p>
     * The depth is the length of the list returned by the {@link #getParsedNames} method.
     * As such it is a derived parameter.
     *
     * @since 2.3
     */
    public abstract int depth();
    
    /**
     * Returns the sequence of {@linkplain LocalName local names} making this generic name.
     * Each element in this list is like a directory name in a file path name.
     * The length of this sequence is the generic name depth.
     */
    public abstract List getParsedNames();

    /**
     * Returns the last element in the sequence of {@linkplain #getParsedNames parsed names}.
     * For any {@link LocalName}, this is always {@code this}.
     *
     * @see LocalName#name
     * @see ScopedName#name
     *
     * @since 2.3
     */
    public abstract LocalName name();

    /**
     * Returns the separator character. Default to <code>':'</code>.
     * This method is overridden by {@link org.geotools.util.ScopedName}.
     */
    char getSeparator() {
        return DEFAULT_SEPARATOR;
    }

    /**
     * Returns a string representation of this generic name. This string representation
     * is local-independant. It contains all elements listed by {@link #getParsedNames}
     * separated by an arbitrary character (usually {@code :} or {@code /}).
     * This rule implies that the {@code toString()} method for a
     * {@linkplain ScopedName scoped name} will contains the scope, while the
     * {@code toString()} method for the {@linkplain LocalName local version} of
     * the same name will not contains the scope.
     */
    public String toString() {
        final StringBuffer buffer = new StringBuffer();
        final List    parsedNames = getParsedNames();
        final char      separator = getSeparator();
        for (final Iterator it=parsedNames.iterator(); it.hasNext();) {
            if (buffer.length() != 0) {
                buffer.append(separator);
            }
            buffer.append(it.next());
        }
        return buffer.toString();
    }

    /**
     * Returns a local-dependent string representation of this generic name. This string
     * is similar to the one returned by {@link #toString} except that each element has
     * been localized in the {@linkplain InternationalString#toString(Locale)
     * specified locale}. If no international string is available, then this method should
     * returns an implementation mapping to {@link #toString} for all locales.
     */
    public InternationalString toInternationalString() {
        return new International(getParsedNames(), getSeparator());
    }

    /**
     * An international string built from a snapshot of {@link GenericName}.
     *
     * @version $Id: GenericName.java 23632 2006-12-29 22:13:51Z desruisseaux $
     * @author Martin Desruisseaux
     */
    private static final class International extends AbstractInternationalString
                                          implements Serializable
    {
        /**
         * Serial number for interoperability with different versions.
         */
        private static final long serialVersionUID = -4234089612436334148L;

        /**
         * The sequence of {@linkplain LocalName local names} making this generic name.
         * This is the value returned by {@link GenericName#getParsedNames}.
         */
        private final List parsedNames;

        /**
         * The separator character. This is the value returned by {@link GenericName#getSeparator}.
         */
        private final char separator;

        /**
         * Constructs a new international string from the specified {@link GenericName} fields.
         *
         * @param parsedNames The value returned by {@link GenericName#getParsedNames}.
         * @param separator   The value returned by {@link GenericName#getSeparator}.
         */
        public International(final List parsedNames, final char separator) {
            this.parsedNames = parsedNames;
            this.separator   = separator;
        }

        /**
         * Returns a string representation for the specified locale.
         */
        public String toString(final Locale locale) {
            final StringBuffer buffer = new StringBuffer();
            for (final Iterator it=parsedNames.iterator(); it.hasNext();) {
                if (buffer.length() != 0) {
                    buffer.append(separator);
                }
                buffer.append(((org.opengis.util.GenericName) it.next())  // Remove cast with 1.5
                              .toInternationalString().toString(locale));
            }
            return buffer.toString();
        }

        /**
         * Compares this international string with the specified object for equality.
         */
        public boolean equals(final Object object) {
            if (object!=null && object.getClass().equals(getClass())) {
                final International that = (International) object;
                return Utilities.equals(this.parsedNames, that.parsedNames) &&
                                        this.separator == that.separator;
            }
            return false;
        }

        /**
         * Returns a hash code value for this international text.
         */
        public int hashCode() {
            return (int)serialVersionUID ^ parsedNames.hashCode();
        }
    }

    /**
     * Compares this name with the specified object for order. Returns a negative integer,
     * zero, or a positive integer as this name lexicographically precedes, is equals to,
     * or follows the specified object. The comparaison is performed in the following
     * order:
     * <ul>
     *   <li>Compares each element in the {@linkplain #getParsedNames list of parsed names}. If an
     *       element of this name lexicographically precedes or follows the corresponding element
     *       of the specified name, returns a negative or a positive integer respectively.</li>
     *   <li>If all elements in both names are lexicographically equal, then if this name has less
     *       or more elements than the specified name, returns a negative or a positive integer
     *       respectively.</li>
     *   <li>Otherwise, returns 0.</li>
     * </ul>
     */
    public int compareTo(final Object object) {
        final org.opengis.util.GenericName that = (org.opengis.util.GenericName) object;
        final Iterator thisNames = this.getParsedNames().iterator();
        final Iterator thatNames = that.getParsedNames().iterator();
        while (thisNames.hasNext()) {
            if (!thatNames.hasNext()) {
                return +1;
            }
            final Comparable thisNext = (Comparable) thisNames.next();
            final Comparable thatNext = (Comparable) thatNames.next();
            if (thisNext==this && thatNext==that) {
                // Never-ending loop: usually an implementation error
                throw new IllegalStateException();
            }
            final int compare = thisNext.compareTo(thatNext);
            if (compare != 0) {
                return compare;
            }
        }
        return thatNames.hasNext() ? -1 : 0;
    }

    /**
     * Compares this generic name with the specified object for equality.
     */
    public boolean equals(final Object object) {
        if (object!=null && object.getClass().equals(getClass())) {
            final GenericName that = (GenericName) object;
            return Utilities.equals(this.getParsedNames(), that.getParsedNames()) &&
                                    this.getSeparator() == that.getSeparator();
        }
        return false;
    }

    /**
     * Returns a hash code value for this generic name.
     */
    public int hashCode() {
        return (int)serialVersionUID ^ getParsedNames().hashCode();
    }
}
