/*
 * 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 com.datastax.oss.driver.api.core.cql;

import com.datastax.oss.driver.api.core.ConsistencyLevel;
import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.NoNodeAvailableException;
import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
import com.datastax.oss.driver.api.core.config.DriverExecutionProfile;
import com.datastax.oss.driver.api.core.context.DriverContext;
import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy;
import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance;
import com.datastax.oss.driver.api.core.metadata.Node;
import com.datastax.oss.driver.api.core.metadata.token.Token;
import com.datastax.oss.driver.api.core.session.Request;
import com.datastax.oss.driver.api.core.session.Session;
import com.datastax.oss.driver.api.core.time.TimestampGenerator;
import com.datastax.oss.driver.api.core.type.reflect.GenericType;
import com.datastax.oss.driver.internal.core.util.RoutingKey;
import com.datastax.oss.protocol.internal.request.query.QueryOptions;
import edu.umd.cs.findbugs.annotations.CheckReturnValue;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.CompletionStage;

/**
 * A request to execute a CQL query.
 *
 * @param <SelfT> the "self type" used for covariant returns in subtypes.
 */
public interface Statement<SelfT extends Statement<SelfT>> extends Request {
  // Implementation note: "CqlRequest" would be a better name, but we keep "Statement" to match
  // previous driver versions.

  /**
   * The type returned when a CQL statement is executed synchronously.
   *
   * <p>Most users won't use this explicitly. It is needed for the generic execute method ({@link
   * Session#execute(Request, GenericType)}), but CQL statements will generally be run with one of
   * the driver's built-in helper methods (such as {@link CqlSession#execute(Statement)}).
   */
  GenericType<ResultSet> SYNC = GenericType.of(ResultSet.class);

  /**
   * The type returned when a CQL statement is executed asynchronously.
   *
   * <p>Most users won't use this explicitly. It is needed for the generic execute method ({@link
   * Session#execute(Request, GenericType)}), but CQL statements will generally be run with one of
   * the driver's built-in helper methods (such as {@link CqlSession#executeAsync(Statement)}).
   */
  GenericType<CompletionStage<AsyncResultSet>> ASYNC =
      new GenericType<CompletionStage<AsyncResultSet>>() {};

  /**
   * A special value for {@link #getQueryTimestamp()} that means "no value".
   *
   * <p>It is equal to {@link Long#MIN_VALUE}.
   */
  long NO_DEFAULT_TIMESTAMP = QueryOptions.NO_DEFAULT_TIMESTAMP;

  /**
   * A special value for {@link #getNowInSeconds()} that means "no value".
   *
   * <p>It is equal to {@link Integer#MIN_VALUE}.
   */
  int NO_NOW_IN_SECONDS = QueryOptions.NO_NOW_IN_SECONDS;

  /**
   * Sets the name of the execution profile that will be used for this statement.
   *
   * <p>For all the driver's built-in implementations, calling this method with a non-null argument
   * automatically resets {@link #getExecutionProfile()} to null.
   *
   * <p>All the driver's built-in implementations are immutable, and return a new instance from this
   * method. However custom implementations may choose to be mutable and return the same instance.
   */
  @NonNull
  @CheckReturnValue
  SelfT setExecutionProfileName(@Nullable String newConfigProfileName);

  /**
   * Sets the execution profile to use for this statement.
   *
   * <p>For all the driver's built-in implementations, calling this method with a non-null argument
   * automatically resets {@link #getExecutionProfileName()} to null.
   *
   * <p>All the driver's built-in implementations are immutable, and return a new instance from this
   * method. However custom implementations may choose to be mutable and return the same instance.
   */
  @NonNull
  @CheckReturnValue
  SelfT setExecutionProfile(@Nullable DriverExecutionProfile newProfile);

  /**
   * Sets the keyspace to use for token-aware routing.
   *
   * <p>See {@link Request#getRoutingKey()} for a description of the token-aware routing algorithm.
   *
   * @param newRoutingKeyspace The keyspace to use, or {@code null} to disable token-aware routing.
   */
  @NonNull
  @CheckReturnValue
  SelfT setRoutingKeyspace(@Nullable CqlIdentifier newRoutingKeyspace);

