/*
 * 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.spark.sql.connector.catalog;

import org.apache.spark.annotation.Evolving;
import org.apache.spark.sql.connector.expressions.Transform;
import org.apache.spark.sql.catalyst.analysis.NoSuchNamespaceException;
import org.apache.spark.sql.catalyst.analysis.NoSuchTableException;
import org.apache.spark.sql.catalyst.analysis.TableAlreadyExistsException;
import org.apache.spark.sql.errors.QueryCompilationErrors;
import org.apache.spark.sql.errors.QueryExecutionErrors;
import org.apache.spark.sql.types.StructType;
import org.apache.spark.sql.util.CaseInsensitiveStringMap;

import java.util.ArrayList;
import java.util.Map;
import java.util.Set;

/**
 * Catalog API for connectors that expose tables.
 * <p>
 * Connectors that expose <i>only</i> tables implement this interface. Connectors that expose
 * both tables and views must implement {@link RelationCatalog} (which extends both this
 * interface and {@link ViewCatalog} and adds the cross-cutting contract for the combined
 * case); the methods on this interface remain table-only -- they do not interact with views.
 * <p>
 * TableCatalog implementations may be case-sensitive or case-insensitive. Spark will pass
 * {@link Identifier table identifiers} without modification. Field names passed to
 * {@link #alterTable(Identifier, TableChange...)} will be normalized to match the case used in
 * the table schema when updating, renaming, or dropping existing columns when catalyst
 * analysis is case-insensitive.
 *
 * @since 3.0.0
 */
@Evolving
public interface TableCatalog extends CatalogPlugin {

  /**
   * A reserved property to specify the location of the table. The files of the table
   * should be under this location. The location is a Hadoop Path string.
   */
  String PROP_LOCATION = "location";

  /**
   * A reserved property to indicate that the table location is managed, not user-specified.
   * If this property is "true", it means it's a managed table even if it has a location. As an
   * example, SHOW CREATE TABLE will not generate the LOCATION clause.
   */
  String PROP_IS_MANAGED_LOCATION = "is_managed_location";

  /**
   * A reserved property to specify a table was created with EXTERNAL.
   */
  String PROP_EXTERNAL = "external";

  /**
   * A reserved property that indicates table entity type (external, managed, view, etc.).
   */
  String PROP_TABLE_TYPE = "table_type";

  /**
   * A reserved property to specify the description of the table.
   */
  String PROP_COMMENT = "comment";

  /**
   * A reserved property to specify the collation of the table.
   */
  String PROP_COLLATION = "collation";

  /**
   * A reserved property to specify the provider of the table.
   */
  String PROP_PROVIDER = "provider";

  /**
   * A reserved property to specify the owner of the table.
   */
  String PROP_OWNER = "owner";

  /**
   * A prefix used to pass OPTIONS in table properties
   */
  String OPTION_PREFIX = "option.";

  /**
   * @return the set of capabilities for this TableCatalog
   */
  default Set<TableCatalogCapability> capabilities() { return Set.of(); }

  /**
   * List the tables in a namespace from the catalog.
   *
   * @param namespace a multi-part namespace
   * @return an array of Identifiers for tables
   * @throws NoSuchNamespaceException If the namespace does not exist (optional).
   */
  Identifier[] listTables(String[] namespace) throws NoSuchNamespaceException;

  /**
   * List the table summaries in a namespace from the catalog.
   * <p>
   * Returns one summary per entry returned by {@link #listTables}. Each {@link TableSummary}
   * carries the entry's {@code tableType}.
   * <p>
   * The default implementation enumerates via {@link #listTables} + {@link #loadTable}.
   * Catalogs that can fetch summaries in a single round-trip should override.
   *
   * @param namespace a multi-part namespace
   * @return an array of summaries for tables in the namespace
   * @throws NoSuchNamespaceException If the namespace does not exist (optional).
   * @throws NoSuchTableException If certain table listed by listTables API does not exist.
   */
  default TableSummary[] listTableSummaries(String[] namespace)
          throws NoSuchNamespaceException, NoSuchTableException {
    Identifier[] tableIdentifiers = this.listTables(namespace);
    ArrayList<TableSummary> tableSummaries = new ArrayList<>(tableIdentifiers.length);
    for (Identifier identifier : tableIdentifiers) {
      Table table = this.loadTable(identifier);

      // If table type property is not present, we assume that table type is `FOREIGN`.
      String tableType = table.properties().getOrDefault(
              TableCatalog.PROP_TABLE_TYPE,
              TableSummary.FOREIGN_TABLE_TYPE);

      tableSummaries.add(TableSummary.of(identifier, tableType));
    };

    return tableSummaries.toArray(TableSummary[]::new);
  }

