/*
 * 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.xml.bind;

import java.net.URI;
import java.util.Set;
import java.util.List;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Collection;
import java.util.AbstractMap;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.io.Serializable;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.citation.Citation;
import org.apache.sis.xml.XLink;
import org.apache.sis.xml.IdentifierMap;
import org.apache.sis.xml.IdentifierSpace;
import org.apache.sis.util.internal.shared.Strings;
import org.apache.sis.util.internal.shared.SetOfUnknownSize;
import org.apache.sis.util.collection.Containers;
import org.apache.sis.pending.jdk.JDK19;


/**
 * Implementation of the map of identifiers associated to {@link org.apache.sis.xml.IdentifiedObject} instances.
 * This base class implements an unmodifiable map, but the {@link ModifiableIdentifierMap} subclass adds write
 * capabilities.
 *
 * <p>This class works as a wrapper around a collection of identifiers. Because all operations
 * are performed by an iteration over the collection elements, this implementation is suitable
 * only for small maps (less than 10 elements). Given that objects typically have only one or
 * two identifiers, this is considered acceptable.</p>
 *
 * <h2>Special cases</h2>
 * The identifiers for the following authorities are handled in a special way:
 * <ul>
 *   <li>{@link IdentifierSpace#HREF}: handled as a shortcut to {@link XLink#getHRef()}.</li>
 * </ul>
 *
 * <h2>Handling of duplicated authorities</h2>
 * The collection shall not contain more than one identifier for the same
 * {@linkplain Identifier#getAuthority() authority}. However, duplications may happen if the user
 * has direct access to the list, for example through {@link Citation#getIdentifiers()}. If such
 * duplication is found, then this map implementation applies the following rules:
 *
 * <ul>
 *   <li>All getter methods (including the iterators and the values returned by the {@code put}
 *       and {@code remove} methods) return only the identifier code associated to the first
 *       occurrence of each authority. Any subsequent occurrences of the same authorities are
 *       silently ignored.</li>
 *   <li>All setter methods <em>may</em> affect <em>all</em> identifiers previously associated to
 *       the given authority, not just the first occurrence. The only guarantee is that the list
 *       is update in such a way that the effect of setter methods are visible to subsequent calls
 *       to getter methods.</li>
 * </ul>
 *
 * <h2>Handling of null identifiers</h2>
 * The collection of identifiers shall not contain any null element. This is normally ensured by
 * the {@link org.apache.sis.metadata.ModifiableMetadata} internal collection implementations.
 * This class performs opportunist null checks as an additional safety, but consistency is not
 * guaranteed. See {@link #size()} for more information.
 *
 * <h2>Thread safety</h2>
 * This class is thread safe if the underlying identifier collection is thread safe.
 *
 * @author  Martin Desruisseaux (Geomatys)
 *
 * @see org.apache.sis.xml.IdentifiedObject
 */
public class IdentifierMapAdapter extends AbstractMap<Citation,String> implements IdentifierMap, Serializable {
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = -1445849218952061605L;

    /**
     * An immutable empty instance.
     */
    public static final IdentifierMap EMPTY = new IdentifierMapAdapter(Set.of());

    /**
     * The identifiers to wrap in a map view.
     *
     * @see #getIdentifiers(Class)
     */
    @SuppressWarnings("serial")         // Most SIS implementations are serializable.
    public final Collection<Identifier> identifiers;

    /**
     * Creates a new map which will be a view over the given identifiers.
     *
     * @param  identifiers  the identifiers to wrap in a map view.
     */
    public IdentifierMapAdapter(final Collection<Identifier> identifiers) {
        this.identifiers = identifiers;
    }

    /**
     * Returns the identifiers as a collection of the specified type.
     * The given type is the return type of the {@code getIdentifiers()} method which is delegating to this method.
     * The returned collection is modifiable only if {@link #identifiers} is already of the desired type.
     * This is the case for {@link org.apache.sis.metadata.iso.ISOMetadata#getIdentifiers()},
     * which is the API for which we want modifiable collections.
     *
     * @param  type  {@code Collection.class}, {@code List.class} or {@code Set.class}.
     * @return the identifiers as a collection of the specified type.
     */
    @SuppressWarnings("ReturnOfCollectionOrArrayField")
    public final Collection<Identifier> getIdentifiers(final Class<?> type) {
        if (!type.isInstance(identifiers)) {
            if (type.isAssignableFrom(Set.class)) {
                return Set.copyOf(identifiers);
            }
            if (type.isAssignableFrom(List.class)) {
                return List.copyOf(identifiers);
            }
        }
        return identifiers;
    }