  /**
   * Sets the {@link Node} that should handle this query.
   *
   * <p>In the general case, use of this method is <em>heavily discouraged</em> and should only be
   * used in the following cases:
   *
   * <ol>
   *   <li>Querying node-local tables, such as tables in the {@code system} and {@code system_views}
   *       keyspaces.
   *   <li>Applying a series of schema changes, where it may be advantageous to execute schema
   *       changes in sequence on the same node.
   * </ol>
   *
   * <p>Configuring a specific node causes the configured {@link LoadBalancingPolicy} to be
   * completely bypassed. However, if the load balancing policy dictates that the node is at
   * distance {@link NodeDistance#IGNORED} or there is no active connectivity to the node, the
   * request will fail with a {@link NoNodeAvailableException}.
   *
   * @param node The node that should be used to handle executions of this statement or null to
   *     delegate to the configured load balancing policy.
   */
  @NonNull
  @CheckReturnValue
  SelfT setNode(@Nullable Node node);

  /**
   * Shortcut for {@link #setRoutingKeyspace(CqlIdentifier)
   * setRoutingKeyspace(CqlIdentifier.fromCql(newRoutingKeyspaceName))}.
   *
   * @param newRoutingKeyspaceName The keyspace to use, or {@code null} to disable token-aware
   *     routing.
   */
  @NonNull
  @CheckReturnValue
  default SelfT setRoutingKeyspace(@Nullable String newRoutingKeyspaceName) {
    return setRoutingKeyspace(
        newRoutingKeyspaceName == null ? null : CqlIdentifier.fromCql(newRoutingKeyspaceName));
  }

  /**
   * Sets the key to use for token-aware routing.
   *
   * <p>See {@link Request#getRoutingKey()} for a description of the token-aware routing algorithm.
   *
   * @param newRoutingKey The routing key to use, or {@code null} to disable token-aware routing.
   */
  @NonNull
  @CheckReturnValue
  SelfT setRoutingKey(@Nullable ByteBuffer newRoutingKey);

  /**
   * Sets the key to use for token-aware routing, when the partition key has multiple components.
   *
   * <p>This method assembles the components into a single byte buffer and passes it to {@link
   * #setRoutingKey(ByteBuffer)}. Neither the individual components, nor the vararg array itself,
   * can be {@code null}.
   */
  @NonNull
  @CheckReturnValue
  default SelfT setRoutingKey(@NonNull ByteBuffer... newRoutingKeyComponents) {
    return setRoutingKey(RoutingKey.compose(newRoutingKeyComponents));
  }

  /**
   * Sets the token to use for token-aware routing.
   *
   * <p>See {@link Request#getRoutingKey()} for a description of the token-aware routing algorithm.
   *
   * @param newRoutingToken The routing token to use, or {@code null} to disable token-aware
   *     routing.
   */
  @NonNull
  @CheckReturnValue
  SelfT setRoutingToken(@Nullable Token newRoutingToken);

  /**
   * Sets the custom payload to use for execution.
   *
   * <p>All the driver's built-in statement implementations are immutable, and return a new instance
   * from this method. However custom implementations may choose to be mutable and return the same
   * instance.
   *
   * <p>Note that it's your responsibility to provide a thread-safe map. This can be achieved with a
   * concurrent or immutable implementation, or by making it effectively immutable (meaning that
   * it's never modified after being set on the statement).
   */
  @NonNull
  @CheckReturnValue
  SelfT setCustomPayload(@NonNull Map<String, ByteBuffer> newCustomPayload);

  /**
   * Sets the idempotence to use for execution.
   *
   * <p>All the driver's built-in implementations are immutable, and return a new instance from this
   * method. However custom implementations may choose to be mutable and return the same instance.
   *
   * @param newIdempotence a boolean instance to set a statement-specific value, or {@code null} to
   *     use the default idempotence defined in the configuration.
   */
  @NonNull
  @CheckReturnValue
  SelfT setIdempotent(@Nullable Boolean newIdempotence);

