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

// J2SE dependencies
import java.util.Set;
import java.util.AbstractSet;
import java.util.AbstractMap;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import java.io.Serializable;
import java.io.ObjectStreamException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

// OpenGIS dependencies
import org.opengis.referencing.operation.Projection;

// Geotools dependencies
import org.geotools.resources.i18n.Logging;
import org.geotools.resources.i18n.LoggingKeys;


/**
 * A set of EPSG authority codes. This set requires a living connection to the EPSG database.
 * All {@link #iterator} method call creates a new {@link ResultSet} holding the codes. However,
 * call to {@link #contains} map directly to a SQL call.
 * <p>
 * Serialization of this class store a copy of all authority codes. The serialization
 * do not preserve any connection to the database.
 *
 * @since 2.2
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/factory/epsg/AuthorityCodes.java $
 * @version $Id: AuthorityCodes.java 27862 2007-11-12 19:51:19Z desruisseaux $
 * @author Martin Desruisseaux
 */
final class AuthorityCodes extends AbstractSet implements Serializable {
    /**
     * For compatibility with different versions.
     */
    private static final long serialVersionUID = 7105664579449680562L;

    /**
     * The logger name.
     */
    private static final String LOGGER = "org.geotools.referencing.factory.epsg";

    /**
     * The factory which is the owner of this set. One purpose of this field (even if it were not
     * used directly by this class) is to avoid garbage collection of the factory as long as this
     * set is in use. This is required because {@link DirectEpsgFactory#finalize} closes the JDBC
     * connections.
     */
    private final DirectEpsgFactory factory;

    /**
     * The type for this code set. This is translated to the most appropriate
     * interface type even if the user supplied an implementation type.
     */
    public final Class type;

    /**
     * {@code true} if {@link #type} is assignable to {@link Projection}.
     */
    private final boolean isProjection;

    /**
     * A view of this set as a map with object's name as values, or {@code null} if none.
     * Will be created only when first needed.
     */
    private transient java.util.Map asMap;

    /**
     * The SQL command to use for creating the {@code queryAll} statement.
     * Used for iteration over all codes.
     */
    final String sqlAll;

    /**
     * The SQL command to use for creating the {@code querySingle} statement.
     * Used for fetching the description from a code.
     */
    private final String sqlSingle;

    /**
     * The statement to use for querying all codes.
     * Will be created only when first needed.
     */
    private transient PreparedStatement queryAll;

    /**
     * The statement to use for querying a single code.
     * Will be created only when first needed.
     */
    private transient PreparedStatement querySingle;

    /**
     * The connection to the underlying database. This set should never close
     * this connection. Closing it is {@link DirectEpsgFactory}'s job.
     */
    private final Connection connection;

    /**
     * The collection's size, or a negative value if not yet computed. The records will be counted
     * only when first needed. The special value -2 if set by {@link #isEmpty} if the size has not
     * yet been computed, but we know that the set is not empty.
     */
    private int size = -1;

    /**
     * Creates a new set of authority codes for the specified type.
     *
     * @param  connection The connection to the EPSG database.
     * @param  table      The table to query.
     * @param  type       The type to query.
     * @param  factory    The factory originator.
     */
    public AuthorityCodes(final Connection        connection,
                          final TableInfo         table,
                          final Class             type,
                          final DirectEpsgFactory factory)
    {
        this.factory    = factory;
        this.connection = connection;
        final StringBuffer buffer = new StringBuffer("SELECT ");
        buffer.append(table.codeColumn);
        if (table.nameColumn != null) {
            buffer.append(", ").append(table.nameColumn);
        }
        buffer.append(" FROM ").append(table.table);
        boolean hasWhere = false;
        Class tableType = table.type;
        if (table.typeColumn != null) {
            for (int i=0; i<table.subTypes.length; i++) {
                final Class candidate = table.subTypes[i];
                if (candidate.isAssignableFrom(type)) {
                    buffer.append(" WHERE (").append(table.typeColumn)
                          .append(" LIKE '").append(table.typeNames[i]).append("%'");
                    hasWhere = true;
                    tableType = candidate;
                    break;
                }
            }
            if (hasWhere) {
                buffer.append(')');
            }
        }
        this.type = tableType;
        isProjection = Projection.class.isAssignableFrom(tableType);
        final int length = buffer.length();
        buffer.append(" ORDER BY ").append(table.codeColumn);
        sqlAll = factory.adaptSQL(buffer.toString());
        buffer.setLength(length);
        buffer.append(hasWhere ? " AND " : " WHERE ").append(table.codeColumn).append(" = ?");
        sqlSingle = factory.adaptSQL(buffer.toString());
    }