    /**
     * If the given authority is a special case, returns its {@link NonMarshalledAuthority} integer enum.
     * Otherwise returns -1. See javadoc for more information about special cases.
     *
     * @param  authority  a {@link Citation} constant. The type is relaxed to {@code Object}
     *         because the signature of some {@code Map} methods are that way.
     */
    static int specialCase(final Object authority) {
        if (authority == IdentifierSpace.HREF) return NonMarshalledAuthority.HREF;
        // A future Apache SIS version may add more special cases here.
        return -1;
    }

    /**
     * Extracts the {@code xlink:href} value from the {@link XLink} if presents.
     * This method does not test if an explicit {@code xlink:href} identifier exists;
     * this check must be done by the caller <strong>before</strong> to invoke this method.
     *
     * @see ModifiableIdentifierMap#setHRef(URI)
     */
    private URI getHRef() {
        final Identifier identifier = getIdentifier(IdentifierSpace.XLINK);
        if (identifier instanceof SpecializedIdentifier<?>) {
            final Object link = ((SpecializedIdentifier<?>) identifier).value;
            if (link instanceof XLink) {
                return ((XLink) link).getHRef();
            }
        }
        return null;
    }

    /**
     * Returns the string representation of the given value, or {@code null} if none.
     *
     * @param  value  the value returned be one of the above {@code getFoo()} methods.
     */
    private static String toString(final Object value) {
        return (value != null) ? value.toString() : null;
    }




    //  ╔════════════════════════════════════════════════════════════════════════╗
    //  ║                                                                        ║
    //  ║    END OF SPECIAL CASES.                                               ║
    //  ║                                                                        ║
    //  ║    Implementation of IdentifierMap methods follow. Each method may     ║
    //  ║    have a switch statement over the special cases declared above.      ║
    //  ║                                                                        ║
    //  ╚════════════════════════════════════════════════════════════════════════╝

    /**
     * Whether this map support {@code put} and {@code remove} operations.
     */
    boolean isModifiable() {
        return false;
    }

    /**
     * Returns {@code true} if the collection of identifiers contains at least one element.
     * This method does not verify if the collection contains null element (it should not).
     */
    @Override
    public final boolean isEmpty() {
        return identifiers.isEmpty();
    }

    /**
     * Counts the number of entries, ignoring null elements and duplicated authorities.
     *
     * <p>Because {@code null} elements are ignored, this method may return 0 even if {@link #isEmpty()}
     * returns {@code false}. However, this inconsistency should not happen in practice because
     * {@link org.apache.sis.metadata.ModifiableMetadata} internal collection implementations
     * do not allow null values.</p>
     */
    @Override
    public final int size() {
        final HashSet<Citation> done = JDK19.newHashSet(identifiers.size());
        for (final Identifier identifier : identifiers) {
            if (identifier != null) {
                done.add(identifier.getAuthority());
            }
        }
        return done.size();
    }

    /**
     * Returns {@code true} if at least one identifier declares the given {@linkplain Identifier#getCode() code}.
     *
     * @param  code  the code to search, which should be an instance of {@link String}.
     * @return {@code true} if at least one identifier uses the given code.
     */
    @Override
    public final boolean containsValue(final Object code) {
        if (code instanceof String) {
            for (final Identifier identifier : identifiers) {
                if (identifier != null && code.equals(identifier.getCode())) {
                    return true;
                }
            }
            return code.equals(toString(getHRef()));
            // A future Apache SIS version may add more special cases here.
        }
        return false;
    }

    /**
     * Returns {@code true} if at least one identifier declares the given
     * {@linkplain Identifier#getAuthority() authority}.
     *
     * @param  authority  the authority to search, which should be an instance of {@link Citation}.
     * @return {@code true} if at least one identifier uses the given authority.
     */
    @Override
    public final boolean containsKey(final Object authority) {
        if (authority instanceof Citation) {
            if (getIdentifier((Citation) authority) != null) {
                return true;
            }
            switch (specialCase(authority)) {
                case NonMarshalledAuthority.HREF: return getHRef() != null;
                // A future Apache SIS version may add more special cases here.
            }
        }
        return false;
    }

    /**
     * Returns the identifier for the given key, or {@code null} if none.
     */
    final Identifier getIdentifier(final Citation authority) {
        for (final Identifier identifier : identifiers) {
            if (identifier != null && Objects.equals(authority, identifier.getAuthority())) {
                return identifier;
            }
        }
        return null;
    }

    /**
     * Returns the identifier associated with the given authority,
     * or {@code null} if no specialized identifier was found.
     */
    @Override
    @SuppressWarnings("unchecked")
    public final <T> T getSpecialized(final IdentifierSpace<T> authority) {
        final Identifier identifier = getIdentifier(authority);
        if (identifier instanceof SpecializedIdentifier<?>) {
            return ((SpecializedIdentifier<T>) identifier).value;
        }
        switch (specialCase(authority)) {
            case NonMarshalledAuthority.HREF: return (T) getHRef();
            // A future Apache SIS version may add more special cases here.
        }
        return null;
    }

