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

import java.util.Optional;
import java.util.Collection;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.lang.reflect.Method;
import org.opengis.util.GenericName;
import org.opengis.metadata.Metadata;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.metadata.spatial.SpatialRepresentationType;
import org.apache.sis.storage.Resource;
import org.apache.sis.storage.Aggregate;
import org.apache.sis.storage.DataStore;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.IllegalNameException;
import org.apache.sis.storage.StorageConnector;
import org.apache.sis.storage.event.StoreEvent;
import org.apache.sis.storage.event.StoreListener;
import org.apache.sis.storage.event.WarningEvent;
import org.apache.sis.internal.sql.feature.Database;
import org.apache.sis.internal.sql.feature.Resources;
import org.apache.sis.internal.storage.MetadataBuilder;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.Exceptions;


/**
 * A data store capable to read and create features from a spatial database.
 * {@code SQLStore} requires a {@link DataSource} to be specified (indirectly) at construction time.
 * The {@code DataSource} should provide pooled connections, because {@code SQLStore} will frequently
 * opens and closes them.
 *
 * @author  Johann Sorel (Geomatys)
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.0
 * @since   1.0
 * @module
 */
public class SQLStore extends DataStore implements Aggregate {
    /**
     * Names of possible public getter methods for data source title, in preference order.
     */
    private static final String[] NAME_GETTERS = {
            "getDescription",           // PostgreSQL, SQL Server
            "getDataSourceName",        // Derby
            "getDatabaseName",          // Derby, PostgreSQL, SQL Server
            "getUrl",                   // PostgreSQL
            "getURL"                    // SQL Server
    };

    /**
     * The data source to use for obtaining connections to the database.
     */
    private final DataSource source;

    /**
     * The result of inspecting database schema for deriving {@link org.apache.sis.feature.DefaultFeatureType}s.
     * Created when first needed. May be discarded and recreated if the store needs a refresh.
     */
    private Database model;

    /**
     * Fully qualified names (including catalog and schema) of the tables to include in this store.
     */
    private final GenericName[] tableNames;

    /**
     * The metadata, created when first requested.
     */
    private Metadata metadata;

    /**
     * Creates a new instance for the given storage.
     * The given {@code connector} shall contain a {@link DataSource}.
     * The given table names shall be qualified names of 1, 2 or 3 components.
     * The name components are {@code <catalog>.<schema pattern>.<table pattern>} where:
     *
     * <ul>
     *   <li>{@code <catalog>}, if present, is the name of a catalog as stored in the database.</li>
     *   <li>{@code <schema pattern>}, if present, is the pattern of a schema.
     *       The pattern can use {@code '_'} and {@code '%'} wildcards characters.</li>
     *   <li>{@code <table pattern>} (mandatory) is the pattern of a table.
     *       The pattern can use {@code '_'} and {@code '%'} wildcards characters.</li>
     * </ul>
     *
     * Qualified table names can be created by the {@link SQLStoreProvider#createTableName(String, String, String)}
     * convenience method. Only the main tables need to be specified; dependencies will be followed automatically.
     *
     * @param  provider    the factory that created this {@code DataStore} instance, or {@code null} if unspecified.
     * @param  connector   information about the storage (JDBC data source, <i>etc</i>).
     * @param  tableNames  fully qualified names (including catalog and schema) of the tables to include in this store.
     * @throws DataStoreException if an error occurred while creating the data store for the given storage.
     */
    protected SQLStore(final SQLStoreProvider provider, final StorageConnector connector, GenericName... tableNames)
            throws DataStoreException
    {
        super(provider, connector);
        source = connector.getStorageAs(DataSource.class);
        ArgumentChecks.ensureNonNull("tableNames", tableNames);
        tableNames = tableNames.clone();
        for (int i=0; i<tableNames.length; i++) {
            final GenericName name = tableNames[i];
            ArgumentChecks.ensureNonNullElement("tableNames", i, tableNames);
            final int depth = name.depth();
            if (depth < 1 || depth > 3) {
                throw new IllegalNameException(Resources.format(Resources.Keys.IllegalQualifiedName_1, name));
            }
        }
        this.tableNames = tableNames;
    }

    /**
     * Returns the parameters used to open this netCDF data store.
     * The parameters are described by {@link SQLStoreProvider#getOpenParameters()} and contains
     * at least a parameter named {@value SQLStoreProvider#LOCATION} with a {@link DataSource} value.
     *
     * @return parameters used for opening this data store.
     */
    @Override
    public Optional<ParameterValueGroup> getOpenParameters() {
        if (provider == null) {
            return Optional.empty();
        }
        final ParameterValueGroup pg = provider.getOpenParameters().createValue();
        pg.parameter(SQLStoreProvider.LOCATION).setValue(source);
        pg.parameter(SQLStoreProvider.TABLES).setValue(tableNames);
        return Optional.of(pg);
    }

