/*
 * 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.sis.util.iso;

import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Iterator;
import java.util.ConcurrentModificationException;
import java.io.Serializable;
import jakarta.xml.bind.annotation.XmlTransient;
import org.opengis.util.NameFactory;
import org.opengis.util.NameSpace;
import org.opengis.util.LocalName;
import org.opengis.util.ScopedName;
import org.opengis.util.GenericName;
import org.opengis.util.InternationalString;
import org.apache.sis.util.SimpleInternationalString;
import org.apache.sis.util.resources.Errors;


/**
 * Base class for sequence of identifiers rooted within the context of a {@linkplain DefaultNameSpace namespace}.
 * Names shall be <em>immutable</em> and thread-safe. A name can be local to a namespace.
 * See the {@linkplain org.apache.sis.util.iso package javadoc} for an illustration of name anatomy.
 *
 * <p>The easiest way to create a name is to use the {@link Names#createLocalName(CharSequence, String, CharSequence)}
 * convenience static method. That method supports the common case where the name is made only of a
 * (<var>namespace</var>, <var>local part</var>) pair of strings. However, generic names allows finer grain.
 * For example, the above-cited strings can both be split into smaller name components.
 * If such finer grain control is desired, {@link DefaultNameFactory} can be used instead of {@link Names}.</p>
 *
 * <h2>Natural ordering</h2>
 * This class has a natural ordering that is inconsistent with {@link #equals(Object)}.
 * See {@link #compareTo(GenericName)} for more information.
 *
 * <h2>Note for implementers</h2>
 * Subclasses need only to implement the following methods:
 * <ul>
 *   <li>{@link #scope()}</li>
 *   <li>{@link #getParsedNames()}</li>
 * </ul>
 *
 * Subclasses shall make sure that any overridden methods remain safe to call from multiple threads
 * and do not change any public {@code GenericName} state.
 *
 * @author  Martin Desruisseaux (IRD, Geomatys)
 * @version 1.4
 *
 * @see org.apache.sis.referencing.NamedIdentifier
 * @see org.apache.sis.storage.FeatureNaming
 * @see org.apache.sis.feature.AbstractIdentifiedType#getName()
 * @see org.apache.sis.referencing.AbstractIdentifiedObject#getAlias()
 *
 * @since 0.3
 */

/*
 * JAXB annotation would be @XmlType(name = "CodeType"), but this cannot be used here
 * since "CodeType" is used for various classes (including LocalName and ScopedName).
 */
@XmlTransient
public abstract class AbstractName implements GenericName, Serializable {
    /**
     * Serial number for inter-operability with different versions.
     */
    private static final long serialVersionUID = 667242702456713391L;

    /**
     * A view of this name as a fully-qualified one.
     * Will be created only when first needed.
     */
    transient GenericName fullyQualified;

    /**
     * The string representation of this name, to be returned by {@link #toString()} or
     * {@link #toInternationalString()}. This field will initially references a {@link String}
     * object when first needed, and may be replaced by a {@link InternationalString} object
     * later if such object is asked for.
     */
    transient CharSequence asString;

    /**
     * The cached hash code, or {@code 0} if not yet computed.
     */
    private transient int hash;

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