    /**
     * Returns the code of the first identifier associated with the given
     * {@linkplain Identifier#getAuthority() authority}, or {@code null} if no identifier was found.
     *
     * @param  authority  the authority to search, which should be an instance of {@link Citation}.
     * @return the code of the identifier for the given authority, or {@code null} if none.
     */
    @Override
    public final String get(final Object authority) {
        if (authority instanceof Citation) {
            final Identifier identifier = getIdentifier((Citation) authority);
            if (identifier != null) {
                return identifier.getCode();
            }
            switch (specialCase(authority)) {
                case NonMarshalledAuthority.HREF: return toString(getHRef());
                // A future Apache SIS version may add more special cases here.
            }
        }
        return null;
    }

    /**
     * Removes all identifiers associated with the given {@linkplain Identifier#getAuthority() authority}.
     *
     * @param  authority  the authority to search, which should be an instance of {@link Citation}.
     * @return the code of the identifier for the given authority, or {@code null} if none.
     * @throws UnsupportedOperationException if the collection of identifiers is unmodifiable.
     */
    @Override
    public String remove(Object authority) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    /**
     * Removes every entries in the underlying collection.
     *
     * @throws UnsupportedOperationException if the collection of identifiers is unmodifiable.
     */
    @Override
    public void clear() throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    /**
     * Sets the code of the identifier having the given authority to the given value.
     * If no identifier is found for the given authority, a new one is created.
     * If more than one identifier is found for the given authority, then all previous identifiers may be removed
     * in order to ensure that the new entry will be the first entry, so it can be find by the {@code get} method.
     *
     * @param  authority  the authority for which to set the code.
     * @param  code  the new code for the given authority, or {@code null} for removing the entry.
     * @return the previous code for the given authority, or {@code null} if none.
     * @throws UnsupportedOperationException if the collection of identifiers is unmodifiable.
     */
    @Override
    public String put(Citation authority, String code) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    /**
     * Sets the identifier associated with the given authority, and returns the previous value.
     */
    @Override
    public <T> T putSpecialized(IdentifierSpace<T> authority, T value) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    /**
     * Returns a view over the collection of identifiers. This view supports removal operation
     * if the underlying collection of identifiers supports the {@link Iterator#remove()} method.
     *
     * <p>If the backing identifier collection contains null entries, those entries will be ignored.
     * If the backing collection contains many entries for the same authority, then only the first
     * occurrence is included.</p>
     *
     * @return a view over the collection of identifiers.
     */
    @Override
    public Set<Entry<Citation,String>> entrySet() {
        /*
         * Do not cache the entries set because if is very cheap to create and not needed very often.
         * Not caching allows this implementation to be thread-safe without synchronization or volatile
         * fields if the underlying list is thread-safe. Furthermore, IdentifierMapAdapter are temporary
         * objects anyway in the current ISOMetadata implementation.
         */
        return new SetOfUnknownSize<Entry<Citation,String>>() {
            /** Delegates to the enclosing class. */
            @Override public void clear() throws UnsupportedOperationException {
                IdentifierMapAdapter.this.clear();
            }

            /** Delegates to the enclosing class. */
            @Override public boolean isEmpty() {
                return IdentifierMapAdapter.this.isEmpty();
            }

            /** Delegates to the enclosing class. */
            @Override public int size() {
                return IdentifierMapAdapter.this.size();
            }

            /** Returns an iterator over the (<var>citation</var>, <var>code</var>) entries. */
            @Override public Iterator<Entry<Citation, String>> iterator() {
                return new Iter(identifiers, isModifiable());
            }
        };
    }

    /**
     * The iterator over the (<var>citation</var>, <var>code</var>) entries. This iterator is created by the
     * {@link #entrySet()} collection. It extends {@link HashMap} as an opportunist implementation strategy,
     * but users do not need to know this detail.
     *
     * <p>This iterator supports the {@link #remove()} operation if the underlying collection supports it.</p>
     *
     * <p>The map entries are used as a safety against duplicated authority values. The map values
     * are non-null only after we iterated over an authority. Then the value is {@link Boolean#TRUE}
     * if the identifier has been removed, of {@code Boolean#FALSE} otherwise.</p>
     *
     * @author  Martin Desruisseaux (Geomatys)
     */
    @SuppressWarnings("serial")                 // Not intended to be serialized.
    private static final class Iter extends HashMap<Citation,Boolean> implements Iterator<Entry<Citation,String>> {
        /**
         * An iterator over the {@link IdentifierMapAdapter#identifiers} collection,
         * or (@code null} if we have reached the iteration end.
         */
        private Iterator<? extends Identifier> identifiers;

