/*
 * Copyright DataStax, Inc.
 *
 * Licensed 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.
 */

'use strict';

const utils = require('../utils');
const errors = require('../errors');

const asyncIteratorSymbol = Symbol.asyncIterator || '@@asyncIterator';

/** @module types */

/**
 * Creates a new instance of ResultSet.
 * @class
 * @classdesc Represents the result of a query.
 * @param {Object} response
 * @param {String} host
 * @param {Object} triedHosts
 * @param {Number} speculativeExecutions
 * @param {Number} consistency
 * @param {Boolean} isSchemaInAgreement
 * @constructor
 */
function ResultSet(response, host, triedHosts, speculativeExecutions, consistency, isSchemaInAgreement) {
  // if no execution was made at all, set to 0.
  if (speculativeExecutions === -1) {
    speculativeExecutions = 0;
  }
  /**
   * Information on the execution of a successful query:
   * @member {Object}
   * @property {Number} achievedConsistency The consistency level that has been actually achieved by the query.
   * @property {String} queriedHost The Cassandra host that coordinated this query.
   * @property {Object} triedHosts Gets the associative array of host that were queried before getting a valid response,
   * being the last host the one that replied correctly.
   * @property {Object} speculativeExecutions The number of speculative executions (not including the first) executed before
   * getting a valid response.
   * @property {Uuid} traceId Identifier of the trace session.
   * @property {Array.<string>} warnings Warning messages generated by the server when executing the query.
   * @property {Boolean} isSchemaInAgreement 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 (see <code>protocolOptions.maxSchemaAgreementWaitSeconds</code>).
   * </p>
   * <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</code>. If this method returns <code>false</code>,
   *   clients can call [Metadata.checkSchemaAgreement()]{@link module:metadata~Metadata#checkSchemaAgreement} later to
   *   perform the check manually.
   * </p>
   */
  this.info = {
    queriedHost: host,
    triedHosts: triedHosts,
    speculativeExecutions: speculativeExecutions,
    achievedConsistency: consistency,
    traceId: null,
    warnings: null,
    customPayload: null,
    isSchemaInAgreement
  };

  if (response.flags) {
    this.info.traceId = response.flags.traceId;
    this.info.warnings = response.flags.warnings;
    this.info.customPayload = response.flags.customPayload;
  }

  /**
   * Gets an array rows returned by the query.
   * When the result set represents a response from a write query, this property will be <code>undefined</code>.
   * When the read query result contains more rows than the fetch size (5000), this property will only contain the
   * first rows up to fetch size. To obtain all the rows, you can use the built-in async iterator that will retrieve the
   * following pages of results.
   * @type {Array<Row>|undefined}
   */
  this.rows = response.rows;

  /**
   * Gets the row length of the result, regardless if the result has been buffered or not
   * @type {Number|undefined}
   */
  this.rowLength = this.rows ? this.rows.length : response.rowLength;

  /**
   * Gets the columns returned in this ResultSet.
   * @type {Array.<{name, type}>}
   * @default null
   */
  this.columns = null;

  /**
   * A string token representing the current page state of query. It can be used in the following executions to
   * continue paging and retrieve the remained of the result for the query.
   * @type {String|null}
   * @default null
   */
  this.pageState = null;

  /**
   * Method used to manually fetch the next page of results.
   * This method is only exposed when using the {@link Client#eachRow} method and there are more rows available in
   * following pages.
   * @type Function
   */
  this.nextPage = undefined;

  /**
   * Method used internally to fetch the next page of results using promises.
   * @internal
   * @ignore
   * @type {Function}
   */
  this.nextPageAsync = undefined;

  const meta = response.meta;

  if (meta) {
    this.columns = meta.columns;

    if (meta.pageState) {
      this.pageState = meta.pageState.toString('hex');

      // Expose rawPageState internally
      Object.defineProperty(this, 'rawPageState', { value: meta.pageState, enumerable: false });
    }
  }
}

/**
 * Returns the first row or null if the result rows are empty.
 */
ResultSet.prototype.first = function () {
  if (this.rows && this.rows.length) {
    return this.rows[0];
  }
  return null;
};

