/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    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.
 *
 *    This package contains documentation from OpenGIS specifications.
 *    OpenGIS consortium's work is fully acknowledged here.
 */
package org.geotools.referencing.factory;

import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.LogRecord;

import org.opengis.referencing.FactoryException;
import org.geotools.factory.Hints;
import org.geotools.factory.OptionalFactory;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Loggings;
import org.geotools.resources.i18n.LoggingKeys;


/**
 * A buffered authority factory which will defer the {@linkplain #createBackingStore creation
 * of a backing store} until when first needed. This approach allow to etablish a connection to
 * a database (for example) only when first needed. In addition, the backing store can be
 * automatically disposed after a timeout and recreated when needed again.
 *
 * @since 2.1
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/library/referencing/src/main/java/org/geotools/referencing/factory/DeferredAuthorityFactory.java $
 * @version $Id: DeferredAuthorityFactory.java 37339 2011-05-28 10:32:42Z aaime $
 * @author Martin Desruisseaux (IRD)
 *
 * @todo Extends {@link BufferedAuthorityFactory} for now in order to improve the trunk stability
 *       during GEOT-1286 development, but we may revisit that after GEOT-1286 completion.
 */
public abstract class DeferredAuthorityFactory extends BufferedAuthorityFactory
                                            implements OptionalFactory
{
    /**
     * The timer for {@linkplain AbstractAuthorityFactory#dispose disposing} backing stores.
     */
    private static Timer TIMER = new Timer("GT authority factory disposer", true);

    /**
     * The task for disposing the backing store, or {@code null} if none.
     * This task will be scheduled for repeated execution by {@link #setTimeout}.
     */
    private TimerTask disposer;

    /**
     * {@code true} if the backing store was used since the last time the timer task was run.
     * A value of {@code true} means that the task must wait again. A value of {@code false}
     * means that it can dispose the backing store.
     */
    private boolean used;

    /**
     * Constructs an instance without initial backing store. Subclasses are responsible for
     * creating an appropriate backing store when the {@link #createBackingStore} method is
     * invoked.
     *
     * @param userHints An optional set of hints, or {@code null} if none.
     * @param priority The priority for this factory, as a number between
     *        {@link #MINIMUM_PRIORITY MINIMUM_PRIORITY} and
     *        {@link #MAXIMUM_PRIORITY MAXIMUM_PRIORITY} inclusive.
     *
     * @see #createBackingStore
     *
     * @since 2.2
     */
    protected DeferredAuthorityFactory(final Hints userHints, final int priority) {
        super(priority, DEFAULT_MAX);
    }

    /**
     * Returns {@code true} if this factory is available. The default implementation returns
     * {@code false} if {@link #createBackingStore} throws an exception.
     */
    @Override
    public boolean isAvailable() {
        return super.isAvailable();
    }

    /**
     * Returns the backing store authority factory.
     *
     * @return The backing store to uses in {@code createXXX(...)} methods.
     * @throws FactoryException if the creation of backing store failed.
     */
    @Override
    final AbstractAuthorityFactory getBackingStore() throws FactoryException {
        if (backingStore == null) {
            synchronized (this) {
                if(backingStore == null) {
                    backingStore = createBackingStore();
                    if (backingStore == null) {
                        throw new FactoryNotFoundException(Errors.format(ErrorKeys.NO_DATA_SOURCE));
                    }
                    completeHints();
                }
            }
        }
        used = true; // Tell to the disposer to wait again.
        return backingStore;
    }

    /**
     * Creates the backing store authority factory. This method is invoked the first time a
     * {@code createXXX(...)} method is invoked.
     *
     * @return The backing store to uses in {@code createXXX(...)} methods.
     * @throws FactoryNotFoundException if the backing store has not been found.
     * @throws FactoryException if the creation of backing store failed for an other reason.
     */
    protected abstract AbstractAuthorityFactory createBackingStore() throws FactoryException;

    /**
     * Set a timer for disposing the backing store after the specified amount of milliseconds of
     * inactivity. The {@link #createBackingStore} method will be responsible for creating a new
     * backing store when needed. Note that the backing store disposal can be vetoed if
     * {@link #canDisposeBackingStore} returns {@code false}.
     *
     * @param delay The minimal delay before to close the backing store. This delay is very
     *        approximative. The backing store will not be closed before, but may take as
     *        much as twice that time before to be closed.
     */
    public synchronized void setTimeout(final long delay) {
        // if we have been disposed, don't do anything
        if(TIMER == null)
            return;
        
        if (disposer != null) {
            disposer.cancel();
        }
        disposer = new Disposer();
        TIMER.schedule(disposer, delay, delay);
    }

    /**
     * Returns {@code true} if the backing store can be disposed now. This method is invoked
     * automatically after the amount of time specified by {@link #setTimeout} if the factory
     * were not used during that time. The default implementation always returns {@code true}.
     * Subclasses should override this method and returns {@code false} if they want to prevent
     * the backing store disposal under some circonstances.
     *
     * @param backingStore The backing store in process of being disposed.
     */
    protected boolean canDisposeBackingStore(final AbstractAuthorityFactory backingStore) {
        return true;
    }

    /**
     * Releases resources immediately instead of waiting for the garbage collector. This
     * method disposes the backing store regardeless of {@link #canDisposeBackingStore} value.
     */
    @Override
    public synchronized void dispose() throws FactoryException {
        if (disposer != null) {
            disposer.cancel();
            disposer = null;
        }
        super.dispose();
    }
        
    /**
     * Disposes of the backing store
     * @throws FactoryException
     */
    protected synchronized void disposeBackingStore() {
        try {
            if(backingStore != null) {
                LOGGER.log(Level.INFO, "Disposing " + getClass() + " backing store");
                backingStore.dispose();
                backingStore = null;
            }
        } catch (FactoryException exception) {
            backingStore = null;
            final LogRecord record = Loggings.format(Level.WARNING,
                    LoggingKeys.CANT_DISPOSE_BACKING_STORE);
            record.setSourceMethodName("run");
            record.setSourceClassName(Disposer.class.getName());
            record.setThrown(exception);
            record.setLoggerName(LOGGER.getName());
            LOGGER.log(record);
        }
    }

    /**
     * The task for closing the backing store after the timeout.
     */
    private final class Disposer extends TimerTask {
        public void run() {
            synchronized (DeferredAuthorityFactory.this) {
                if (used || !canDisposeBackingStore(backingStore)) {
                    used = false;
                    return;
                }
                if (cancel()) {
                    disposer = null;
                    if (backingStore != null) { 
                        disposeBackingStore();
                    }
                    // Needed in order to lets GC do its job.
                    hints.remove(Hints.DATUM_AUTHORITY_FACTORY);
                    hints.remove(Hints.CS_AUTHORITY_FACTORY);
                    hints.remove(Hints.CRS_AUTHORITY_FACTORY);
                    hints.remove(Hints.COORDINATE_OPERATION_AUTHORITY_FACTORY);
                }
            }
        }
    }
}
