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

import java.util.Iterator;
import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.sql.SQLTransientException;
import org.opengis.util.FactoryException;
import org.opengis.referencing.AuthorityFactory;
import org.opengis.referencing.cs.CSAuthorityFactory;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.datum.DatumAuthorityFactory;
import org.opengis.referencing.operation.CoordinateOperationAuthorityFactory;
import org.apache.sis.referencing.util.LazySet;
import org.apache.sis.system.Reflect;
import org.apache.sis.system.Loggers;
import org.apache.sis.system.Modules;
import org.apache.sis.system.SystemListener;
import org.apache.sis.referencing.internal.EPSGFactoryProxy;
import org.apache.sis.referencing.factory.MultiAuthoritiesFactory;
import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
import org.apache.sis.referencing.factory.UnavailableFactoryException;
import org.apache.sis.referencing.factory.sql.EPSGFactory;
import org.apache.sis.util.logging.Logging;


/**
 * Provides the CRS, CS, datum and coordinate operation authority factories.
 * Provides also the system-wide {@link MultiAuthoritiesFactory} instance used by {@link CRS#forCode(String)}.
 * Current version handles the EPSG factory in a special way, but we may try to avoid doing special cases in a
 * future SIS version (this may require more help from {@link ServiceLoader}).
 *
 * @author  Martin Desruisseaux (Geomatys)
 */
final class AuthorityFactories<T extends AuthorityFactory> extends LazySet<T> {
    /**
     * The logger to use for reporting object creations.
     */
    static final Logger LOGGER = Logger.getLogger(Loggers.CRS_FACTORY);

    /**
     * An array containing only the EPSG factory. Content of this array is initially null.
     * The EPSG factory will be created when first needed by {@link #initialValues()}.
     * This array is returned directly (not cloned) by {@link #initialValues()}.
     */
    private static final GeodeticAuthorityFactory[] EPSG = new GeodeticAuthorityFactory[1];

    /**
     * The unique system-wide authority factory instance that contains all factories found on the module path,
     * plus the EPSG factory. The {@link EPSGFactoryProxy} most be excluded from this list, since the EPSG
     * factory is handled in a special way.
     */
    static final MultiAuthoritiesFactory ALL = new MultiAuthoritiesFactory(
            new AuthorityFactories<>(CRSAuthorityFactory.class),
            new AuthorityFactories<>(CSAuthorityFactory.class),
            new AuthorityFactories<>(DatumAuthorityFactory.class),
            new AuthorityFactories<>(CoordinateOperationAuthorityFactory.class))
    {
        /** Anonymous constructor */ {
            setLenient(true);
        }

        @Override
        public void reload() {
            EPSG(null);
            super.reload();
        }
    };

    /**
     * Registers a hook for forcing {@code ALL} to reload all CRS, CS, datum and coordinate operation factories
     * when the module path changed.
     */
    static {
        SystemListener.add(new SystemListener(Modules.REFERENCING) {
            @Override protected void classpathChanged() {ALL.reload();}
        });
    }

    /**
     * The type of service to request with {@link ServiceLoader}, or {@code null} if unknown.
     */
    private final Class<T> service;

    /**
     * Creates a new provider for factories of the given type.
     */
    private AuthorityFactories(final Class<T> type) {
        service = type;
    }

    /**
     * Creates the iterator which will provide the elements of this set before filtering.
     */
    @Override
    protected Iterator<? extends T> createSourceIterator() {
        return ServiceLoader.load(service, Reflect.getContextClassLoader()).iterator();
    }

    /**
     * Sets the EPSG factory to the given value.
     */
    static void EPSG(final GeodeticAuthorityFactory factory) {
        synchronized (EPSG) {
            EPSG[0] = factory;
        }
    }

    /**
     * Returns the factory connected to the EPSG geodetic dataset if possible, or the EPSG fallback otherwise.
     * If an EPSG data source has been found, then this method returns an instance of {@link EPSGFactory} but
     * there is no guarantee that attempts to use that factory will succeed; for example maybe the EPSG schema
     * does not exist. Callers should be prepared to either receive an {@link EPSGFactoryFallback} directly if
     * the EPSG data source does not exist, or replace the {@code EPSGFactory} by a {@code EPSGFactoryFallback}
     * later if attempt to use the returned factory fails.
     */
    static GeodeticAuthorityFactory EPSG() {
        synchronized (EPSG) {
            GeodeticAuthorityFactory factory = EPSG[0];
            if (factory == null) {
                try {
                    factory = new EPSGFactory(null);
                } catch (FactoryException e) {
                    log(e, false);
                    factory = EPSGFactoryFallback.INSTANCE;
                }
                EPSG[0] = factory;
            }
            return factory;
        }
    }