  /**
   * Load table metadata by {@link Identifier identifier} from the catalog.
   *
   * @param ident a table identifier
   * @return the table's metadata
   * @throws NoSuchTableException If the table doesn't exist
   */
  Table loadTable(Identifier ident) throws NoSuchTableException;

  /**
   * Load table metadata by {@link Identifier identifier} from the catalog. Spark will write data
   * into this table later.
   *
   * @param ident a table identifier
   * @param writePrivileges
   * @return the table's metadata
   * @throws NoSuchTableException If the table doesn't exist
   *
   * @since 3.5.3
   */
  default Table loadTable(
      Identifier ident,
      Set<TableWritePrivilege> writePrivileges) throws NoSuchTableException {
    return loadTable(ident);
  }

  /**
   * Load table metadata of a specific version by {@link Identifier identifier} from the catalog.
   *
   * @param ident a table identifier
   * @param version version of the table
   * @return the table's metadata
   * @throws NoSuchTableException If the table doesn't exist
   */
  default Table loadTable(Identifier ident, String version) throws NoSuchTableException {
    throw QueryCompilationErrors.noSuchTableError(name(), ident);
  }

  /**
   * Load table metadata at a specific time by {@link Identifier identifier} from the catalog.
   *
   * @param ident a table identifier
   * @param timestamp timestamp of the table, which is microseconds since 1970-01-01 00:00:00 UTC
   * @return the table's metadata
   * @throws NoSuchTableException If the table doesn't exist
   */
  default Table loadTable(Identifier ident, long timestamp) throws NoSuchTableException {
    throw QueryCompilationErrors.noSuchTableError(name(), ident);
  }

  /**
   * Load a {@link Changelog} for the given table, representing the row-level changes within the
   * range specified by {@code context}.
   * <p>
   * The default implementation throws an analysis exception indicating that the catalog does
   * not support CDC. Catalogs that support CDC must override this method.
   *
   * @param ident a table identifier
   * @param context the CDC query context (range, deduplication mode, etc.)
   * @param options all options passed to the changelog query, including the CDC-recognized
   *                keys (range, deduplication mode, etc.) that are also parsed into {@code context}
   * @return a Changelog instance for the requested table and range
   * @throws NoSuchTableException If the table doesn't exist
   *
   * @since 4.2.0
   */
  default Changelog loadChangelog(
      Identifier ident,
      ChangelogContext context,
      CaseInsensitiveStringMap options) throws NoSuchTableException {
    throw new UnsupportedOperationException(name() + " does not support Change Data Capture (CDC)");
  }

  /**
   * Invalidate cached table metadata for an {@link Identifier identifier}.
   * <p>
   * If the table is already loaded or cached, drop cached data. If the table does not exist or is
   * not cached, do nothing. Calling this method should not query remote services.
   *
   * @param ident a table identifier
   */
  default void invalidateTable(Identifier ident) {
  }

  /**
   * Test whether a table exists using an {@link Identifier identifier} from the catalog.
   *
   * @param ident a table identifier
   * @return true if a table exists at {@code ident}, false otherwise
   */
  default boolean tableExists(Identifier ident) {
    try {
      return loadTable(ident) != null;
    } catch (NoSuchTableException e) {
      return false;
    }
  }

  /**
   * Create a table in the catalog.
   * <p>
   * @deprecated This is deprecated. Please override
   * {@link #createTable(Identifier, Column[], Transform[], Map)} instead.
   */
  @Deprecated(since = "3.4.0")
  default Table createTable(
      Identifier ident,
      StructType schema,
      Transform[] partitions,
      Map<String, String> properties) throws TableAlreadyExistsException, NoSuchNamespaceException {
    throw QueryCompilationErrors.mustOverrideOneMethodError("createTable");
  }

  /**
   * Create a table in the catalog.
   * <p>
   * @deprecated This is deprecated. Please override
   * {@link #createTable(Identifier, TableInfo)} instead.
   */
  @Deprecated(since = "4.1.0")
  default Table createTable(
      Identifier ident,
      Column[] columns,
      Transform[] partitions,
      Map<String, String> properties) throws TableAlreadyExistsException, NoSuchNamespaceException {
    return createTable(ident, CatalogV2Util.v2ColumnsToStructType(columns), partitions, properties);
  }