    /**
     * SQL data store root resource has no identifier.
     *
     * @return empty.
     */
    @Override
    public Optional<GenericName> getIdentifier() throws DataStoreException {
        return Optional.empty();
    }

    /**
     * Returns the database model, analyzing the database schema when first needed.
     */
    private synchronized Database model() throws DataStoreException {
        if (model == null) {
            try (Connection c = source.getConnection()) {
                model = new Database(this, c, source, tableNames, listeners);
            } catch (SQLException e) {
                throw new DataStoreException(Exceptions.unwrap(e));
            }
        }
        return model;
    }

    /**
     * Returns the database model, analyzing the database schema when first needed.
     * This method performs the same work than {@link #model()}, but using an existing connection.
     * Callers must own a synchronization lock on {@code this}.
     *
     * @param c  connection to the database.
     */
    private Database model(final Connection c) throws DataStoreException, SQLException {
        if (model == null) {
            model = new Database(this, c, source, tableNames, listeners);
        }
        return model;
    }

    /**
     * Returns information about the dataset as a whole. The returned metadata object can contain information
     * such as the list of feature types.
     *
     * @return information about the dataset.
     * @throws DataStoreException if an error occurred while reading the data.
     */
    @Override
    public synchronized Metadata getMetadata() throws DataStoreException {
        if (metadata == null) {
            final MetadataBuilder builder = new MetadataBuilder();
            builder.addSpatialRepresentation(SpatialRepresentationType.TEXT_TABLE);
            try (Connection c = source.getConnection()) {
                final Database model = model(c);
                if (model.hasGeometry) {
                    builder.addSpatialRepresentation(SpatialRepresentationType.VECTOR);
                }
                model.listTables(c.getMetaData(), builder);
            } catch (SQLException e) {
                throw new DataStoreException(Exceptions.unwrap(e));
            }
            /*
             * Try to find a title from the data source description.
             */
            for (final String c : NAME_GETTERS) {
                try {
                    final Method method = source.getClass().getMethod(c);
                    if (method.getReturnType() == String.class) {
                        String name = (String) method.invoke(source);
                        if (name != null && !(name = name.trim()).isEmpty()) {
                            builder.addTitle(name);
                            break;
                        }
                    }
                } catch (NoSuchMethodException | SecurityException e) {
                    // Ignore - try the next method.
                } catch (ReflectiveOperationException e) {
                    throw new DataStoreException(Exceptions.unwrap(e));
                }
            }
            metadata = builder.build(true);
        }
        return metadata;
    }

    /**
     * Returns the resources (features or coverages) in this SQL store.
     * The list contains only the tables explicitly named at construction time.
     *
     * @return children resources that are components of this SQL store.
     * @throws DataStoreException if an error occurred while fetching the components.
     */
    @Override
    public Collection<Resource> components() throws DataStoreException {
        return model().tables();
    }

    /**
     * Searches for a resource identified by the given identifier.
     * The given identifier should match one of the table names.
     * It may be one of the tables named at construction time, or one of the dependencies.
     * The given name may be qualified with the schema name, or may be only the table name if there is no ambiguity.
     *
     * @param  identifier  identifier of the resource to fetch. Must be non-null.
     * @return resource associated to the given identifier (never {@code null}).
     * @throws IllegalNameException if no resource is found for the given identifier, or if more than one resource is found.
     * @throws DataStoreException if another kind of error occurred while searching resources.
     */
    @Override
    public Resource findResource(final String identifier) throws DataStoreException {
        return model().findTable(this, identifier);
    }

    /**
     * Registers a listener to notify when the specified kind of event occurs in this data store.
     * The current implementation of this data store can emit only {@link WarningEvent}s;
     * any listener specified for another kind of events will be ignored.
     */
    @Override
    public <T extends StoreEvent> void addListener(Class<T> eventType, StoreListener<? super T> listener) {
        // If an argument is null, we let the parent class throws (indirectly) NullArgumentException.
        if (listener == null || eventType == null || eventType.isAssignableFrom(WarningEvent.class)) {
            super.addListener(eventType, listener);
        }
    }

    /**
     * Closes this SQL store and releases any underlying resources.
     *
     * @throws DataStoreException if an error occurred while closing the SQL store.
     */
    @Override
    public void close() throws DataStoreException {
    }
}