    /**
     * Returns all codes.
     */
    private ResultSet getAll() throws SQLException {
        assert Thread.holdsLock(this);
        if (queryAll != null) {
            try {
                return queryAll.executeQuery();
            } catch (SQLException ignore) {
                /*
                 * Failed to reuse an existing statement. This problem occurs in some occasions
                 * with the JDBC-ODBC bridge in Java 6 (the error message is "Invalid handle").
                 * I'm not sure where the bug come from (didn't noticed it when using HSQL). We
                 * will try again with a new statement created in the code after this 'catch'
                 * clause. Note that we set 'queryAll' to null first in case of failure during
                 * the 'prepareStatement(...)' execution.
                 */
                queryAll.close();
                queryAll = null;
                recoverableException("getAll", ignore);
            }
        }
        queryAll = connection.prepareStatement(sqlAll);
        return queryAll.executeQuery();
    }

    /**
     * Returns a single code.
     */
    private ResultSet getSingle(final Object code) throws SQLException {
        assert Thread.holdsLock(this);
        if (querySingle == null) {
            querySingle = connection.prepareStatement(sqlSingle);
        }
        querySingle.setString(1, code.toString());
        return querySingle.executeQuery();
    }

    /**
     * Returns {@code true} if the code in the specified result set is acceptable.
     * This method handle projections in a special way.
     */
    private boolean isAcceptable(final ResultSet results) throws SQLException {
        if (!isProjection) {
            return true;
        }
        final String code = results.getString(1);
        synchronized (factory) {
            return factory.isProjection(code);
        }
    }

    /**
     * Returns {@code true} if the code in the specified code is acceptable.
     * This method handle projections in a special way.
     */
    private boolean isAcceptable(final String code) throws SQLException {
        if (!isProjection) {
            return true;
        }
        synchronized (factory) {
            return factory.isProjection(code);
        }
    }

    /**
     * Returns {@code true} if this collection contains no elements.
     * This method fetch at most one row instead of counting all rows.
     */
    public synchronized boolean isEmpty() {
        if (size != -1) {
            return size == 0;
        }
        boolean empty = true;
        try {
            final ResultSet results = getAll();
            while (results.next()) {
                if (isAcceptable(results)) {
                    empty = false;
                    break;
                }
            }
            results.close();
        } catch (SQLException exception) {
            unexpectedException("isEmpty", exception);
        }
        size = empty ? 0 : -2;
        return empty;
    }

    /**
     * Count the number of elements in the underlying result set.
     */
    public synchronized int size() {
        if (size >= 0) {
            return size;
        }
        int count = 0;
        try {
            final ResultSet results = getAll();
            while (results.next()) {
                if (isAcceptable(results)) {
                    count++;
                }
            }
            results.close();
        } catch (SQLException exception) {
            unexpectedException("size", exception);
        }
        size = count; // Stores only on success.
        return count;
    }

    /**
     * Returns {@code true} if this collection contains the specified element.
     */
    public synchronized boolean contains(final Object code) {
        boolean exists = false;
        if (code != null) try {
            final ResultSet results = getSingle(code);
            while (results.next()) {
                if (isAcceptable(results)) {
                    exists = true;
                    break;
                }
            }
            results.close();
        } catch (SQLException exception) {
            unexpectedException("contains", exception);
        }
        return exists;
    }

    /**
     * Returns an iterator over the codes. The iterator is backed by a living {@link ResultSet},
     * which will be closed as soon as the iterator reach the last element.
     */
    public synchronized java.util.Iterator iterator() {
        try {
            final Iterator iterator = new Iterator(getAll());
            /*
             * Set the statement to null without closing it, in order to force a new statement
             * creation if getAll() is invoked before the iterator finish its iteration.  This
             * is needed because only one ResultSet is allowed for each Statement.
             */
            queryAll = null;
            return iterator;
        } catch (SQLException exception) {
            unexpectedException("iterator", exception);
            return Collections.EMPTY_SET.iterator();
        }
    }

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

    /**
     * Closes the underlying statements. Note: this method is also invoked directly
     * by {@link DirectEpsgFactory#dispose}, which is okay in this particular case since
     * the implementation of this method can be executed an arbitrary amount of times.
     */
    protected synchronized void finalize() throws SQLException {
        if (querySingle != null) {
            querySingle.close();
            querySingle = null;
        }
        if (queryAll != null) {
            queryAll.close();
            queryAll = null;
        }
    }

