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

// J2SE dependencies
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Set;

// JTS dependencies
import com.vividsolutions.jts.geom.CoordinateSequenceFactory;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.PrecisionModel;

// Geotools dependencies
import org.geotools.factory.Hints;
import org.geotools.factory.FactoryCreator;
import org.geotools.factory.FactoryRegistry;
import org.geotools.factory.FactoryRegistryException;
import org.geotools.resources.LazySet;


/**
 * Defines static methods used to access {@linkplain GeometryFactory geometry},
 * {@linkplain CoordinateSequenceFactory coordinate sequence} or
 * {@linkplain PrecisionModel precision model} factories.
 *
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/main/src/main/java/org/geotools/geometry/jts/FactoryFinder.java $
 * @version $Id: FactoryFinder.java 20703 2006-07-24 16:57:44Z jgarnett $
 * @author Martin Desruisseaux
 */
public class FactoryFinder {
    /**
     * The service registry for this manager.
     * Will be initialized only when first needed.
     */
    private static FactoryRegistry registry;

    /**
     * Do not allows any instantiation of this class.
     */
    private FactoryFinder() {
        // singleton
    }

    /**
     * Returns the service registry. The registry will be created the first
     * time this method is invoked.
     */
    private static FactoryRegistry getServiceRegistry() {
        assert Thread.holdsLock(FactoryFinder.class);
        if (registry == null) {
            registry = new Registry();
        }
        return registry;
    }

    /**
     * Returns the first implementation of {@link GeometryFactory} matching the specified hints.
     * If no implementation matches, a new one is created if possible or an exception is thrown
     * otherwise.
     * <p>
     * Hints that may be understood includes
     * {@link Hints#JTS_COORDINATE_SEQUENCE_FACTORY JTS_COORDINATE_SEQUENCE_FACTORY},
     * {@link Hints#JTS_PRECISION_MODEL             JTS_PRECISION_MODEL} and
     * {@link Hints#JTS_SRID                        JTS_SRID}.
     *
     * @param  hints An optional map of hints, or {@code null} if none.
     * @return The first geometry factory that matches the supplied hints.
     * @throws FactoryRegistryException if no implementation was found or can be created for the
     *         {@link GeometryFactory} category and the given hints.
     */
    public static synchronized GeometryFactory getGeometryFactory(final Hints hints) throws FactoryRegistryException {
        return (GeometryFactory) getServiceRegistry().getServiceProvider(
                GeometryFactory.class, null, hints, Hints.JTS_GEOMETRY_FACTORY);
    }

    /**
     * Returns a set of all available implementations for the {@link GeometryFactory} category.
     *
     * @return Set of available geometry factory implementations.
     */
    public static synchronized Set getGeometryFactories() {
        return new LazySet(getServiceRegistry().getServiceProviders(GeometryFactory.class));
    }

    /**
     * Returns the first implementation of {@link PrecisionModel} matching the specified hints.
     * If no implementation matches, a new one is created if possible or an exception is thrown
     * otherwise.
     *
     * @param  hints An optional map of hints, or {@code null} if none.
     * @return The first precision model that matches the supplied hints.
     * @throws FactoryRegistryException if no implementation was found or can be created for the
     *         {@link PrecisionModel} category and the given hints.
     */
    public static synchronized PrecisionModel getPrecisionModel(final Hints hints) throws FactoryRegistryException {
        return (PrecisionModel) getServiceRegistry().getServiceProvider(
                PrecisionModel.class, null, hints, Hints.JTS_PRECISION_MODEL);
    }

    /**
     * Returns a set of all available implementations for the {@link PrecisionModel} category.
     *
     * @return Set of available precision model implementations.
     */
    public static synchronized Set getPrecisionModels() {
        return new LazySet(getServiceRegistry().getServiceProviders(PrecisionModel.class));
    }

    /**
     * Returns the first implementation of {@link CoordinateSequenceFactory} matching the specified
     * hints. If no implementation matches, a new one is created if possible or an exception is
     * thrown otherwise.
     *
     * @param  hints An optional map of hints, or {@code null} if none.
     * @return The first coordinate sequence factory that matches the supplied hints.
     * @throws FactoryRegistryException if no implementation was found or can be created for the
     *         {@link CoordinateSequenceFactory} interface and the given hints.
     */
    public static synchronized CoordinateSequenceFactory getCoordinateSequenceFactory(final Hints hints) throws FactoryRegistryException {
        return (CoordinateSequenceFactory) getServiceRegistry().getServiceProvider(
                CoordinateSequenceFactory.class, null, hints, Hints.JTS_COORDINATE_SEQUENCE_FACTORY);
    }

    /**
     * Returns a set of all available implementations for the {@link CoordinateSequenceFactory}
     * interface.
     *
     * @return Set of available coordinate sequence factory implementations.
     */
    public static synchronized Set getCoordinateSequenceFactories() {
        return new LazySet(getServiceRegistry().getServiceProviders(CoordinateSequenceFactory.class));
    }

