/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2005-2006, GeoTools Project Managment Committee (PMC)
 *    (C) 2005, 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;
 *    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.referencing.factory;

// J2SE dependencies
import java.io.Serializable;
import java.io.ObjectStreamException;
import java.sql.SQLException;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.LinkedHashSet;
import java.util.logging.Level;
import java.util.logging.LogRecord;

// OpenGIS dependencies
import org.opengis.metadata.Identifier;
import org.opengis.referencing.AuthorityFactory;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.NoSuchIdentifierException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.operation.CoordinateOperationAuthorityFactory; // For javadoc

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


/**
 * A lazy set of {@linkplain IdentifiedObject identified objects}. This set creates
 * {@link IdentifiedObject}s from authority codes only when first needed. This class
 * is typically used as the set returned by implementations of the
 * {@link CoordinateOperationAuthorityFactory#createFromCoordinateReferenceSystemCodes
 * createFromCoordinateReferenceSystemCodes} method. Deferred creation in this case may
 * have great performance impact since a set may contains about 40 entries (e.g.
 * transformations from "ED50" (EPSG:4230) to "WGS 84" (EPSG:4326)) while some users
 * only want to look for the first entry (e.g. the default
 * {@link org.geotools.referencing.operation.AuthorityBackedFactory} implementation).
 * <p>
 * <h3>Exception handling</h3>
 * If the underlying factory failed to creates an object because of an unsupported
 * operation method ({@link NoSuchIdentifierException}), the exception is logged with
 * the {@link Level#FINE FINE} level (because this is a recoverable failure) and
 * the iteration continue. If the operation creation failed for any other kind of
 * reason ({@link FactoryException}), then the exception is rethrown as an unchecked
 * {@link BackingStoreException}. This default behavior can be changed if a subclass
 * overrides the {@link #isRecoverableFailure isRecoverableFailure} method.
 * <p>
 * <h3>Serialization</h3>
 * Serialization of this class forces the immediate creation of all
 * {@linkplain IdentifiedObject identified objects} not yet created.
 * The serialized set is disconnected from the {@linkplain #factory underlying factory}.
 * <p>
 * <h3>Thread safety</h3>
 * This class is not thread-safe.
 *
 * @since 2.2
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/factory/IdentifiedObjectSet.java $
 * @version $Id: IdentifiedObjectSet.java 23635 2007-01-01 20:58:15Z desruisseaux $
 * @author Martin Desruisseaux
 */
public class IdentifiedObjectSet extends AbstractSet implements Serializable {
    /**
     * For cross-version compatibility during serialisation.
     */
    private static final long serialVersionUID = -4221260663706882719L;

    /**
     * The map of object codes (keys), and the actual identified objects (values)
     * when it has been created. Each entry has a null value until the corresponding
     * object is created.
     */
    private final Map/*<String,IdentifiedObject>*/ objects = new LinkedHashMap();

    /**
     * The factory to use for creating {@linkplain IdentifiedObject identified objects}
     * when first needed.
     */
    protected final AuthorityFactory factory;

    /**
     * Creates an initially empty set. The {@linkplain IdentifiedObject}s
     * will be created when first needed using the specified factory.
     *
     * @param factory The factory to use for deferred {@link IdentifiedObject}s creations.
     */
    public IdentifiedObjectSet(final AuthorityFactory factory) {
        this.factory = factory;
    }

    /**
     * Removes all of the elements from this collection.
     */
    public void clear() {
        objects.clear();
	}

    /**
     * Returns the number of objects available in this set. Note that this
     * number may decrease during the iteration process if the creation of
     * some {@linkplain IdentifiedObject identified objects} failed.
     */
    public int size() {
        return objects.size();
    }

    /**
     * Ensures that this collection contains an object for the specified authority code.
     * The {@linkplain IdentifiedObject identified object} will be created from the specified
     * code only when first needed. This method returns {@code true} if this set changed as a
     * result of this call.
     */
    public boolean addAuthorityCode(final String code) {
        final boolean already = objects.containsKey(code);
        final IdentifiedObject old = (IdentifiedObject) objects.put(code, null);
        if (old != null) {
            // A fully created object was already there. Keep it.
            objects.put(code, old);
            return false;
        }
        return !already;
    }

    /**
     * Ensures that this collection contains the specified object. This set do not allows multiple
     * objects for the same {@linkplain #getAuthorityCode authority code}. If this set already
     * contains an object using the same {@linkplain #getAuthorityCode authority code} than the
     * specified one, then the old object is replaced by the new one even if the objects are not
     * otherwise identical.
     */
    public boolean add(final Object object) {
        final String code = getAuthorityCode((IdentifiedObject) object);
        return !Utilities.equals(objects.put(code, object), object);
    }