ResultSet.prototype.getPageState = function () {
  // backward-compatibility
  return this.pageState;
};

ResultSet.prototype.getColumns = function () {
  // backward-compatibility
  return this.columns;
};

/**
 * When this instance is the result of a conditional update query, it returns whether it was successful.
 * Otherwise, it returns <code>true</code>.
 * <p>
 *   For consistency, this method always returns <code>true</code> for non-conditional queries (although there is
 *   no reason to call the method in that case). This is also the case for conditional DDL statements
 *   (CREATE KEYSPACE... IF NOT EXISTS, CREATE TABLE... IF NOT EXISTS), for which the server doesn't return
 *   information whether it was applied or not.
 * </p>
 */
ResultSet.prototype.wasApplied = function () {
  if (!this.rows || this.rows.length === 0) {
    return true;
  }
  const firstRow = this.rows[0];
  const applied = firstRow['[applied]'];
  return typeof applied === 'boolean' ? applied : true;
};

/**
 * Gets the iterator function.
 * <p>
 *   Retrieves the iterator of the underlying fetched rows, without causing the driver to fetch the following
 *   result pages. For more information on result paging,
 *   [visit the documentation]{@link http://docs.datastax.com/en/developer/nodejs-driver/latest/features/paging/}.
 * </p>
 * @alias module:types~ResultSet#@@iterator
 * @see {@link module:types~ResultSet#@@asyncIterator}
 * @example <caption>Using for...of statement</caption>
 * const query = 'SELECT user_id, post_id, content FROM timeline WHERE user_id = ?';
 * const result = await client.execute(query, [ id ], { prepare: true });
 * for (const row of result) {
 *   console.log(row['email']);
 * }
 * @returns {Iterator.<Row>}
 */
ResultSet.prototype[Symbol.iterator] = function getIterator() {
  if (!this.rows) {
    return utils.emptyArray[Symbol.iterator]();
  }
  return this.rows[Symbol.iterator]();
};

/**
 * Gets the async iterator function.
 * <p>
 *   Retrieves the async iterator representing the entire query result, the driver will fetch the following result
 *   pages.
 * </p>
 * <p>Use the async iterator when the query result might contain more rows than the <code>fetchSize</code>.</p>
 * <p>
 *   Note that using the async iterator will not affect the internal state of the <code>ResultSet</code> instance.
 *   You should avoid using both <code>rows</code> property that contains the row instances of the first page of
 *   results, and the async iterator, that will yield all the rows in the result regardless on the number of pages.
 * </p>
 * <p>Multiple concurrent async iterations are not supported.</p>
 * @alias module:types~ResultSet#@@asyncIterator
 * @example <caption>Using for await...of statement</caption>
 * const query = 'SELECT user_id, post_id, content FROM timeline WHERE user_id = ?';
 * const result = await client.execute(query, [ id ], { prepare: true });
 * for await (const row of result) {
 *   console.log(row['email']);
 * }
 * @returns {AsyncIterator<Row>}
 */
ResultSet.prototype[asyncIteratorSymbol] = function getAsyncGenerator() {
  let index = 0;
  let pageState = this.rawPageState;
  let rows = this.rows;

  if (!rows || rows.length === 0) {
    return { next: () => Promise.resolve({ done: true }) };
  }

  const self = this;

  // Async generators are not present in Node.js 8, implement it manually
  return {
    async next() {
      if (index >= rows.length && pageState) {
        if (!self.nextPageAsync) {
          throw new errors.DriverInternalError('Property nextPageAsync should be set when pageState is defined');
        }

        const rs = await self.nextPageAsync(pageState);
        rows = rs.rows;
        index = 0;
        pageState = rs.rawPageState;
      }

      if (index < rows.length) {
        return { done: false, value: rows[index++] };
      }

      return { done: true };
    }
  };
};

/**
 * Determines whether there are more pages of results.
 * If so, the driver will initially retrieve and contain only the first page of results.
 * To obtain all the rows, use the [AsyncIterator]{@linkcode module:types~ResultSet#@@asyncIterator}.
 * @returns {boolean}
 */
ResultSet.prototype.isPaged = function() {
  return !!this.rawPageState;
};

module.exports = ResultSet;