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

import com.datastax.driver.core.utils.Bytes;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/** Basic information on the execution of a query. */
public class ExecutionInfo {
  private final int speculativeExecutions;
  private final int successfulExecutionIndex;
  private final List<Host> triedHosts;
  private final ConsistencyLevel achievedConsistency;
  private final QueryTrace trace;
  private final ByteBuffer pagingState;
  private final ProtocolVersion protocolVersion;
  private final CodecRegistry codecRegistry;
  private final Statement statement;
  private volatile boolean schemaInAgreement;
  private final List<String> warnings;
  private final Map<String, ByteBuffer> incomingPayload;

  private ExecutionInfo(
      int speculativeExecutions,
      int successfulExecutionIndex,
      List<Host> triedHosts,
      ConsistencyLevel achievedConsistency,
      QueryTrace trace,
      ByteBuffer pagingState,
      ProtocolVersion protocolVersion,
      CodecRegistry codecRegistry,
      Statement statement,
      boolean schemaAgreement,
      List<String> warnings,
      Map<String, ByteBuffer> incomingPayload) {
    this.speculativeExecutions = speculativeExecutions;
    this.successfulExecutionIndex = successfulExecutionIndex;
    this.triedHosts = triedHosts;
    this.achievedConsistency = achievedConsistency;
    this.trace = trace;
    this.pagingState = pagingState;
    this.protocolVersion = protocolVersion;
    this.codecRegistry = codecRegistry;
    this.statement = statement;
    this.schemaInAgreement = schemaAgreement;
    this.warnings = warnings;
    this.incomingPayload = incomingPayload;
  }

  ExecutionInfo(Host singleHost) {
    this(
        0,
        0,
        ImmutableList.of(singleHost),
        null,
        null,
        null,
        null,
        null,
        null,
        true,
        Collections.<String>emptyList(),
        null);
  }

  public ExecutionInfo(
      int speculativeExecutions,
      int successfulExecutionIndex,
      List<Host> triedHosts,
      ConsistencyLevel achievedConsistency,
      Map<String, ByteBuffer> customPayload) {
    this(
        speculativeExecutions,
        successfulExecutionIndex,
        triedHosts,
        achievedConsistency,
        null,
        null,
        null,
        null,
        null,
        false,
        null,
        customPayload);
  }

  ExecutionInfo with(
      QueryTrace newTrace,
      List<String> newWarnings,
      ByteBuffer newPagingState,
      Statement newStatement,
      ProtocolVersion protocolVersion,
      CodecRegistry codecRegistry) {
    return new ExecutionInfo(
        speculativeExecutions,
        successfulExecutionIndex,
        triedHosts,
        achievedConsistency,
        newTrace,
        newPagingState,
        protocolVersion,
        codecRegistry,
        newStatement,
        schemaInAgreement,
        newWarnings,
        incomingPayload);
  }

  /**
   * The list of tried hosts for this query.
   *
   * <p>In general, this will be a singleton list with the host that coordinated that query.
   * However:
   *
   * <ul>
   *   <li>if a host is tried by the driver but is dead or in error, that host is recorded and the
   *       query is retried;
   *   <li>on a timeout or unavailable exception, some {@link
   *       com.datastax.driver.core.policies.RetryPolicy} may retry the query on the same host, so
   *       the same host might appear twice.
   *   <li>if {@link com.datastax.driver.core.policies.SpeculativeExecutionPolicy speculative
   *       executions} are enabled, this will also contain hosts that were tried by other executions
   *       (however, note that this only contains hosts which timed out, or for which a response was
   *       received; if an execution is waiting for a response from a host and another execution
   *       completes the request in the meantime, then the host of the first execution will not be
   *       in that list).
   * </ul>
   *
   * <p>If you are only interested in fetching the final (and often only) node coordinating the
   * query, {@link #getQueriedHost} provides a shortcut to fetch the last element of the list
   * returned by this method.
   *
   * @return the list of tried hosts for this query, in the order tried.
   */
  public List<Host> getTriedHosts() {
    return triedHosts;
  }

  /**
   * Return the Cassandra host that coordinated this query.
   *
   * <p>This is a shortcut for {@code getTriedHosts().get(getTriedHosts().size() - 1)}.
   *
   * @return return the Cassandra host that coordinated this query.
   */
  public Host getQueriedHost() {
    return triedHosts.get(triedHosts.size() - 1);
  }

  /**
   * The number of speculative executions that were started for this query.
   *
   * <p>This does not include the initial, normal execution of the query. Therefore, if speculative
   * executions are disabled, this will always be 0. If they are enabled and one speculative
   * execution was triggered in addition to the initial execution, this will be 1, etc.
   *
   * @see #getSuccessfulExecutionIndex()
   * @see
   *     Cluster.Builder#withSpeculativeExecutionPolicy(com.datastax.driver.core.policies.SpeculativeExecutionPolicy)
   */
  public int getSpeculativeExecutions() {
    return speculativeExecutions;
  }