  /**
   * Sets tracing for execution.
   *
   * <p>All the driver's built-in implementations are immutable, and return a new instance from this
   * method. However custom implementations may choose to be mutable and return the same instance.
   */
  @NonNull
  @CheckReturnValue
  SelfT setTracing(boolean newTracing);

  /**
   * @deprecated this method only exists to ease the transition from driver 3, it is an alias for
   *     {@link #setTracing(boolean) setTracing(true)}.
   */
  @Deprecated
  @NonNull
  @CheckReturnValue
  default SelfT enableTracing() {
    return setTracing(true);
  }

  /**
   * @deprecated this method only exists to ease the transition from driver 3, it is an alias for
   *     {@link #setTracing(boolean) setTracing(false)}.
   */
  @Deprecated
  @NonNull
  @CheckReturnValue
  default SelfT disableTracing() {
    return setTracing(false);
  }

  /**
   * Returns the query timestamp, in microseconds, to send with the statement. See {@link
   * #setQueryTimestamp(long)} for details.
   *
   * <p>If this is equal to {@link #NO_DEFAULT_TIMESTAMP}, the {@link TimestampGenerator} configured
   * for this driver instance will be used to generate a timestamp.
   *
   * @see #NO_DEFAULT_TIMESTAMP
   * @see TimestampGenerator
   */
  long getQueryTimestamp();

  /**
   * @deprecated this method only exists to ease the transition from driver 3, it is an alias for
   *     {@link #getQueryTimestamp()}.
   */
  @Deprecated
  default long getDefaultTimestamp() {
    return getQueryTimestamp();
  }

  /**
   * Sets the query timestamp, in microseconds, to send with the statement.
   *
   * <p>This is an alternative to appending a {@code USING TIMESTAMP} clause in the statement's
   * query string, and has the advantage of sending the timestamp separately from the query string
   * itself, which doesn't have to be modified when executing the same statement with different
   * timestamps. Note that, if both a {@code USING TIMESTAMP} clause and a query timestamp are set
   * for a given statement, the timestamp from the {@code USING TIMESTAMP} clause wins.
   *
   * <p>This method can be used on any instance of {@link SimpleStatement}, {@link BoundStatement}
   * or {@link BatchStatement}. For a {@link BatchStatement}, the timestamp will apply to all its
   * child statements; it is not possible to define per-child timestamps using this method, and
   * consequently, if this method is called on a batch child statement, the provided timestamp will
   * be silently ignored. If different timestamps are required for individual child statements, this
   * can only be achieved with a custom {@code USING TIMESTAMP} clause in each child query.
   *
   * <p>If this is equal to {@link #NO_DEFAULT_TIMESTAMP}, the {@link TimestampGenerator} configured
   * for this driver instance will be used to generate a timestamp.
   *
   * <p>All the driver's built-in implementations are immutable, and return a new instance from this
   * method. However custom implementations may choose to be mutable and return the same instance.
   *
   * @see #NO_DEFAULT_TIMESTAMP
   * @see TimestampGenerator
   */
  @NonNull
  @CheckReturnValue
  SelfT setQueryTimestamp(long newTimestamp);

  /**
   * @deprecated this method only exists to ease the transition from driver 3, it is an alias for
   *     {@link #setQueryTimestamp(long)}.
   */
  @Deprecated
  @NonNull
  @CheckReturnValue
  default SelfT setDefaultTimestamp(long newTimestamp) {
    return setQueryTimestamp(newTimestamp);
  }

  /**
   * Sets how long to wait for this request to complete. This is a global limit on the duration of a
   * session.execute() call, including any retries the driver might do.
   *
   * @param newTimeout the timeout to use, or {@code null} to use the default value defined in the
   *     configuration.
   * @see DefaultDriverOption#REQUEST_TIMEOUT
   */
  @NonNull
  @CheckReturnValue
  SelfT setTimeout(@Nullable Duration newTimeout);

