/*
 * 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.Map;
import java.util.Objects;
import java.util.Optional;
import org.opengis.util.NameSpace;
import org.opengis.util.NameFactory;
import org.opengis.util.GenericName;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.iso.DefaultNameFactory;

import static org.apache.sis.storage.sql.feature.Database.WILDCARD;


/**
 * Definition of a resource (table, view or query) to include in a {@link SQLStore}.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.4
 * @since   1.1
 */
public final class ResourceDefinition {
    /**
     * The namespace for table names, created when first needed.
     * Used for specifying the name separator, which is {@code '.'}.
     */
    private static volatile NameSpace tableNS;

    /**
     * The table name or the query name.
     * This field has two meanings, depending on whether {@link #query} is null or not:
     *
     * <ul>
     *   <li>If {@link #query} is null, then this is the fully qualified name (including catalog and schema)
     *       of the table to include in the store. It may contain {@code LIKE} wildcard characters, in which
     *       case all tables matching the pattern will be included.</li>
     *   <li>If {@link #query} is non-null, then this is an arbitrary name to assign to the resource which will
     *       contain the query result.</li>
     * </ul>
     *
     * @see #getName()
     */
    private final GenericName name;

    /**
     * The SQL query to execute for the resource, or {@code null} if the resource is a table or view.
     *
     * @see #getQuery()
     */
    final String query;

    /**
     * Creates a new definition.
     */
    private ResourceDefinition(final GenericName name, final String query) {
        this.name  = name;
        this.query = query;
    }

    /**
     * Wraps the given table names and queries in an array of resource definitions.
     */
    static ResourceDefinition[] wrap(final GenericName[] tableNames, final Map<?,?> queries) {
        final int tableCount = (tableNames != null) ? tableNames.length : 0;
        final ResourceDefinition[] definitions = new ResourceDefinition[
                (queries != null) ? queries.size() + tableCount : tableCount];
        for (int i=0; i<tableCount; i++) {
            final GenericName name = tableNames[i];
            ArgumentChecks.ensureNonNullElement("tables", i, name);
            definitions[i] = new ResourceDefinition(name, null);
        }
        if (queries != null) {
            int i = 0;
            for (final Map.Entry<?,?> entry : queries.entrySet()) {
                /*
                 * Keys should be instances of `GenericName`, but strings are accepted as well
                 * as a convenience for local names.
                 */
                Object p = entry.getKey();
                ArgumentChecks.ensureNonNullElement("queries[#].key", i, p);
                final GenericName name = (p instanceof GenericName) ? (GenericName) p :
                        DefaultNameFactory.provider().createLocalName(null, p.toString());
                /*
                 * Values shall be non-empty strings.
                 */
                p = entry.getValue();
                ArgumentChecks.ensureNonNullElement("queries[#].value", i, p);
                final String sql = p.toString();
                ArgumentChecks.ensureNonEmpty("sql", sql);
                definitions[tableCount + i] = new ResourceDefinition(name, sql);
            }
        }
        return definitions;
    }

    /**
     * Creates a resource definition for a table or a view in any catalog and schema of the database.
     * The table name can contain SQL wildcard characters:
     * {@code '_'} matches any single character and {@code '%'} matches any sequence of characters.
     *
     * @param  tablePattern   pattern (with {@code '_'} and {@code '%'} wildcards) of a table.
     * @return resource definition for the named table.
     */
    public static ResourceDefinition table(final String tablePattern) {
        return table(null, null, tablePattern);
    }