    /**
     * Returns the fallback to use if the authority factory is not available. Unless the problem may be temporary,
     * this method replaces the {@link EPSGFactory} instance by {@link EPSGFactoryFallback} in order to prevent
     * the same exception to be thrown and logged on every calls to {@link CRS#forCode(String)}.
     */
    static GeodeticAuthorityFactory fallback(final UnavailableFactoryException e) throws UnavailableFactoryException {
        final boolean isTransient = (e.getCause() instanceof SQLTransientException);
        final AuthorityFactory unavailable = e.getUnavailableFactory();
        GeodeticAuthorityFactory factory;
        final boolean alreadyDone;
        synchronized (EPSG) {
            factory = EPSG[0];
            alreadyDone = (factory == EPSGFactoryFallback.INSTANCE);
            if (!alreadyDone) {                             // May have been set in another thread (race condition).
                if (unavailable != factory) {
                    throw e;                                // Exception did not come from a factory that we control.
                }
                factory = EPSGFactoryFallback.INSTANCE;
                if (!isTransient) {
                    ALL.reload();
                    EPSG[0] = factory;
                }
            }
        }
        if (!alreadyDone) {
            log(e, true);
        }
        return factory;
    }

    /**
     * Notifies that a factory is unavailable, but without giving a fallback and without logging.
     * The caller is responsible for throwing an exception, or for logging a warning and provide its own fallback.
     *
     * @return {@code false} if the caller can try again, or {@code true} if the failure can be considered final.
     */
    static boolean failure(final UnavailableFactoryException e) {
        if (!(e.getCause() instanceof SQLTransientException)) {
            final AuthorityFactory unavailable = e.getUnavailableFactory();
            synchronized (EPSG) {
                final GeodeticAuthorityFactory factory = EPSG[0];
                if (factory == EPSGFactoryFallback.INSTANCE) {      // May have been set in another thread.
                    return false;
                }
                if (unavailable == factory) {
                    ALL.reload();
                    EPSG[0] = EPSGFactoryFallback.INSTANCE;
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Logs the given exception at the given level. This method pretends that the logging come from
     * {@link CRS#getAuthorityFactory(String)}, which is the public facade for {@link #EPSG()}.
     */
    private static void log(final Exception e, final boolean isWarning) {
        String message = e.getMessage();        // Prefer the locale of system administrator.
        if (message == null) {
            message = e.toString();
        }
        final LogRecord record = new LogRecord(isWarning ? Level.WARNING : Level.CONFIG, message);
        if (isWarning && !(e instanceof UnavailableFactoryException)) {
            record.setThrown(e);
        }
        Logging.completeAndLog(LOGGER, CRS.class, "getAuthorityFactory", record);
    }

    /**
     * Invoked by {@link LazySet} for adding the EPSG factory before any other factory fetched by {@code ServiceLoader}.
     * We put the EPSG factory first because it is often used anyway even for {@code CRS} and {@code AUTO} namespaces.
     *
     * <p>This method tries to instantiate an {@link EPSGFactory} if possible,
     * or an {@link EPSGFactoryFallback} otherwise.</p>
     *
     * @return the EPSG factory in an array. Callers shall not modify the returned array.
     */
    @Override
    @SuppressWarnings("unchecked")
    protected T[] initialValues() {
        EPSG();                         // Force EPSGFactory instantiation if not already done.
        return (T[]) EPSG;
    }

    /**
     * Invoked by {@link LazySet} for fetching the next element from the given iterator.
     * Skips the {@link EPSGFactoryProxy} if possible, or returns {@code null} otherwise.
     * Note that {@link MultiAuthoritiesFactory} is safe to null values.
     */
    @Override
    protected T next(final Iterator<? extends T> it) {
        T e = it.next();
        if (e instanceof EPSGFactoryProxy) {
            e = it.hasNext() ? it.next() : null;
        }
        return e;
    }
}