  /**
   * Returns the paging state to send with the statement, or {@code null} if this statement has no
   * paging state.
   *
   * <p>Paging states are used in scenarios where a paged result is interrupted then resumed later.
   * The paging state can only be reused with the exact same statement (same query string, same
   * parameters). It is an opaque value that is only meant to be collected, stored and re-used. If
   * you try to modify its contents or reuse it with a different statement, the results are
   * unpredictable.
   */
  @Nullable
  ByteBuffer getPagingState();

  /**
   * Sets the paging state to send with the statement, or {@code null} if this statement has no
   * paging state.
   *
   * <p>Paging states are used in scenarios where a paged result is interrupted then resumed later.
   * The paging state can only be reused with the exact same statement (same query string, same
   * parameters). It is an opaque value that is only meant to be collected, stored and re-used. If
   * you try to modify its contents or reuse it with a different statement, the results are
   * unpredictable.
   *
   * <p>All the driver's built-in implementations are immutable, and return a new instance from this
   * method. However custom implementations may choose to be mutable and return the same instance;
   * if you do so, you must override {@link #copy(ByteBuffer)}.
   */
  @NonNull
  @CheckReturnValue
  SelfT setPagingState(@Nullable ByteBuffer newPagingState);

  /**
   * Sets the paging state to send with the statement, or {@code null} if this statement has no
   * paging state.
   *
   * <p>This variant uses the "safe" paging state wrapper, it will throw immediately if the
   * statement doesn't match the one that the state was initially extracted from (same query string,
   * same parameters). The advantage is that it fails fast, instead of waiting for an error response
   * from the server.
   *
   * <p>Note that, if this statement is a {@link SimpleStatement} with bound values, those values
   * must be encoded in order to perform the check. This method uses the default codec registry and
   * default protocol version. This might fail if you use custom codecs; in that case, use {@link
   * #setPagingState(PagingState, Session)} instead.
   *
   * @throws IllegalArgumentException if the given state does not match this statement.
   * @see #setPagingState(ByteBuffer)
   * @see ExecutionInfo#getSafePagingState()
   */
  @NonNull
  @CheckReturnValue
  default SelfT setPagingState(@Nullable PagingState newPagingState) {
    return setPagingState(newPagingState, null);
  }

  /**
   * Alternative to {@link #setPagingState(PagingState)} that specifies the session the statement
   * will be executed with. <b>You only need this for simple statements, and if you use custom
   * codecs.</b> Bound statements already know which session they are attached to.
   */
  @NonNull
  @CheckReturnValue
  default SelfT setPagingState(@Nullable PagingState newPagingState, @Nullable Session session) {
    if (newPagingState == null) {
      return setPagingState((ByteBuffer) null);
    } else if (newPagingState.matches(this, session)) {
      return setPagingState(newPagingState.getRawPagingState());
    } else {
      throw new IllegalArgumentException(
          "Paging state mismatch, "
              + "this means that either the paging state contents were altered, "
              + "or you're trying to apply it to a different statement");
    }
  }

  /**
   * Returns the page size to use for the statement.
   *
   * @return the set page size, otherwise 0 or a negative value to use the default value defined in
   *     the configuration.
   * @see DefaultDriverOption#REQUEST_PAGE_SIZE
   */
  int getPageSize();

  /**
   * @deprecated this method only exists to ease the transition from driver 3, it is an alias for
   *     {@link #getPageSize()}.
   */
  @Deprecated
  default int getFetchSize() {
    return getPageSize();
  }

  /**
   * Configures how many rows will be retrieved simultaneously in a single network roundtrip (the
   * goal being to avoid loading too many results in memory at the same time).
   *
   * @param newPageSize the page size to use, set to 0 or a negative value to use the default value
   *     defined in the configuration.
   * @see DefaultDriverOption#REQUEST_PAGE_SIZE
   */
  @NonNull
  @CheckReturnValue
  SelfT setPageSize(int newPageSize);