    /**
     * Creates a resource definition for a table or a view in the database.
     * The table name can be any of the followings:
     *
     * <ul>
     *   <li>{@code catalog.schemaPattern.tablePattern}</li>
     *   <li>{@code schemaPattern.tablePattern}</li>
     *   <li>{@code tablePattern}</li>
     * </ul>
     *
     * The schema and table names (but not the catalog) can contain SQL wildcard characters:
     * {@code '_'} matches any single character and {@code '%'} matches any sequence of characters.
     *
     * @param  catalog        name of a catalog as it is stored in the database, or {@code null} for any catalog.
     * @param  schemaPattern  pattern (with {@code '_'} and {@code '%'} wildcards) of a schema, or {@code null} for any schema.
     * @param  tablePattern   pattern (with {@code '_'} and {@code '%'} wildcards) of a table.
     * @return resource definition for the named table.
     */
    @SuppressWarnings("fallthrough")
    public static ResourceDefinition table(final String catalog, String schemaPattern, final String tablePattern) {
        ArgumentChecks.ensureNonNull("tablePattern", tablePattern);
        final int numParts;
        if (catalog != null) {
            numParts = 3;
            if (schemaPattern == null) {
                schemaPattern = WILDCARD;
            }
        } else if (schemaPattern != null && !schemaPattern.equals(WILDCARD)) {
            numParts = 2;
        } else {
            numParts = 1;
        }
        final String[] names = new String[numParts];
        int i = 0;
        switch (numParts) {
            default: throw new AssertionError(numParts);
            case 3: names[i++] = catalog;           // Fall through
            case 2: names[i++] = schemaPattern;     // Fall through
            case 1: names[i]   = tablePattern;
        }
        final NameFactory factory = DefaultNameFactory.provider();
        NameSpace ns = tableNS;
        if (ns == null) {
            var properties = Map.of("separator",      ".",
                                    "separator.head", ":");
            tableNS = ns = factory.createNameSpace(factory.createLocalName(null, "database"), properties);
        }
        return new ResourceDefinition(factory.createGenericName(ns, names), null);
    }

    /**
     * Creates a resource definition for a SQL query.
     * Each column in the query should have a distinct name, using SQL {@code AS} keyword if needed.
     * It is caller's responsibility to ensure that the given query is not subject to SQL injection vulnerability.
     *
     * @param  name   name of the resource.
     * @param  query  the SQL query to execute.
     * @return resource definition for the given SQL query.
     */
    public static ResourceDefinition query(final String name, final String query) {
        ArgumentChecks.ensureNonEmpty("name",  name);
        ArgumentChecks.ensureNonEmpty("query", query);
        final NameFactory factory = DefaultNameFactory.provider();
        return new ResourceDefinition(factory.createLocalName(null, name), query);
    }

    /**
     * Returns the name of the table, view or query to access as a resource.
     * There is small differences in the way it is used depending on whether
     * the resource is a table or a query:
     *
     * <ul>
     *   <li>If the resource is a table or a view, then this is the fully qualified name (including catalog and schema)
     *       of the table or view to include in the store. It may contain {@code LIKE} wildcard characters, in which
     *       case all tables matching the pattern will be included.</li>
     *   <li>If the resource is a query, then this is an arbitrary name to assign to the resource which will contain
     *       the query result.</li>
     * </ul>
     *
     * @return the name of the table, view or query.
     */
    public GenericName getName() {
        return name;
    }

    /**
     * Returns the SQL query to execute for the resource, or empty if the resource is a table or a view.
     *
     * @return the SQL query to execute for the resource.
     */
    public Optional<String> getQuery() {
        return Optional.ofNullable(query);
    }

    /**
     * Returns {@code true} if this definition is equal to the given object.
     *
     * @param  obj  another object, or {@code null}.
     * @return {@code true} if the other object is a resource definition equals to this one.
     */
    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof ResourceDefinition) {
            final ResourceDefinition other = (ResourceDefinition) obj;
            return name.equals(other.name) && Objects.equals(query, other.query);
        }
        return false;
    }

    /**
     * Returns a hash code value for this resource definition.
     *
     * @return a hash code value.
     */
    @Override
    public int hashCode() {
        return name.hashCode() * 71 + Objects.hashCode(query);
    }

    /**
     * Returns a string representation of this resource definition.
     *
     * @return a string representation of this resource definition.
     */
    @Override
    public String toString() {
        final StringBuilder b = new StringBuilder("Resource[\"").append(name).append('"');
        if (query != null) {
            b.append(" = ").append(query);
        }
        return b.append(']').toString();
    }
}