        /**
         * The next entry to be returned by {@link #next()}, or {@code null} if not yet computed.
         * This field will be computed only when {@link #next()} or {@link #hasNext()} is invoked.
         */
        private transient Entry<Citation,String> next;

        /**
         * The current authority. Used only for removal operations.
         */
        private transient Citation authority;

        /**
         * {@code true} if the iterator should support the {@link #remove()} operation.
         */
        private final boolean isModifiable;

        /**
         * Creates a new iterator for the given collection of identifiers.
         */
        Iter(final Collection<? extends Identifier> identifiers, final boolean isModifiable) {
            super(Containers.hashMapCapacity(identifiers.size()));
            this.identifiers = identifiers.iterator();
            this.isModifiable = isModifiable;
        }

        /**
         * Advances to the next non-null identifier, skips duplicated authorities, wraps the
         * identifier in an entry if needed and stores the result in the {@link #next} field.
         * If we reach the iteration end, then this method set the {@link #identifiers}
         * iterator to {@code null}.
         */
        private void toNext() {
            final Iterator<? extends Identifier> it = identifiers;
            if (it != null) {
                while (it.hasNext()) {
                    final Identifier identifier = it.next();
                    if (identifier != null) {
                        @SuppressWarnings("LocalVariableHidesMemberVariable")
                        final Citation authority = identifier.getAuthority();
                        final Boolean state = put(authority, Boolean.FALSE);
                        if (state == null) {
                            if (identifier instanceof IdentifierMapEntry) {
                                next = (IdentifierMapEntry) identifier;
                            } else {
                                next = new IdentifierMapEntry.Immutable(authority, identifier.getCode());
                            }
                            this.authority = authority;
                            return;
                        }
                        if (state) {
                            // Found a duplicated entry, and user asked for the
                            // removal of that authority.
                            it.remove();
                        }
                    }
                }
                identifiers = null;
            }
        }

        /**
         * If we need to search for the next element, fetches it now.
         * Then returns {@code true} if we didn't reached the iteration end.
         */
        @Override
        public boolean hasNext() {
            if (next == null) {
                toNext();
            }
            return identifiers != null;
        }

        /**
         * If we need to search for the next element, searches it now. Then set {@link #next}
         * to {@code null} as a flag meaning that next invocations will need to search again
         * for an element, and returns the element that we got.
         */
        @Override
        public Entry<Citation,String> next() throws NoSuchElementException {
            Entry<Citation,String> entry = next;
            if (entry == null) {
                toNext();
                entry = next;
            }
            next = null;
            if (identifiers == null) {
                throw new NoSuchElementException();
            }
            return entry;
        }

        /**
         * Removes the last element returned by {@link #next()}. Note that if the {@link #next}
         * field is non-null, that would mean that the iteration has moved since the last call
         * to the {@link #next()} method, in which case the iterator is invalid.
         */
        @Override
        public void remove() throws IllegalStateException {
            if (!isModifiable) {
                throw new UnsupportedOperationException();
            }
            final Iterator<? extends Identifier> it = identifiers;
            if (it == null || next != null) {
                throw new IllegalStateException();
            }
            it.remove();
            put(authority, Boolean.TRUE);
        }

        /**
         * Iterators are not intended to be cloned.
         */
        @Override
        @SuppressWarnings("CloneDoesntCallSuperClone")
        public Object clone() {
            throw new UnsupportedOperationException();
        }

        /**
         * Returns the next value to be returned, for debugging purpose only.
         */
        @Override
        public String toString() {
            return Strings.toString(Iter.class, "next", next);
        }
    }

    /**
     * Overrides the string representation in order to use only the authority title as keys.
     * We do that because the string representations of {@code DefaultCitation} objects are
     * very big.
     *
     * <p>String examples:</p>
     * <ul>
     *   <li>{gml:id=“myID”}</li>
     *   <li>{gco:uuid=“42924124-032a-4dfe-b06e-113e3cb81cf0”}</li>
     *   <li>{xlink:href=“http://www.mydomain.org/myHREF”}</li>
     * </ul>
     *
     * @see SpecializedIdentifier#toString()
     */
    @Override
    public String toString() {
    final StringBuilder buffer = new StringBuilder(50).append('{');
    for (final Entry<Citation,String> entry : entrySet()) {
        if (buffer.length() != 1) {
                buffer.append(", ");
            }
            SpecializedIdentifier.format(buffer, entry.getKey(), entry.getValue());
        }
        return buffer.append('}').toString();
    }
}