  /**
   * @deprecated this method only exists to ease the transition from driver 3, it is an alias for
   *     {@link #setPageSize(int)}.
   */
  @Deprecated
  @NonNull
  @CheckReturnValue
  default SelfT setFetchSize(int newPageSize) {
    return setPageSize(newPageSize);
  }

  /**
   * Returns the {@link ConsistencyLevel} to use for the statement.
   *
   * @return the set consistency, or {@code null} to use the default value defined in the
   *     configuration.
   * @see DefaultDriverOption#REQUEST_CONSISTENCY
   */
  @Nullable
  ConsistencyLevel getConsistencyLevel();

  /**
   * Sets the {@link ConsistencyLevel} to use for this statement.
   *
   * @param newConsistencyLevel the consistency level to use, or null to use the default value
   *     defined in the configuration.
   * @see DefaultDriverOption#REQUEST_CONSISTENCY
   */
  @NonNull
  @CheckReturnValue
  SelfT setConsistencyLevel(@Nullable ConsistencyLevel newConsistencyLevel);

  /**
   * Returns the serial {@link ConsistencyLevel} to use for the statement.
   *
   * @return the set serial consistency, or {@code null} to use the default value defined in the
   *     configuration.
   * @see DefaultDriverOption#REQUEST_SERIAL_CONSISTENCY
   */
  @Nullable
  ConsistencyLevel getSerialConsistencyLevel();

  /**
   * Sets the serial {@link ConsistencyLevel} to use for this statement.
   *
   * @param newSerialConsistencyLevel the serial consistency level to use, or null to use the
   *     default value defined in the configuration.
   * @see DefaultDriverOption#REQUEST_SERIAL_CONSISTENCY
   */
  @NonNull
  @CheckReturnValue
  SelfT setSerialConsistencyLevel(@Nullable ConsistencyLevel newSerialConsistencyLevel);

  /** Whether tracing information should be recorded for this statement. */
  boolean isTracing();

  /**
   * A custom "now in seconds" to use when applying the request (for testing purposes).
   *
   * <p>This method's default implementation returns {@link #NO_NOW_IN_SECONDS}. The only reason it
   * exists is to preserve binary compatibility. Internally, the driver overrides it to return the
   * value that was set programmatically (if any).
   *
   * @see #NO_NOW_IN_SECONDS
   */
  default int getNowInSeconds() {
    return NO_NOW_IN_SECONDS;
  }

  /**
   * Sets the "now in seconds" to use when applying the request (for testing purposes).
   *
   * <p>This method's default implementation returns the statement unchanged. The only reason it
   * exists is to preserve binary compatibility. Internally, the driver overrides it to record the
   * new value.
   *
   * @see #NO_NOW_IN_SECONDS
   */
  @NonNull
  @CheckReturnValue
  @SuppressWarnings("unchecked")
  default SelfT setNowInSeconds(int nowInSeconds) {
    return (SelfT) this;
  }

  /**
   * Calculates the approximate size in bytes that the statement will have when encoded.
   *
   * <p>The size might be over-estimated by a few bytes due to global options that may be defined on
   * a {@link Session} but not explicitly set on the statement itself.
   *
   * <p>The result of this method is not cached, calling it will cause some encoding to be done in
   * order to determine some of the statement's attributes sizes. Therefore, use this method
   * sparingly in order to avoid unnecessary computation.
   *
   * @return the approximate number of bytes this statement will take when encoded.
   */
  int computeSizeInBytes(@NonNull DriverContext context);

  /**
   * Creates a <b>new instance</b> with a different paging state.
   *
   * <p>Since all the built-in statement implementations in the driver are immutable, this method's
   * default implementation delegates to {@link #setPagingState(ByteBuffer)}. However, if you write
   * your own mutable implementation, make sure it returns a different instance.
   */
  @NonNull
  @CheckReturnValue
  default SelfT copy(@Nullable ByteBuffer newPagingState) {
    return setPagingState(newPagingState);
  }
}