    /**
     * Returns a SIS name implementation with the values of the given arbitrary implementation.
     * This method performs the first applicable action in the following choices:
     *
     * <ul>
     *   <li>If the given object is {@code null}, then this method returns {@code null}.</li>
     *   <li>Otherwise if the given object is an instance of {@link LocalName}, then this
     *       method delegates to {@link DefaultLocalName#castOrCopy(LocalName)}.</li>
     *   <li>Otherwise if the given object is already an instance of {@code AbstractName},
     *       then it is returned unchanged.</li>
     *   <li>Otherwise a new instance of an {@code AbstractName} subclass is created using the
     *       {@link DefaultNameFactory#createGenericName(NameSpace, CharSequence[])} method.</li>
     * </ul>
     *
     * @param  object  the object to get as a SIS implementation, or {@code null} if none.
     * @return a SIS implementation containing the values of the given object (may be the
     *         given object itself), or {@code null} if the argument was null.
     */
    public static AbstractName castOrCopy(final GenericName object) {
        if (object instanceof LocalName) {
            return DefaultLocalName.castOrCopy((LocalName) object);
        }
        if (object == null || object instanceof AbstractName) {
            return (AbstractName) object;
        }
        /*
         * Recreates a new name for the given name in order to get
         * a SIS implementation from an arbitrary implementation.
         */
        final List<? extends LocalName> parsedNames = object.getParsedNames();
        final CharSequence[] names = new CharSequence[parsedNames.size()];
        int i=0;
        for (final LocalName component : parsedNames) {
            names[i++] = component.toInternationalString();
        }
        if (i != names.length) {
            throw new ConcurrentModificationException(Errors.format(Errors.Keys.UnexpectedChange_1, "parsedNames"));
        }
        /*
         * Following cast should be safe because we know that the
         * Apache SIS implementation creates `AbstractName` instances.
         */
        final NameFactory factory = DefaultNameFactory.provider();
        return (AbstractName) factory.createGenericName(object.scope(), names);
    }

    /**
     * Returns the scope (name space) in which this name is local. For example if a
     * {@linkplain #toFullyQualifiedName() fully qualified name} is {@code "org.opengis.util.Record"}
     * and if this instance is the {@code "util.Record"} part, then its scope is
     * {@linkplain DefaultNameSpace#name() named} {@code "org.opengis"}.
     *
     * <p>Continuing with the above example, the full {@code "org.opengis.util.Record"} name has
     * no scope. If this method is invoked on such name, then the SIS implementation returns a
     * global scope instance (i.e. an instance for which {@link DefaultNameSpace#isGlobal()}
     * returns {@code true}) which is unique and named {@code "global"}.</p>
     *
     * @return the scope of this name.
     */
    @Override
    public abstract NameSpace scope();

    /**
     * Indicates the number of levels specified by this name. The default implementation returns
     * the size of the list returned by the {@link #getParsedNames()} method.
     *
     * @return the depth of this name.
     */
    @Override
    public int depth() {
        return getParsedNames().size();
    }

    /**
     * Returns the size of the backing array. This is used only has a hint for optimizations
     * in attempts to share internal arrays. The {@link DefaultScopedName} class is the only
     * one to override this method. For other classes, the {@link #depth()} can be assumed.
     */
    int arraySize() {
        return depth();
    }

    /**
     * Returns the sequence of {@linkplain DefaultLocalName local names} making this generic name.
     * The length of this sequence is the {@linkplain #depth() depth}. It does not include the
     * {@linkplain #scope() scope}.
     *
     * @return the local names making this generic name, without the {@linkplain #scope() scope}.
     *         Shall never be {@code null} neither empty.
     */
    @Override
    public abstract List<? extends LocalName> getParsedNames();

    /**
     * Returns the first element in the sequence of {@linkplain #getParsedNames() parsed names}.
     * For any {@code LocalName}, this is always {@code this}.
     *
     * <h4>Example</h4>
     * If {@code this} name is {@code "org.opengis.util.Record"}
     * (no matter its scope, then this method returns {@code "org"}.
     *
     * @return the first element in the list of {@linkplain #getParsedNames() parsed names}.
     */
    @Override
    public LocalName head() {
        return getParsedNames().get(0);
    }

    /**
     * Returns the last element in the sequence of {@linkplain #getParsedNames() parsed names}.
     * For any {@code LocalName}, this is always {@code this}.
     *
     * <h4>Example</h4>
     * If {@code this} name is {@code "org.opengis.util.Record"}
     * (no matter its scope, then this method returns {@code "Record"}.
     *
     * @return the last element in the list of {@linkplain #getParsedNames() parsed names}.
     */
    @Override
    public LocalName tip() {
        final List<? extends LocalName> names = getParsedNames();
        return names.get(names.size() - 1);
    }