    /**
     * Returns the identified object for the specified value, {@linkplain #createObject creating}
     * it if needed.
     *
     * @throws BackingStoreException if the object creation failed.
     */
    private IdentifiedObject get(final String code) throws BackingStoreException {
        IdentifiedObject object = (IdentifiedObject) objects.get(code);
        if (object==null && objects.containsKey(code)) {
            try {
                object = createObject(code);
                objects.put(code, object);
            } catch (FactoryException exception) {
                if (!isRecoverableFailure(exception)) {
                    throw new BackingStoreException(exception);
                }
                log(exception, code);
                objects.remove(code);
            }
        }
        return object;
    }

    /**
     * Returns {@code true} if this collection contains the specified object.
     */
    public boolean contains(final Object object) {
        final String code = getAuthorityCode((IdentifiedObject) object);
        final IdentifiedObject current = get(code);
        return object.equals(current);
    }

    /**
     * Removes a single instance of the specified element from this collection,
     * if it is present.
     */
    public boolean remove(final Object object) {
        final String code = getAuthorityCode((IdentifiedObject) object);
        final IdentifiedObject current = get(code);
        if (object.equals(current)) {
            objects.remove(code);
            return true;
        }
        return false;
    }

    /**
     * Removes from this collection all of its elements that are contained in
     * the specified collection.
     */
    public boolean removeAll(final Collection collection) {
        boolean modified = false;
        for (final Iterator it=collection.iterator(); it.hasNext();) {
            if (remove(it.next())) {
                modified = true;
            }
        }
        return modified;
    }

    /**
     * Returns an iterator over the objects in this set. If the iteration encounter any
     * kind of {@link FactoryException} other than {@link NoSuchIdentifierException}, then
     * the exception will be rethrown as an unchecked {@link BackingStoreException}.
     */
    public Iterator iterator() {
        return new Iter(objects.entrySet().iterator());
    }

    /**
     * Ensures that the <var>n</var> first objects in this set are created. This method is
     * typically invoked after some calls to {@link #addAuthorityCode} in order to make sure
     * that the {@linkplain #factory underlying factory} is really capable to create at least
     * one object. {@link FactoryException} (except the ones accepted as
     * {@linkplain #isRecoverableFailure recoverable failures}) are thrown as if they were never
     * wrapped into {@link BackingStoreException}.
     *
     * @param n The number of object to resolve. If this number is equals or greater than the
     *          {@linkplain #size set's size}, then the creation of all objects is garantee
     *          successful.
     * @throws FactoryException if an {@linkplain #createObject object creation} failed.
     */
    public void resolve(int n) throws FactoryException {
        if (n > 0) try {
            for (final Iterator it=iterator(); it.hasNext();) {
                it.next();
                if (--n == 0) {
                    break;
                }
            }
        } catch (BackingStoreException exception) {
            final Throwable cause = exception.getCause();
            if (cause instanceof FactoryException) {
                throw (FactoryException) cause;
            }
            throw exception;
        }
    }

    /**
     * Returns the {@linkplain #getAuthorityCode authority code} of all objects in this set.
     * The returned array contains the codes in iteration order. This method do not trig the
     * {@linkplain #createObject creation} of any new object.
     * <p>
     * This method is typically used together with {@link #setAuthorityCodes} for altering the
     * iteration order on the basis of authority codes.
     */
    public String[] getAuthorityCodes() {
        final Set codes = objects.keySet();
        return (String[]) codes.toArray(new String[codes.size()]);
    }

    /**
     * Set the content of this set as an array of authority codes. For any code in the given list,
     * this method will preserve the corresponding {@linkplain IdentifiedObject identified object}
     * if it was already created. Other objects will be {@linkplain #createObject created} only
     * when first needed, as usual in this {@code IdentifiedObjectSet} implementation.
     * <p>
     * This method is typically used together with {@link #getAuthorityCodes} for altering the
     * iteration order on the basis of authority codes. If the specified {@code codes} array
     * contains the same elements than {@link #getAuthorityCodes} in a different order, then
     * this method just set the new ordering.
     *
     * @see #addAuthorityCode
     */
    public void setAuthorityCodes(final String[] codes) {
        final Map copy = new HashMap(objects);
        objects.clear();
        for (int i=0; i<codes.length; i++) {
            final String code = codes[i];
            objects.put(code, (IdentifiedObject) copy.get(code));
        }
    }