    /**
     * Scans for factory plug-ins on the application class path. This method is
     * needed because the application class path can theoretically change, or
     * additional plug-ins may become available. Rather than re-scanning the
     * classpath on every invocation of the API, the class path is scanned
     * automatically only on the first invocation. Clients can call this
     * method to prompt a re-scan. Thus this method need only be invoked by
     * sophisticated applications which dynamically make new plug-ins
     * available at runtime.
     */
    public static void scanForPlugins() {
        if (registry != null) {
            registry.scanForPlugins();
        }
    }

    /**
     * A custom registry for JTS factories. There is usually no need for custom implementation of
     * {@link ServiceRegistry} for geotools object. However, JTS factories are an other story
     * because they don't know anything about the Geotools's factory plugin system. Consequently
     * we need to process JTS factories in a special way.
     */
    private static final class Registry extends FactoryCreator {
        /**
         * Creates a registry for JTS factories.
         */
        public Registry() {
            super(Arrays.asList(new Class[] {
                    GeometryFactory.class,
                    PrecisionModel.class,
                    CoordinateSequenceFactory.class}));
        }

        /**
         * Creates a new instance of the specified factory using the specified hints.
         *
         * @param  category The category to instantiate.
         * @param  implementation The factory class to instantiate.
         * @param  hints The implementation hints.
         * @return The factory.
         * @throws FactoryRegistryException if the factory creation failed.
         */
        protected Object createServiceProvider(final Class category,
                                               final Class implementation,
                                               final Hints hints)
                throws FactoryRegistryException
        {
            if (GeometryFactory.class.isAssignableFrom(category) &&
                GeometryFactory.class.equals(implementation))
            {
                return new GeometryFactory(getPrecisionModel(hints), getSRID(hints),
                                           getCoordinateSequenceFactory(hints));
            }
            return super.createServiceProvider(category, implementation, hints);
        }

        /**
         * Extracts the SRID from the hints, or returns {@code 0} if none.
         */
        private static int getSRID(final Hints hints) {
            if (hints != null) {
                final Integer SRID = (Integer) hints.get(Hints.JTS_SRID);
                if (SRID != null) {
                    return SRID.intValue();
                }
            }
            return 0;
        }

        /**
         * Returns {@code true} if the specified {@code provider} meets the requirements specified
         * by a map of {@code hints}. This method is invoked automatically when the {@code provider}
         * is known to meets standard Geotools requirements.
         * <p>
         * This implementation add JTS-specific checks. More specifically, we checks if
         * {@link GeometryFactory} uses the required {@link CoordinateSequenceFactory}
         * and {@link PrecisionModel}.
         *
         * @param provider The provider to checks.
         * @param category The factory's category.
         * @param hints    The user requirements.
         * @return {@code true} if the {@code provider} meets the user requirements.
         */
        protected boolean isAcceptable(final Object provider, final Class category, final Hints hints) {
            if (GeometryFactory.class.isAssignableFrom(category)) {
                final GeometryFactory           factory   = (GeometryFactory) provider;
                final CoordinateSequenceFactory sequence  = factory.getCoordinateSequenceFactory();
                final PrecisionModel            precision = factory.getPrecisionModel();
                if (!isAcceptable(sequence,  hints.get(Hints.JTS_COORDINATE_SEQUENCE_FACTORY)) ||
                    !isAcceptable(precision, hints.get(Hints.JTS_PRECISION_MODEL)))
                {
                    return false;
                }
                final int SRID = getSRID(hints);
                if (SRID!=0 && SRID!=factory.getSRID()) {
                    return false;
                }
            }
            return super.isAcceptable(provider, category, hints);
        }

        /**
         * Checks if an actual {@link GeometryFactory} property matches the given hint.
         *
         * @param actual The geometry factory property, either a {@link CoordinateSequenceFactory}
         *               or a {@link PrecisionModel} concrete implementation.
         * @param requested The user's hint, either a concrete implementation of the same class
         *                  than {@code actual}, a {@link Class} or an array of them.
         * @return {@code true} if the {@code actual} value matches the {@code requested}
         *         value, or {@code false} otherwise.
         */
        private static boolean isAcceptable(final Object actual,
                                            final Object requested)
        {
            /*
             * If the user didn't provided any hint, or if the factory
             * already uses the requested object, accepts.
             */
            if (requested==null || requested.equals(actual)) {
                return true;
            }
            /*
             * If hint is an array (either Class object or concrete
             * implementations), iterates over all array's elements.
             */
            if (requested.getClass().isArray()) {
                final int length = Array.getLength(requested);
                for (int i=0; i<length; i++) {
                    if (!isAcceptable(actual, Array.get(requested, i))) {
                        return false;
                    }
                }
                return true;
            }
            /*
             * If hint is only a class instead of an actual implementation,
             * accepts instances of this class or any subclasses.
             */
            if (actual!=null && requested instanceof Class) {
                return ((Class) requested).isAssignableFrom(actual.getClass());
            }
            return false;
        }
    }
}