    /**
     * Returns a view of this name as a fully-qualified name. The {@linkplain #scope() scope}
     * of a fully qualified name is {@linkplain DefaultNameSpace#isGlobal() global}.
     * If the scope of this name is already global, then this method returns {@code this}.
     *
     * @return the fully-qualified name (never {@code null}).
     */
    @Override
    public synchronized GenericName toFullyQualifiedName() {
        if (fullyQualified == null) {
            final NameSpace scope = scope();
            if (scope.isGlobal()) {
                fullyQualified = this;
            } else {
                final GenericName prefix = scope.name();
                assert prefix.scope().isGlobal() : prefix;
                fullyQualified = new DefaultScopedName(prefix, this);
            }
        }
        return fullyQualified;
    }

    /**
     * Returns this name expanded with the specified scope. One may represent this operation
     * as a concatenation of the specified {@code scope} with {@code this}. For example, if
     * {@code this} name is {@code "util.Record"} and the given {@code scope} argument is
     * {@code "org.opengis"}, then {@code this.push(scope)} shall return
     * {@code "org.opengis.util.Record"}.
     *
     * @param  scope  the name to use as prefix.
     * @return a concatenation of the given scope with this name.
     */
    @Override
    public ScopedName push(final GenericName scope) {
        return new DefaultScopedName(scope, this);
    }

    /**
     * Returns the separator to write before the given name. If the scope of the given name
     * is a {@link DefaultNameSpace} instance, then this method returns its head separator.
     * We really want {@link DefaultNameSpace#headSeparator}, not {@link DefaultNameSpace#separator}.
     * See {@link DefaultNameSpace#child(CharSequence, String)} for details.
     *
     * @param  name  the name after which to write a separator.
     * @return the separator to write after the given name.
     */
    private static String headSeparator(final GenericName name) {
        return (name != null) ? DefaultNameSpace.getSeparator(name.scope(), true)
                              : DefaultNameSpace.DEFAULT_SEPARATOR_STRING;
    }