    /**
     * Returns the code to uses as a key for the specified object. The default implementation
     * returns the code of the first {@linkplain IdentifiedObject#getIdentifiers identifier},
     * if any, or the code of the{@linkplain IdentifiedObject#getName primary name} otherwise.
     * Subclasses may overrides this method if they want to use a different key for this set.
     */
    protected String getAuthorityCode(final IdentifiedObject object) {
        final Identifier id;
        final Set identifiers = object.getIdentifiers();
        if (identifiers!=null && !identifiers.isEmpty()) {
            id = (Identifier) identifiers.iterator().next();
        } else {
            id = object.getName();
        }
        return id.getCode();
    }

    /**
     * Creates an object for the specified authority code. This method is invoked during the
     * iteration process if an object was not already created. The default implementation invokes
     * <code>{@linkplain #factory}.{@link AuthorityFactory#createObject createObject}(code)</code>.
     * Subclasses may override this method if they want to invoke a more specific method.
     */
    protected IdentifiedObject createObject(final String code) throws FactoryException {
        return factory.createObject(code);
    }

    /**
     * Returns {@code true} if the specified exception should be handled as a recoverable failure.
     * This method is invoked during the iteration process if the factory failed to create some
     * object. If this method returns {@code true} for the given exception, then the exception
     * will be logged in the {@linkplain AbstractAuthorityFactory#LOGGER Geotools factory logger}
     * with the {@link Level#FINE FINE} level. If this method returns {@code false}, then the
     * exception will be retrown as a {@link BackingStoreException}. The default implementation
     * returns {@code true} only for {@link NoSuchIdentifierException} (not to be confused with
     * {@link NoSuchAuthorityCodeException}).
     */
    protected boolean isRecoverableFailure(final FactoryException exception) {
        return (exception instanceof NoSuchIdentifierException);
    }

    /**
     * Log an message for the specified exception.
     *
     * @todo Localize.
     */
    static void log(final FactoryException exception, final String code) {
        final LogRecord record = new LogRecord(Level.FINE,
                "Failed to create an object for code \"" + code + "\".");
        record.setSourceClassName(IdentifiedObjectSet.class.getName());
        record.setSourceMethodName("createObject");
        record.setThrown(exception);
        AbstractAuthorityFactory.LOGGER.log(record);
    }

    /**
     * Returns a serializable copy of this set. This method is invoked automatically during
     * serialization. The serialised set of identified objects is disconnected from the
     * {@linkplain #factory underlying factory}.
     */
    protected Object writeReplace() throws ObjectStreamException {
        return new LinkedHashSet(this);
    }

    /**
     * The iterator over the entries in the enclosing set. This iterator will creates the
     * {@linkplain IdentifiedObject identified objects} when first needed.
     *
     * @version $Id: IdentifiedObjectSet.java 23635 2007-01-01 20:58:15Z desruisseaux $
     * @author Martin Desruisseaux
     */
    private final class Iter implements Iterator {
        /**
         * The iterator over the entries from the underlying map.
         */
        private final Iterator iterator;

        /**
         * The next object to returns, or {@code null} if the iteration is over.
         */
        private IdentifiedObject element;

        /**
         * Creates a new instance of this iterator.
         */
        public Iter(final Iterator iterator) {
            this.iterator = iterator;
            toNext();
        }

        /**
         * Moves to the next element.
         *
         * @throws BackingStoreException if the underlying factory failed to creates the
         *         coordinate operation.
         */
        private void toNext() throws BackingStoreException {
            while (iterator.hasNext()) {
                final Map.Entry entry = (Map.Entry) iterator.next();
                element = (IdentifiedObject) entry.getValue();
                if (element == null) {
                    final String code = (String) entry.getKey();
                    try {
                        element = createObject(code);
                    } catch (FactoryException exception) {
                        if (!isRecoverableFailure(exception)) {
                            throw new BackingStoreException(exception);
                        }
                        log(exception, code);
                        iterator.remove();
                        continue;
                    }
                    entry.setValue(element);
                }
                return; // Element found.
            }
            element = null; // No more element found.
        }

        /**
         * Returns {@code true} if there is more elements.
         */
        public boolean hasNext() {
            return element != null;
        }

        /**
         * Returns the next element.
         *
         * @throws NoSuchElementException if there is no more operations in the set.
         */
        public Object next() throws NoSuchElementException {
            final IdentifiedObject next = element;
            if (next == null) {
                throw new NoSuchElementException();
            }
            toNext();
            return next;
        }

        /**
         * Removes the last element from the underlying set.
         */
        public void remove() {
            iterator.remove();
        }
    }
}