  /**
   * The index of the execution that completed this query.
   *
   * <p>0 represents the initial, normal execution of the query, 1 represents the first speculative
   * execution, etc.
   *
   * @see #getSpeculativeExecutions()
   * @see
   *     Cluster.Builder#withSpeculativeExecutionPolicy(com.datastax.driver.core.policies.SpeculativeExecutionPolicy)
   */
  public int getSuccessfulExecutionIndex() {
    return successfulExecutionIndex;
  }

  /**
   * If the query returned without achieving the requested consistency level due to the {@link
   * com.datastax.driver.core.policies.RetryPolicy}, this return the biggest consistency level that
   * has been actually achieved by the query.
   *
   * <p>Note that the default {@code RetryPolicy} ({@link
   * com.datastax.driver.core.policies.DefaultRetryPolicy}) will never allow a query to be
   * successful without achieving the initially requested consistency level and hence with that
   * default policy, this method will <b>always</b> return {@code null}. However, it might
   * occasionally return a non-{@code null} with say, {@link
   * com.datastax.driver.core.policies.DowngradingConsistencyRetryPolicy}.
   *
   * @return {@code null} if the original consistency level of the query was achieved, or the
   *     consistency level that was ultimately achieved if the {@code RetryPolicy} triggered a retry
   *     at a different consistency level than the original one.
   */
  public ConsistencyLevel getAchievedConsistencyLevel() {
    return achievedConsistency;
  }

  /**
   * Return the query trace if tracing was enabled on this query.
   *
   * <p>Note that accessing the fields of the the returned object will trigger a <b>blocking</b>
   * background query.
   *
   * @return the {@code QueryTrace} object for this query if tracing was enable for this query, or
   *     {@code null} otherwise.
   */
  public QueryTrace getQueryTrace() {
    return trace;
  }

  /**
   * Placeholder for async query trace retrieval (not implemented yet).
   *
   * <p>Async query trace retrieval will be implemented in a future version. This method is added
   * now to avoid breaking binary compatibility later. The current implementation merely wraps the
   * result of {@link #getQueryTrace()} in an immediate future; it will still trigger a blocking
   * query when the query trace's fields are accessed.
   *
   * @return currently, an immediate future containing the result of {@link #getQueryTrace()}.
   */
  public ListenableFuture<QueryTrace> getQueryTraceAsync() {
    return Futures.immediateFuture(trace);
  }

  /**
   * The paging state of the query.
   *
   * <p>This object represents the next page to be fetched if this query is multi page. It can be
   * saved and reused later on the same statement.
   *
   * @return the paging state or null if there is no next page.
   * @see Statement#setPagingState(PagingState)
   */
  public PagingState getPagingState() {
    if (this.pagingState == null) return null;
    return new PagingState(
        this.pagingState, this.statement, this.protocolVersion, this.codecRegistry);
  }

  /**
   * Returns the "raw" paging state of the query.
   *
   * <p>Contrary to {@link #getPagingState()}, there will be no validation when this is later
   * reinjected into a statement.
   *
   * @return the paging state or null if there is no next page.
   * @see Statement#setPagingStateUnsafe(byte[])
   */
  public byte[] getPagingStateUnsafe() {
    if (this.pagingState == null) return null;
    return Bytes.getArray(this.pagingState);
  }

  /**
   * Whether the cluster had reached schema agreement after the execution of this query.
   *
   * <p>After a successful schema-altering query (ex: creating a table), the driver will check if
   * the cluster's nodes agree on the new schema version. If not, it will keep retrying for a given
   * delay (configurable via {@link Cluster.Builder#withMaxSchemaAgreementWaitSeconds(int)}).
   *
   * <p>If this method returns {@code false}, clients can call {@link
   * Metadata#checkSchemaAgreement()} later to perform the check manually.
   *
   * <p>Note that the schema agreement check is only performed for schema-altering queries For other
   * query types, this method will always return {@code true}.
   *
   * @return whether the cluster reached schema agreement, or {@code true} for a non schema-altering
   *     statement.
   */
  public boolean isSchemaInAgreement() {
    return schemaInAgreement;
  }

  void setSchemaInAgreement(boolean schemaAgreement) {
    this.schemaInAgreement = schemaAgreement;
  }

  /**
   * Returns the server-side warnings for this query.
   *
   * <p>This feature is only available with {@link ProtocolVersion#V4} or above; with lower
   * versions, the returned list will always be empty.
   *
   * @return the warnings, or an empty list if there are none.
   * @since 2.2
   */
  public List<String> getWarnings() {
    return warnings;
  }

  /**
   * Return the incoming payload, that is, the payload that the server sent back with its response,
   * if any, or {@code null}, if the server did not include any custom payload.
   *
   * <p>This method returns a read-only view of the original map, but its values remain inherently
   * mutable. Callers should take care not to modify the returned map in any way.
   *
   * <p>This feature is only available with {@link ProtocolVersion#V4} or above; with lower
   * versions, this method will always return {@code null}.
   *
   * @return the custom payload that the server sent back with its response, if any, or {@code
   *     null}, if the server did not include any custom payload.
   * @since 2.2
   */
  public Map<String, ByteBuffer> getIncomingPayload() {
    return incomingPayload;
  }

  /**
   * Get the statement that has been executed.
   *
   * @return the statement executed.
   */
  public Statement getStatement() {
    return this.statement;
  }
}