  /**
   * Create a table in the catalog.
   *
   * @param ident a table identifier
   * @param tableInfo information about the table
   * @return metadata for the new table. This can be null if getting the metadata for the new table
   *         is expensive. Spark will call {@link #loadTable(Identifier)} if needed (e.g. CTAS).
   *
   * @throws TableAlreadyExistsException If a table already exists for the identifier
   * @throws UnsupportedOperationException If a requested partition transform is not supported
   * @throws NoSuchNamespaceException If the identifier namespace does not exist (optional)
   * @since 4.1.0
   */
  default Table createTable(Identifier ident, TableInfo tableInfo)
      throws TableAlreadyExistsException, NoSuchNamespaceException {
    return createTable(ident, tableInfo.columns(), tableInfo.partitions(), tableInfo.properties());
  }

  /**
   * Create a table in the catalog by copying metadata from an existing source table.
   * <p>
   * This method is called for {@code CREATE TABLE ... LIKE ...} statements targeting this catalog.
   * The {@code tableInfo} parameter contains all the explicit information for the new table:
   * columns and partitioning copied from the source, any constraints copied from the source,
   * user-specified TBLPROPERTIES / LOCATION / USING provider (if given), and
   * {@link #PROP_OWNER} set to the current user. Source table properties are intentionally
   * excluded from {@code tableInfo}; connectors may read {@code sourceTable.properties()} to
   * clone additional format-specific or custom state as appropriate for their implementation.
   * <p>
   * The default implementation throws {@link UnsupportedOperationException}. Connectors that
   * support {@code CREATE TABLE ... LIKE ...} must override this method.
   *
   * @param ident a table identifier for the new table
   * @param tableInfo complete description of the new table: columns, partitioning, constraints,
   *                  explicit properties (user overrides + owner); source table properties
   *                  are NOT included
   * @param sourceTable the resolved source table; connectors may read format-specific properties
   *                    or other custom state from this object to clone additional metadata
   * @return metadata for the new table
   *
   * @throws TableAlreadyExistsException If a table already exists for the identifier
   * @throws NoSuchNamespaceException If the identifier namespace does not exist (optional)
   * @throws UnsupportedOperationException If the catalog does not support CREATE TABLE LIKE
   * @since 4.2.0
   */
  default Table createTableLike(
      Identifier ident, TableInfo tableInfo, Table sourceTable)
      throws TableAlreadyExistsException, NoSuchNamespaceException {
    throw new UnsupportedOperationException(name() + " does not support CREATE TABLE LIKE");
  }

  /**
   * If true, mark all the fields of the query schema as nullable when executing
   * CREATE/REPLACE TABLE ... AS SELECT ... and creating the table.
   */
  default boolean useNullableQuerySchema() {
    return true;
  }

  /**
   * Apply a set of {@link TableChange changes} to a table in the catalog.
   * <p>
   * Implementations may reject the requested changes. If any change is rejected, none of the
   * changes should be applied to the table.
   * <p>
   * The requested changes must be applied in the order given.
   *
   * @param ident a table identifier
   * @param changes changes to apply to the table
   * @return updated metadata for the table. This can be null if getting the metadata for the
   *         updated table is expensive. Spark always discard the returned table here.
   *
   * @throws NoSuchTableException If the table doesn't exist
   * @throws IllegalArgumentException If any change is rejected by the implementation.
   */
  Table alterTable(
      Identifier ident,
      TableChange... changes) throws NoSuchTableException;

  /**
   * Drop a table in the catalog.
   *
   * @param ident a table identifier
   * @return true if a table was deleted, false if no table exists for the identifier
   */
  boolean dropTable(Identifier ident);

  /**
   * Drop a table in the catalog and completely remove its data by skipping a trash even if it is
   * supported.
   * <p>
   * If the catalog supports to purge a table, this method should be overridden.
   * The default implementation throws {@link UnsupportedOperationException}.
   *
   * @param ident a table identifier
   * @return true if a table was deleted, false if no table exists for the identifier
   * @throws UnsupportedOperationException If table purging is not supported
   *
   * @since 3.1.0
   */
  default boolean purgeTable(Identifier ident) throws UnsupportedOperationException {
    throw QueryExecutionErrors.unsupportedPurgeTableError();
  }

  /**
   * Renames a table in the catalog.
   * <p>
   * If the catalog does not support table renames between namespaces, it throws
   * {@link UnsupportedOperationException}.
   *
   * @param oldIdent the table identifier of the existing table to rename
   * @param newIdent the new table identifier of the table
   * @throws NoSuchTableException If the table to rename doesn't exist
   * @throws TableAlreadyExistsException If the new table name already exists
   * @throws UnsupportedOperationException If the namespaces of old and new identifiers do not
   *                                       match (optional)
   */
  void renameTable(Identifier oldIdent, Identifier newIdent)
      throws NoSuchTableException, TableAlreadyExistsException;
}