    /**
     * Returns a string representation of this generic name. This string representation
     * is local-independent. It contains all elements listed by {@link #getParsedNames()}
     * separated by a namespace-dependent character (usually {@code ':'} or {@code '/'}).
     * This rule implies that the result may or may not be fully qualified.
     * Special cases:
     *
     * <ul>
     *   <li><code>{@linkplain #toFullyQualifiedName()}.toString()</code> is guaranteed to
     *       contain the {@linkplain #scope() scope} (if any).</li>
     *   <li><code>{@linkplain #tip()}.toString()</code> is guaranteed to not contain
     *       any scope.</li>
     * </ul>
     *
     * @return a local-independent string representation of this name.
     */
    @Override
    public synchronized String toString() {
        if (asString == null) {
            boolean insertSeparator = false;
            final StringBuilder buffer = new StringBuilder();
            for (final LocalName name : getParsedNames()) {
                if (insertSeparator) {
                    buffer.append(headSeparator(name));
                }
                insertSeparator = true;
                buffer.append(name);
            }
            asString = buffer.toString();
        }
        /*
         * Note: there is no need to invoke InternationalString.toString(Locale.ROOT) for
         * the unlocalized version, because our International inner class is implemented in
         * such a way that InternationalString.toString() returns AbstractName.toString().
         */
        return asString.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 returns an implementation mapping
     * to {@link #toString()} for all locales.
     *
     * @return a localizable string representation of this name.
     */
    @Override
    public synchronized InternationalString toInternationalString() {
        if (!(asString instanceof InternationalString)) {
            asString = new International(toString(), getParsedNames());
        }
        return (InternationalString) asString;
    }

    /**
     * An international string built from a snapshot of {@link GenericName}.
     * This class is immutable if the list given to the constructor is immutable.
     *
     * @author  Martin Desruisseaux (IRD, Geomatys)
     */
    private static final class International extends SimpleInternationalString {
        /**
         * Serial number for inter-operability with different versions.
         */
        private static final long serialVersionUID = -5259001179796274879L;

        /**
         * The sequence of {@linkplain DefaultLocalName local names} making this generic name.
         * This is the value returned by {@link AbstractName#getParsedNames()}.
         */
        @SuppressWarnings("serial")
        private final List<? extends LocalName> parsedNames;

        /**
         * Constructs a new international string from the specified {@link AbstractName} fields.
         *
         * @param  asString     the string representation of the enclosing abstract name.
         * @param  parsedNames  the value returned by {@link AbstractName#getParsedNames()}.
         */
        International(final String asString, final List<? extends LocalName> parsedNames) {
            super(asString);
            this.parsedNames = parsedNames;
        }

        /**
         * Returns a string representation for the specified locale.
         */
        @Override
        public String toString(final Locale locale) {
            boolean insertSeparator = false;
            final StringBuilder buffer = new StringBuilder();
            for (final LocalName name : parsedNames) {
                if (insertSeparator) {
                    buffer.append(headSeparator(name));
                }
                insertSeparator = true;
                buffer.append(name.toInternationalString().toString(locale));
            }
            return buffer.toString();
        }

        /**
         * Compares this international string with the specified object for equality.
         */
        @Override
        public boolean equals(final Object object) {
            if (object == this) {
                return true;
            }
            if (super.equals(object)) {
                final International that = (International) object;
                return Objects.equals(parsedNames, that.parsedNames);
            }
            return false;
        }

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

    /**
     * Compares this name with the specified name for order. Returns a negative integer,
     * zero, or a positive integer as this name lexicographically precedes, is equal to,
     * or follows the specified name. The comparison is performed in the following way:
     *
     * <ul>
     *   <li>For each element of the {@linkplain #getParsedNames() list of parsed names} taken
     *       in iteration order, compare the {@link LocalName}. If a 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>
     *
     * @param  name  the other name to compare with this name.
     * @return -1 if this name precedes the given one, +1 if it follows, 0 if equals.
     */
    @Override
    public int compareTo(final GenericName name) {
        final Iterator<? extends LocalName> thisNames = this.getParsedNames().iterator();
        final Iterator<? extends LocalName> thatNames = name.getParsedNames().iterator();
        while (thisNames.hasNext()) {
            if (!thatNames.hasNext()) {
                return +1;
            }
            final LocalName thisNext = thisNames.next();
            final LocalName thatNext = thatNames.next();
            if (thisNext == this && thatNext == name) {
                // Never-ending loop: usually an implementation error
                throw new IllegalStateException(Errors.format(Errors.Keys.CircularReference));
            }
            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.
     * The default implementation returns {@code true} if the {@linkplain #scope() scopes}
     * and the lists of {@linkplain #getParsedNames() parsed names} are equal.
     *
     * @param  object  the object to compare with this name for equality.
     * @return {@code true} if the given object is equal to this name.
     */
    @Override
    public boolean equals(final Object object) {
        if (object == this) {
            return true;
        }
        if (object != null && object.getClass() == getClass()) {
            final AbstractName that = (AbstractName) object;
            return Objects.equals(scope(), that.scope()) &&
                   Objects.equals(getParsedNames(), that.getParsedNames());
        }
        return false;
    }

    /**
     * Returns a hash code value for this generic name.
     */
    @Override
    public int hashCode() {
        if (hash == 0) {
            int code = computeHashCode();
            if (code == 0) {
                code = -1;
            }
            hash = code;
        }
        return hash;
    }

    /**
     * Invoked by {@link #hashCode()} for computing the hash code value when first needed.
     */
    int computeHashCode() {
        return Objects.hash(scope(), getParsedNames()) ^ (int) serialVersionUID;
    }
}