    /**
     * Invoked when an exception occured. This method just log a warning.
     */
    private static void unexpectedException(final String       method,
                                            final SQLException exception)
    {
        unexpectedException(AuthorityCodes.class, method, exception);
    }

    /**
     * Invoked when an exception occured. This method just log a warning.
     */
    static void unexpectedException(final Class        classe,
                                    final String       method,
                                    final SQLException exception)
    {
        org.geotools.util.logging.Logging.unexpectedException(LOGGER, classe, method, exception);
    }

    /**
     * Invoked when a recoverable exception occured.
     */
    private static void recoverableException(final String method, final SQLException exception) {
        // Uses the FINE level instead of WARNING because it may be a recoverable error.
        LogRecord record = Logging.format(Level.FINE, LoggingKeys.UNEXPECTED_EXCEPTION);
        record.setSourceClassName(AuthorityCodes.class.getName());
        record.setSourceMethodName(method);
        record.setThrown(exception);
        org.geotools.util.logging.Logging.getLogger(LOGGER).log(record);        
    }

    /**
     * The iterator over the codes. This inner class must kept a reference toward the enclosing
     * {@link AuthorityCodes} in order to prevent a call to {@link AuthorityCodes#finalize}
     * before the iteration is finished.
     */
    private final class Iterator implements java.util.Iterator {
        /** The result set, or {@code null} if there is no more elements. */
        private ResultSet results;

        /** The next code. */
        private transient String next;

        /** Creates a new iterator for the specified result set. */
        Iterator(final ResultSet results) throws SQLException {
            assert Thread.holdsLock(AuthorityCodes.this);
            this.results = results;
            toNext();
        }

        /** Moves to the next element. */
        private void toNext() throws SQLException {
            while (results.next()) {
                next = results.getString(1);
                if (isAcceptable(next)) {
                    return;
                }
            }
            finalize();
        }

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

        /** Returns the next element. */
        public Object next() {
            if (results == null) {
                throw new NoSuchElementException();
            }
            final String current = next;
            try {
                toNext();
            } catch (SQLException exception) {
                results = null;
                unexpectedException(Iterator.class, "next", exception);
            }
            return current;
        }

        /** Always throws an exception, since this iterator is read-only. */
        public void remove() {
            throw new UnsupportedOperationException();
        }

        /** Closes the underlying result set. */
        protected void finalize() throws SQLException {
            next = null;
            if (results != null) {
                final PreparedStatement owner = (PreparedStatement) results.getStatement();
                results.close();
                results = null;
                synchronized (AuthorityCodes.this) {
                    /*
                     * We don't need the statement anymore. Gives it back to the enclosing class
                     * in order to avoid creating a new one when AuthorityCodes.getAll() will be
                     * invoked again,  or closes the statement if getAll() already created a new
                     * statement anyway.
                     */
                    assert owner != queryAll;
                    if (queryAll == null) {
                        queryAll = owner;
                    } else {
                        owner.close();
                    }
                }
            }
        }
    }

    /**
     * Returns a view of this set as a map with object's name as value, or {@code null} if none.
     */
    final java.util.Map asMap() {
        if (asMap == null) {
            asMap = new Map();
        }
        return asMap;
    }
    
    /**
     * A view of {@link AuthorityCodes} as a map, with authority codes as key and
     * object names as values.
     */
    private final class Map extends AbstractMap {
        /**
         * Returns the number of key-value mappings in this map.
         */
        public int size() {
            return AuthorityCodes.this.size();
        }

        /**
         * Returns {@code true} if this map contains no key-value mappings.
         */
        public boolean isEmpty() {
            return AuthorityCodes.this.isEmpty();
        }

        /**
         * Returns the description to which this map maps the specified EPSG code.
         */
        public Object get(final Object code) {
            String value = null;
            if (code != null) try {
                synchronized (AuthorityCodes.this) {
                    final ResultSet results = getSingle(code);
                    while (results.next()) {
                        if (isAcceptable(results)) {
                            value = results.getString(2);
                            break;
                        }
                    }
                    results.close();
                }
            } catch (SQLException exception) {
                unexpectedException("get", exception);
            }
            return value;
        }

        /**
         * Returns {@code true} if this map contains a mapping for the specified EPSG code.
         */
        public boolean containsKey(final Object key) {
            return contains(key);
        }

        /**
         * Returns a set view of the keys contained in this map.
         */
        public Set keySet() {
            return AuthorityCodes.this;
        }

        /**
         * Returns a set view of the mappings contained in this map.
         *
         * @todo Not yet implemented.
         */
        public Set entrySet() {
            throw new UnsupportedOperationException();
        }
    }
}
