'use strict';

const Stream = require('stream').Stream;
const utils = require('../utils');

/**
 * Utilities for concurrent query execution with the DataStax Node.js Driver.
 * @module concurrent
 */

/**
 * Executes multiple queries concurrently at the defined concurrency level.
 * @static
 * @param {Client} client The {@link Client} instance.
 * @param {String|Array<{query, params}>} query The query to execute per each parameter item.
 * @param {Array<Array>|Stream|Object} parameters An {@link Array} or a readable {@link Stream} composed of {@link Array}
 * items representing each individual set of parameters. Per each item in the {@link Array} or {@link Stream}, an
 * execution is going to be made.
 * @param {Object} [options] The execution options.
 * @param {String} [options.executionProfile] The execution profile to be used.
 * @param {Number} [options.concurrencyLevel=100] The concurrency level to determine the maximum amount of in-flight
 * operations at any given time
 * @param {Boolean} [options.raiseOnFirstError=true] Determines whether execution should stop after the first failed
 * execution and the corresponding exception will be raised.
 * @param {Boolean} [options.collectResults=false] Determines whether each individual
 * [ResultSet]{@link module:types~ResultSet} instance should be collected in the grouped result.
 * @param {Number} [options.maxErrors=100] The maximum amount of errors to be collected before ignoring the rest of
 * the error results.
 * @returns {Promise<ResultSetGroup>} A Promise of {@link ResultSetGroup} that is resolved when all the
 * executions completed and it's rejected when raiseOnFirstError is true and there is one
 * or more failures.
 * @example
Using a fixed query and an Array of Arrays as parameters

 * const query = 'INSERT INTO table1 (id, value) VALUES (?, ?)';
 * const parameters = [[1, 'a'], [2, 'b'], [3, 'c'], ]; // ...
 * const result = await executeConcurrent(client, query, parameters);
 * @example
Using a fixed query and a readable stream

 * const stream = csvStream.pipe(transformLineToArrayStream);
 * const result = await executeConcurrent(client, query, stream);
 * @example
Using a different queries

 * const queryAndParameters = [
 *   { query: 'INSERT INTO videos (id, name, user_id) VALUES (?, ?, ?)',
 *     params: [ id, name, userId ] },
 *   { query: 'INSERT INTO user_videos (user_id, id, name) VALUES (?, ?, ?)',
 *     params: [ userId, id, name ] },
 *   { query: 'INSERT INTO latest_videos (id, name, user_id) VALUES (?, ?, ?)',
 *     params: [ id, name, userId ] },
 * ];
 *
 * const result = await executeConcurrent(client, queryAndParameters);
 */
function executeConcurrent(client, query, parameters, options) {
  if (!client) {
    throw new TypeError('Client instance is not defined');
  }

  if (typeof query === 'string') {
    if (Array.isArray(parameters)) {
      return new ArrayBasedExecutor(client, query, parameters, options).execute();
    }

    if (parameters instanceof Stream) {
      return new StreamBasedExecutor(client, query, parameters, options).execute();
    }

    throw new TypeError('parameters should be an Array or a Stream instance');
  }

  if (Array.isArray(query)) {
    options = parameters;
    return new ArrayBasedExecutor(client, null, query, options).execute();
  }

  throw new TypeError('A string query or query and parameters array should be provided');
}

/**
 * Wraps the functionality to execute given an Array.
 * @ignore
 */
class ArrayBasedExecutor {

  /**
   * @param {Client} client
   * @param {String} query
   * @param {Array<Array>|Array<{query, params}>} parameters
   * @param {Object} [options] The execution options.
   * @private
   */
  constructor(client, query, parameters, options) {
    this._client = client;
    this._query = query;
    this._parameters = parameters;
    options = options || utils.emptyObject;
    this._raiseOnFirstError = options.raiseOnFirstError !== false;
    this._concurrencyLevel = Math.min(options.concurrencyLevel || 100, this._parameters.length);
    this._queryOptions = { prepare: true, executionProfile: options.executionProfile };
    this._result = new ResultSetGroup(options);
    this._stop = false;
  }

  execute() {
    const promises = new Array(this._concurrencyLevel);

    for (let i = 0; i < this._concurrencyLevel; i++) {
      promises[i] = this._executeOneAtATime(i, 0);
    }

    return Promise.all(promises).then(() => this._result);
  }

  _executeOneAtATime(initialIndex, iteration) {
    const index = initialIndex + this._concurrencyLevel * iteration;

    if (index >= this._parameters.length || this._stop) {
      return Promise.resolve();
    }

    const item = this._parameters[index];
    let query;
    let params;

    if (this._query === null) {
      query = item.query;
      params = item.params;
    } else {
      query = this._query;
      params = item;
    }

    return this._client.execute(query, params, this._queryOptions)
      .then(rs => this._result.setResultItem(index, rs))
      .catch(err => this._setError(index, err))
      .then(() => this._executeOneAtATime(initialIndex, iteration + 1));
  }

  _setError(index, err) {
    this._result.setError(index, err);

    if (this._raiseOnFirstError) {
      this._stop = true;
      throw err;
    }
  }
}

/**
 * Wraps the functionality to execute given a Stream.
 * @ignore
 */
class StreamBasedExecutor {

  /**
   * @param {Client} client
   * @param {String} query
   * @param {Stream} stream
   * @param {Object} [options] The execution options.
   * @private
   */
  constructor(client, query, stream, options) {
    this._client = client;
    this._query = query;
    this._stream = stream;
    options = options || utils.emptyObject;
    this._raiseOnFirstError = options.raiseOnFirstError !== false;
    this._concurrencyLevel = options.concurrencyLevel || 100;
    this._queryOptions = { prepare: true, executionProfile: options.executionProfile };
    this._inFlight = 0;
    this._index = 0;
    this._result = new ResultSetGroup(options);
    this._resolveCallback = null;
    this._rejectCallback = null;
    this._readEnded = false;
  }

  execute() {
    return new Promise((resolve, reject) => {
      this._resolveCallback = resolve;
      this._rejectCallback = reject;

      this._stream
        .on('data', params => this._executeOne(params))
        .on('error', err => this._setReadEnded(err))
        .on('end', () => this._setReadEnded());
    });
  }

  _executeOne(params) {
    if (!Array.isArray(params)) {
      return this._setReadEnded(new TypeError('Stream should be in objectMode and should emit Array instances'));
    }

    if (this._readEnded) {
      // Read ended abruptly because of incorrect format or error event being emitted.
      // We shouldn't consider additional items.
      return;
    }

    const index = this._index++;
    this._inFlight++;

    this._client.execute(this._query, params, this._queryOptions)
      .then(rs => {
        this._result.setResultItem(index, rs);
        this._inFlight--;
      })
      .catch(err => {
        this._inFlight--;
        this._setError(index, err);
      })
      .then(() => {
        if (this._stream.isPaused()) {
          this._stream.resume();
        }

        if (this._readEnded && this._inFlight === 0) {
          // When read ended and there are no more in-flight requests
          // We yield the result to the user.
          // It could have ended prematurely when there is a read error
          // or there was an execution error and raiseOnFirstError is true
          // In that case, calling the resolve callback has no effect
          this._resolveCallback(this._result);
        }
      });

    if (this._inFlight >= this._concurrencyLevel) {
      this._stream.pause();
    }
  }

  /**
   * Marks the stream read process as ended.
   * @param {Error} [err] The stream read error.
   * @private
   */
  _setReadEnded(err) {
    if (!this._readEnded) {
      this._readEnded = true;

      if (err) {
        // There was an error while reading from the input stream.
        // This should be surfaced as a failure
        this._rejectCallback(err);
      } else if (this._inFlight === 0) {
        // Ended signaled and there are no more pending messages.
        this._resolveCallback(this._result);
      }
    }
  }

  _setError(index, err) {
    this._result.setError(index, err);

    if (this._raiseOnFirstError) {
      this._readEnded = true;
      this._rejectCallback(err);
    }
  }
}

/**
 * Represents results from different related executions.
 */
class ResultSetGroup {

  /**
   * Creates a new instance of {@link ResultSetGroup}.
   * @ignore
   */
  constructor(options) {
    this._collectResults = options.collectResults;
    this._maxErrors = options.maxErrors || 100;
    this.totalExecuted = 0;
    this.errors = [];

    if (this._collectResults) {
      /**
       * Gets an {@link Array} containing the [ResultSet]{@link module:types~ResultSet} instances from each execution.
       *

       *   Note that when collectResults is set to false, accessing this property will
       *   throw an error.
       *

       * @type {Array}
       */
      this.resultItems = [];
    } else {
      Object.defineProperty(this, 'resultItems', { enumerable: false, get: () => {
        throw new Error('Property resultItems can not be accessed when collectResults is set to false');
      }});
    }
  }

  /** @ignore */
  setResultItem(index, rs) {
    this.totalExecuted++;

    if (this._collectResults) {
      this.resultItems[index] = rs;
    }
  }

  /**
   * Internal method to set the error of an execution.
   * @ignore
   */
  setError(index, err) {
    this.totalExecuted++;

    if (this.errors.length < this._maxErrors) {
      this.errors.push(err);
    }

    if (this._collectResults) {
      this.resultItems[index] = err;
    }
  }
}

exports.executeConcurrent = executeConcurrent;
exports.ResultSetGroup = ResultSetGroup;


"use strict";
const util = require('util');
const errors = require('./errors');

/**
 * Max int that can be accurately represented with 64-bit Number (2^53)
 * @type {number}
 * @const
 */
const maxInt = 9007199254740992;

const emptyObject = Object.freeze({});

function noop() {}

/**
 * Forward-compatible allocation of buffer, filled with zeros.
 * @type {Function}
 */
const allocBuffer = Buffer.alloc || allocBufferFillDeprecated;

/**
 * Forward-compatible unsafe allocation of buffer.
 * @type {Function}
 */
const allocBufferUnsafe = Buffer.allocUnsafe || allocBufferDeprecated;

/**
 * Forward-compatible allocation of buffer to contain a string.
 * @type {Function}
 */
const allocBufferFromString = (Int8Array.from !== Buffer.from && Buffer.from) || allocBufferFromStringDeprecated;

/**
 * Forward-compatible allocation of buffer from an array of bytes
 * @type {Function}
 */
const allocBufferFromArray = (Int8Array.from !== Buffer.from && Buffer.from) || allocBufferFromArrayDeprecated;

function allocBufferDeprecated(size) {
  // eslint-disable-next-line
  return new Buffer(size);
}

function allocBufferFillDeprecated(size) {
  const b = allocBufferDeprecated(size);
  b.fill(0);
  return b;
}

function allocBufferFromStringDeprecated(text, encoding) {
  if (typeof text !== 'string') {
    throw new TypeError('Expected string, obtained ' + util.inspect(text));
  }
  // eslint-disable-next-line
  return new Buffer(text, encoding);
}

function allocBufferFromArrayDeprecated(arr) {
  if (!Array.isArray(arr)) {
    throw new TypeError('Expected Array, obtained ' + util.inspect(arr));
  }
  // eslint-disable-next-line
  return new Buffer(arr);
}

/**
 * Creates a copy of a buffer
 */
function copyBuffer(buf) {
  const targetBuffer = allocBufferUnsafe(buf.length);
  buf.copy(targetBuffer);
  return targetBuffer;
}

/**
 * Appends the original stack trace to the error after a tick of the event loop
 */
function fixStack(stackTrace, error) {
  if (stackTrace) {
    error.stack += '\n  (event loop)\n' + stackTrace.substr(stackTrace.indexOf("\n") + 1);
  }
  return error;
}

/**
 * Uses the logEmitter to emit log events
 * @param {String} type
 * @param {String} info
 * @param [furtherInfo]
 */
function log(type, info, furtherInfo) {
  if (!this.logEmitter) {
    if (!this.options || !this.options.logEmitter) {
      throw new Error('Log emitter not defined');
    }
    this.logEmitter = this.options.logEmitter;
  }
  this.logEmitter('log', type, this.constructor.name, info, furtherInfo || '');
}

/**
 * Gets the sum of the length of the items of an array
 */
function totalLength (arr) {
  if (arr.length === 1) {
    return arr[0].length;
  }
  let total = 0;
  arr.forEach(function (item) {
    let length = item.length;
    length = length ? length : 0;
    total += length;
  });
  return total;
}

/**
 * Merge the contents of two or more objects together into the first object. Similar to jQuery.extend / Object.assign.
 * The main difference between this method is that declared properties with an undefined value are not set
 * to the target.
 */
function extend(target) {
  const sources = Array.prototype.slice.call(arguments, 1);
  sources.forEach(function (source) {
    if (!source) {
      return;
    }
    const keys = Object.keys(source);
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      const value = source[key];
      if (value === undefined) {
        continue;
      }
      target[key] = value;
    }
  });
  return target;
}

/**
 * Returns a new object with the property names set to lowercase.
 */
function toLowerCaseProperties(obj) {
  const keys = Object.keys(obj);
  const result = {};
  for (let i = 0; i < keys.length; i++) {
    const k = keys[i];
    result[k.toLowerCase()] = obj[k];
  }
  return result;
}

/**
 * Extends the target by the most inner props of sources
 * @param {Object} target
 * @returns {Object}
 */
function deepExtend(target) {
  const sources = Array.prototype.slice.call(arguments, 1);
  sources.forEach(function (source) {
    for (const prop in source) {
      if (!source.hasOwnProperty(prop)) {
        continue;
      }
      const targetProp = target[prop];
      const targetType = (typeof targetProp);
      //target prop is
      // a native single type
      // or not existent
      // or is not an anonymous object (not class instance)
      if (!targetProp ||
        targetType === 'number' ||
        targetType === 'string' ||
        util.isArray(targetProp) ||
        util.isDate(targetProp) ||
        targetProp.constructor.name !== 'Object') {
        target[prop] = source[prop];
      }
      else {
        //inner extend
        target[prop] = deepExtend({}, targetProp, source[prop]);
      }
    }
  });
  return target;
}

function propCompare(propName) {
  return function (a, b) {
    if (a[propName] > b[propName]) {
      return 1;
    }
    if (a[propName]  valB) {
      return 1;
    }
    if (valA < valB) {
      return -1;
    }
    return 0;
  });
}
/**
 * Uses the iterator protocol to go through the items of the Array
 * @param arr
 * @returns {{next: function}}
 */
function arrayIterator (arr) {
  let index = 0;
  return { next : function () {
    if (index >= arr.length) {
      return {done: true};
    }
    return {value: arr[index++], done: false };
  }};
}

/**
 * Convert the iterator values into an array
 * @param iterator
 * @returns {Array}
 */
function iteratorToArray(iterator) {
  const values = [];
  let item = iterator.next();
  while (!item.done) {
    values.push(item.value);
    item = iterator.next();
  }
  return values;
}

/**
 * Searches the specified Array for the provided key using the binary
 * search algorithm.  The Array must be sorted.
 * @param {Array} arr
 * @param key
 * @param {function} compareFunc
 * @returns {number} The position of the key in the Array, if it is found.
 * If it is not found, it returns a negative number which is the bitwise complement of the index of the first element that is larger than key.
 */
function binarySearch(arr, key, compareFunc) {
  let low = 0;
  let high = arr.length-1;

  while (low <= high) {
    const mid = (low + high) >>> 1;
    const midVal = arr[mid];
    const cmp = compareFunc(midVal, key);
    if (cmp < 0) {
      low = mid + 1;
    }
    else if (cmp > 0) {
      high = mid - 1;
    }
    else
    {
      //The key was found in the Array
      return mid;
    }
  }
  // key not found
  return ~low;
}

/**
 * Inserts the value in the position determined by its natural order determined by the compare func
 * @param {Array} arr
 * @param item
 * @param {function} compareFunc
 */
function insertSorted(arr, item, compareFunc) {
  if (arr.length === 0) {
    return arr.push(item);
  }
  let position = binarySearch(arr, item, compareFunc);
  if (position < 0) {
    position = ~position;
  }
  arr.splice(position, 0, item);
}

/**
 * Validates the provided parameter is of type function.
 * @param {Function} fn The instance to validate.
 * @param {String} [name] Name of the function to use in the error message. Defaults to 'callback'.
 * @returns {Function}
 */
function validateFn(fn, name) {
  if (typeof fn !== 'function') {
    throw new errors.ArgumentError(util.format('%s is not a function', name || 'callback'));
  }
  return fn;
}

/**
 * Adapts the parameters based on the prepared metadata.
 * If the params are passed as an associative array (Object),
 * it adapts the object into an array with the same order as columns
 * @param {Array|Object} params
 * @param {Array} columns
 * @returns {Array} Returns an array of parameters.
 * @throws {Error} In case a parameter with a specific name is not defined
 */
function adaptNamedParamsPrepared(params, columns) {
  if (!params || util.isArray(params) || !columns || columns.length === 0) {
    // params is an array or there aren't parameters
    return params;
  }
  const paramsArray = new Array(columns.length);
  params = toLowerCaseProperties(params);
  const keys = {};
  for (let i = 0; i < columns.length; i++) {
    const name = columns[i].name;
    if (!params.hasOwnProperty(name)) {
      throw new errors.ArgumentError(util.format('Parameter "%s" not defined', name));
    }
    paramsArray[i] = params[name];
    keys[name] = i;
  }
  return paramsArray;
}

/**
 * Adapts the associative-array of parameters and hints for simple statements
 * into Arrays based on the (arbitrary) position of the keys.
 * @param {Array|Object} params
 * @param {ExecutionOptions} execOptions
 * @returns {{ params: Array<{name, value}>, namedParameters: boolean, keyIndexes: object }} Returns an array of
 * parameters and the keys as an associative array.
 */
function adaptNamedParamsWithHints(params, execOptions) {
  if (!params || util.isArray(params)) {
    //The parameters is an Array or there isn't parameter
    return { params: params, namedParameters: false, keyIndexes: null };
  }

  const keys = Object.keys(params);
  const paramsArray = new Array(keys.length);
  const hints = new Array(keys.length);
  const userHints = execOptions.getHints() || emptyObject;
  const keyIndexes = {};

  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    // As lower cased identifiers
    paramsArray[i] = { name: key.toLowerCase(), value: params[key]};
    hints[i] = userHints[key];
    keyIndexes[key] = i;
  }

  execOptions.setHints(hints);

  return { params: paramsArray, namedParameters: true, keyIndexes };
}

/**
 * Returns a string with a value repeated n times
 * @param {String} val
 * @param {Number} times
 * @returns {String}
 */
function stringRepeat(val, times) {
  if (!times || times < 0) {
    return null;
  }
  if (times === 1) {
    return val;
  }
  return new Array(times + 1).join(val);
}

/**
 * Returns an array containing the values of the Object, similar to Object.values().
 * If obj is null or undefined, it will return an empty array.
 * @param {Object} obj
 * @returns {Array}
 */
function objectValues(obj) {
  if (!obj) {
    return exports.emptyArray;
  }
  const keys = Object.keys(obj);
  const values = new Array(keys.length);
  for (let i = 0; i < keys.length; i++) {
    values[i] = obj[keys[i]];
  }
  return values;
}

/**
 * Wraps the callback-based method. When no originalCallback is not defined, it returns a Promise.
 * @param {ClientOptions} options
 * @param {Function} originalCallback
 * @param {Function} handler
 * @returns {Promise|undefined}
 */
function promiseWrapper(options, originalCallback, handler) {
  if (typeof originalCallback === 'function') {
    // Callback-based invocation
    handler.call(this, originalCallback);
    return undefined;
  }
  const factory = options.promiseFactory || defaultPromiseFactory;
  const self = this;
  return factory(function handlerWrapper(callback) {
    handler.call(self, callback);
  });
}

/**
 * @param {Function} handler
 * @returns {Promise}
 */
function defaultPromiseFactory(handler) {
  return new Promise(function executor(resolve, reject) {
    handler(function handlerCallback(err, result) {
      if (err) {
        return reject(err);
      }
      resolve(result);
    });
  });
}

/**
 * Shuffles an Array in-place.
 * @param {Array} arr
 * @private
 */
function shuffleArray(arr) {
  // Fisher–Yates algorithm
  for (let i = arr.length - 1; i > 0; i--) {
    // Math.random() has an extremely short permutation cycle length but we don't care about collisions
    const j = Math.floor(Math.random() * (i + 1));
    const temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
  }
}

// Classes

/**
 * Represents a unique set of values.
 * @constructor
 */
function HashSet() {
  this.length = 0;
  this.items = {};
}

/**
 * Adds a new item to the set.
 * @param {Object} key
 * @returns {boolean} Returns true if it was added to the set; false if the key is already present.
 */
HashSet.prototype.add = function (key) {
  if (this.items[key]) {
    return false;
  }
  this.items[key] = true;
  this.length++;
  return true;
};

/**
 * @returns {boolean} Returns true if the key is present in the set.
 */
HashSet.prototype.contains = function (key) {
  return this.items[key] === true;
};

/**
 * Returns an array containing the set items.
 * @returns {Array}
 */
HashSet.prototype.toArray = function () {
  return Object.keys(this.items);
};

/**
 * @param {Array} arr
 * @param {Function} fn
 * @param {Function} [callback]
 */
function each(arr, fn, callback) {
  if (!Array.isArray(arr)) {
    throw new TypeError('First parameter is not an Array');
  }
  callback = callback || noop;
  const length = arr.length;
  if (length === 0) {
    return callback();
  }
  let completed = 0;
  for (let i = 0; i < length; i++) {
    fn(arr[i], next);
  }
  function next(err) {
    if (err) {
      const cb = callback;
      callback = noop;
      cb(err);
      return;
    }
    if (++completed !== length) {
      return;
    }
    callback();
  }
}

/**
 * @param {Array} arr
 * @param {Function} fn
 * @param {Function} [callback]
 */
function eachSeries(arr, fn, callback) {
  if (!Array.isArray(arr)) {
    throw new TypeError('First parameter is not an Array');
  }
  callback = callback || noop;
  const length = arr.length;
  if (length === 0) {
    return callback();
  }
  let sync;
  let index = 1;
  fn(arr[0], next);
  if (sync === undefined) {
    sync = false;
  }

  function next(err) {
    if (err) {
      return callback(err);
    }
    if (index >= length) {
      return callback();
    }
    if (sync === undefined) {
      sync = true;
    }
    if (sync) {
      return process.nextTick(function () {
        fn(arr[index++], next);
      });
    }
    fn(arr[index++], next);
  }
}

/**
 * @param {Array} arr
 * @param {Function} fn
 * @param {Function} [callback]
 */
function forEachOf(arr, fn, callback) {
  return mapEach(arr, fn, true, callback);
}

/**
 * @param {Array} arr
 * @param {Function} fn
 * @param {Function} [callback]
 */
function map(arr, fn, callback) {
  return mapEach(arr, fn, false, callback);
}

function mapEach(arr, fn, useIndex, callback) {
  if (!Array.isArray(arr)) {
    throw new TypeError('First parameter must be an Array');
  }
  callback = callback || noop;
  const length = arr.length;
  if (length === 0) {
    return callback(null, []);
  }
  const result = new Array(length);
  let completed = 0;
  const invoke = useIndex ? invokeWithIndex : invokeWithoutIndex;
  for (let i = 0; i < length; i++) {
    invoke(i);
  }

  function invokeWithoutIndex(i) {
    fn(arr[i], function mapItemCallback(err, transformed) {
      result[i] = transformed;
      next(err);
    });
  }

  function invokeWithIndex(i) {
    fn(arr[i], i, function mapItemCallback(err, transformed) {
      result[i] = transformed;
      next(err);
    });
  }

  function next(err) {
    if (err) {
      const cb = callback;
      callback = noop;
      cb(err);
      return;
    }
    if (++completed !== length) {
      return;
    }
    callback(null, result);
  }
}

/**
 * @param {Array} arr
 * @param {Function} fn
 * @param {Function} [callback]
 */
function mapSeries(arr, fn, callback) {
  if (!Array.isArray(arr)) {
    throw new TypeError('First parameter must be an Array');
  }
  callback = callback || noop;
  const length = arr.length;
  if (length === 0) {
    return callback(null, []);
  }
  const result = new Array(length);
  let index = 0;
  let sync;
  invoke(0);
  if (sync === undefined) {
    sync = false;
  }

  function invoke(i) {
    fn(arr[i], function mapItemCallback(err, transformed) {
      result[i] = transformed;
      next(err);
    });
  }

  function next(err) {
    if (err) {
      return callback(err);
    }
    if (++index === length) {
      return callback(null, result);
    }
    if (sync === undefined) {
      sync = true;
    }
    const i = index;
    if (sync) {
      return process.nextTick(function () {
        invoke(i);
      });
    }
    invoke(index);
  }
}

/**
 * @param {Array.<Function>} arr
 * @param {Function} [callback]
 */
function parallel(arr, callback) {
  if (!Array.isArray(arr)) {
    throw new TypeError('First parameter must be an Array');
  }
  callback = callback || noop;
  const length = arr.length;
  let completed = 0;
  for (let i = 0; i < length; i++) {
    arr[i](next);
  }
  function next(err) {
    if (err) {
      const cb = callback;
      callback = noop;
      return cb(err);
    }
    if (++completed !== length) {
      return;
    }
    callback();
  }
}

/**
 * Similar to async.series(), but instead accumulating the result in an Array, it callbacks with the result of the last
 * function in the array.
 * @param {Array.<Function>} arr
 * @param {Function} [callback]
 */
function series(arr, callback) {
  if (!Array.isArray(arr)) {
    throw new TypeError('First parameter must be an Array');
  }
  callback = callback || noop;
  let index = 0;
  let sync;
  next();
  function next(err, result) {
    if (err) {
      return callback(err);
    }
    if (index === arr.length) {
      return callback(null, result);
    }
    if (sync) {
      return process.nextTick(function () {
        sync = true;
        arr[index++](next);
        sync = false;
      });
    }
    sync = true;
    arr[index++](next);
    sync = false;
  }
}

/**
 * @param {Number} count
 * @param {Function} iteratorFunc
 * @param {Function} [callback]
 */
function times(count, iteratorFunc, callback) {
  callback = callback || noop;
  count = +count;
  if (isNaN(count) || count === 0) {
    return callback();
  }
  let completed = 0;
  for (let i = 0; i < count; i++) {
    iteratorFunc(i, next);
  }
  function next(err) {
    if (err) {
      const cb = callback;
      callback = noop;
      return cb(err);
    }
    if (++completed !== count) {
      return;
    }
    callback();
  }
}

/**
 * @param {Number} count
 * @param {Number} limit
 * @param {Function} iteratorFunc
 * @param {Function} [callback]
 */
function timesLimit(count, limit, iteratorFunc, callback) {
  let sync = undefined;
  callback = callback || noop;
  limit = Math.min(limit, count);
  let index = limit - 1;
  let i;
  let completed = 0;
  for (i = 0; i < limit; i++) {
    iteratorFunc(i, next);
  }
  i = -1;
  function next(err) {
    if (err) {
      const cb = callback;
      callback = noop;
      cb(err);
      return;
    }
    if (++completed === count) {
      return callback();
    }
    index++;
    if (index >= count) {
      return;
    }
    if (sync === undefined) {
      sync = (i >= 0);
    }
    if (sync) {
      const captureIndex = index;
      return process.nextTick(function () {
        iteratorFunc(captureIndex, next);
      });
    }
    iteratorFunc(index, next);
  }
}

/**
 * @param {Number} count
 * @param {Function} iteratorFunction
 * @param {Function} callback
 */
function timesSeries(count, iteratorFunction, callback) {
  count = +count;
  if (isNaN(count) || count < 1) {
    return callback();
  }
  let index = 1;
  let sync;
  iteratorFunction(0, next);
  if (sync === undefined) {
    sync = false;
  }
  function next(err) {
    if (err) {
      return callback(err);
    }
    if (index === count) {
      return callback();
    }
    if (sync === undefined) {
      sync = true;
    }
    const i = index++;
    if (sync) {
      //Prevent "Maximum call stack size exceeded"
      return process.nextTick(function () {
        iteratorFunction(i, next);
      });
    }
    //do a sync call as the callback is going to call on a future tick
    iteratorFunction(i, next);
  }
}

/**
 * @param {Function} condition
 * @param {Function} fn
 * @param {Function} callback
 */
function whilst(condition, fn, callback) {
  let sync = 0;
  next();
  function next(err) {
    if (err) {
      return callback(err);
    }
    if (!condition()) {
      return callback();
    }
    if (sync === 0) {
      sync = 1;
      fn(function (err) {
        if (sync === 1) {
          //sync function
          sync = 4;
        }
        next(err);
      });
      if (sync === 1) {
        //async function
        sync = 2;
      }
      return;
    }
    if (sync === 4) {
      //Prevent "Maximum call stack size exceeded"
      return process.nextTick(function () {
        fn(next);
      });
    }
    //do a sync call as the callback is going to call on a future tick
    fn(next);
  }
}

exports.adaptNamedParamsPrepared = adaptNamedParamsPrepared;
exports.adaptNamedParamsWithHints = adaptNamedParamsWithHints;
exports.allocBuffer = allocBuffer;
exports.allocBufferUnsafe = allocBufferUnsafe;
exports.allocBufferFromArray = allocBufferFromArray;
exports.allocBufferFromString = allocBufferFromString;
exports.arrayIterator = arrayIterator;
exports.binarySearch = binarySearch;
exports.copyBuffer = copyBuffer;
exports.deepExtend = deepExtend;
exports.each = each;
exports.eachSeries = eachSeries;
/** @const */
exports.emptyArray = Object.freeze([]);
/** @const */
exports.emptyObject = emptyObject;
exports.extend = extend;
exports.fixStack = fixStack;
exports.forEachOf = forEachOf;
exports.funcCompare = funcCompare;
exports.insertSorted = insertSorted;
exports.iteratorToArray = iteratorToArray;
exports.log = log;
exports.map = map;
exports.mapSeries = mapSeries;
exports.maxInt = maxInt;
exports.noop = noop;
exports.objectValues = objectValues;
exports.parallel = parallel;
exports.promiseWrapper = promiseWrapper;
exports.propCompare = propCompare;
exports.series = series;
exports.stringRepeat = stringRepeat;
exports.shuffleArray = shuffleArray;
exports.times = times;
exports.timesLimit = timesLimit;
exports.timesSeries = timesSeries;
exports.totalLength = totalLength;
exports.validateFn = validateFn;
exports.whilst = whilst;
exports.HashSet = HashSet;


"use strict";

const util = require('util');
const types = require('../types');
const utils = require('../utils.js');
const errors = require('../errors.js');

const doneIteratorObject = Object.freeze({ done: true });

/** @module policies/loadBalancing */
/**
 * Base class for Load Balancing Policies
 * @constructor
 */
function LoadBalancingPolicy() {

}

/**
 * Initializes the load balancing policy, called after the driver obtained the information of the cluster.
 * @param {Client} client
 * @param {HostMap} hosts
 * @param {Function} callback
 */
LoadBalancingPolicy.prototype.init = function (client, hosts, callback) {
  this.client = client;
  this.hosts = hosts;
  callback();
};

/**
 * Returns the distance assigned by this policy to the provided host.
 * @param {Host} host
 */
LoadBalancingPolicy.prototype.getDistance = function (host) {
  return types.distance.local;
};

/**
 * Returns an iterator with the hosts for a new query.
 * Each new query will call this method. The first host in the result will
 * then be used to perform the query.
 * @param {String} keyspace Name of currently logged keyspace at Client level.
 * @param {ExecutionOptions|null} executionOptions The information related to the execution of the request.
 * @param {Function} callback The function to be invoked with the error as first parameter and the host iterator as
 * second parameter.
 */
LoadBalancingPolicy.prototype.newQueryPlan = function (keyspace, executionOptions, callback) {
  callback(new Error('You must implement a query plan for the LoadBalancingPolicy class'));
};

/**
 * Gets an associative array containing the policy options.
 */
LoadBalancingPolicy.prototype.getOptions = function () {
  return new Map();
};

/**
 * This policy yield nodes in a round-robin fashion.
 * @extends LoadBalancingPolicy
 * @constructor
 */
function RoundRobinPolicy() {
  this.index = 0;
}

util.inherits(RoundRobinPolicy, LoadBalancingPolicy);

/**
 * Returns an iterator with the hosts to be used as coordinator for a query.
 * @param {String} keyspace Name of currently logged keyspace at Client level.
 * @param {ExecutionOptions|null} executionOptions The information related to the execution of the request.
 * @param {Function} callback The function to be invoked with the error as first parameter and the host iterator as
 * second parameter.
 */
RoundRobinPolicy.prototype.newQueryPlan = function (keyspace, executionOptions, callback) {
  if (!this.hosts) {
    return callback(new Error('Load balancing policy not initialized'));
  }
  const hosts = this.hosts.values();
  const self = this;
  let counter = 0;

  let planIndex = self.index % hosts.length;
  self.index += 1;
  if (self.index >= utils.maxInt) {
    self.index = 0;
  }

  callback(null, {
    next: function () {
      if (++counter > hosts.length) {
        return doneIteratorObject;
      }
      return {value: hosts[planIndex++ % hosts.length], done: false};
    }
  });
};

/**
 * A data-center aware Round-robin load balancing policy.
 * This policy provides round-robin queries over the nodes of the local
 * data center.
 * @param {?String} [localDc] local datacenter name.  This value overrides the 'localDataCenter' Client option \
 * and is useful for cases where you have multiple execution profiles that you intend on using for routing
 * requests to different data centers.
 * @extends {LoadBalancingPolicy}
 * @constructor
 */
function DCAwareRoundRobinPolicy(localDc) {
  this.localDc = localDc;
  this.index = 0;
  /** @type {Array} */
  this.localHostsArray = null;
}

util.inherits(DCAwareRoundRobinPolicy, LoadBalancingPolicy);

/**
 * Initializes the load balancing policy.
 * @param {Client} client
 * @param {HostMap} hosts
 * @param {Function} callback
 */
DCAwareRoundRobinPolicy.prototype.init = function (client, hosts, callback) {
  this.client = client;
  this.hosts = hosts;
  hosts.on('add', this._cleanHostCache.bind(this));
  hosts.on('remove', this._cleanHostCache.bind(this));

  if (client && client.options) {
    if (this.localDc && !client.options.localDataCenter) {
      this.client.log('info', 'Local data center \'' + this.localDc + '\' was provided as an argument to' +
        ' DCAwareRoundRobinPolicy. It is more preferable to specify the local data center using \'localDataCenter\'' +
        ' in Client options instead when your application is targeting a single data center.');
    }

    // If localDc is unset, use value set in client options.
    this.localDc = this.localDc || client.options.localDataCenter;
  }

  if (!this.localDc) {
    return callback(new errors.ArgumentError('\'localDataCenter\' is not defined in Client options and also was' +
      ' not specified in constructor. At least one is required.'));
  }
  callback();
};

/**
 * Returns the distance depending on the datacenter.
 * @param {Host} host
 */
DCAwareRoundRobinPolicy.prototype.getDistance = function (host) {
  if (!host.datacenter) {
    return types.distance.ignored;
  }
  if (host.datacenter === this.localDc) {
    return types.distance.local;
  }
  return types.distance.remote;
};

DCAwareRoundRobinPolicy.prototype._cleanHostCache = function () {
  this.localHostsArray = null;
};

DCAwareRoundRobinPolicy.prototype._resolveLocalHosts = function() {
  const hosts = this.hosts.values();
  if (this.localHostsArray) {
    //there were already calculated
    return;
  }
  this.localHostsArray = [];
  hosts.forEach(function (h) {
    if (!h.datacenter) {
      //not a remote dc node
      return;
    }
    if (h.datacenter === this.localDc) {
      this.localHostsArray.push(h);
    }
  }, this);
};

/**
 * It returns an iterator that yields local nodes.
 * @param {String} keyspace Name of currently logged keyspace at Client level.
 * @param {ExecutionOptions|null} executionOptions The information related to the execution of the request.
 * @param {Function} callback The function to be invoked with the error as first parameter and the host iterator as
 * second parameter.
 */
DCAwareRoundRobinPolicy.prototype.newQueryPlan = function (keyspace, executionOptions, callback) {
  if (!this.hosts) {
    return callback(new Error('Load balancing policy not initialized'));
  }
  this.index += 1;
  if (this.index >= utils.maxInt) {
    this.index = 0;
  }
  this._resolveLocalHosts();
  // Use a local reference of hosts
  const localHostsArray = this.localHostsArray;
  let planLocalIndex = this.index;
  let counter = 0;
  callback(null, {
    next: function () {
      let host;
      if (counter++ < localHostsArray.length) {
        host = localHostsArray[planLocalIndex++ % localHostsArray.length];
        return { value: host, done: false };
      }
      return doneIteratorObject;
    }
  });
};

/**
 * Gets an associative array containing the policy options.
 */
DCAwareRoundRobinPolicy.prototype.getOptions = function () {
  return new Map([
    ['localDataCenter', this.localDc ]
  ]);
};

/**
 * A wrapper load balancing policy that add token awareness to a child policy.
 * @param {LoadBalancingPolicy} childPolicy
 * @extends LoadBalancingPolicy
 * @constructor
 */
function TokenAwarePolicy (childPolicy) {
  if (!childPolicy) {
    throw new Error("You must specify a child load balancing policy");
  }
  this.childPolicy = childPolicy;
}

util.inherits(TokenAwarePolicy, LoadBalancingPolicy);

TokenAwarePolicy.prototype.init = function (client, hosts, callback) {
  this.client = client;
  this.hosts = hosts;
  this.childPolicy.init(client, hosts, callback);
};

TokenAwarePolicy.prototype.getDistance = function (host) {
  return this.childPolicy.getDistance(host);
};

/**
 * Returns the hosts to use for a new query.
 * The returned plan will return local replicas first, if replicas can be determined, followed by the plan of the
 * child policy.
 * @param {String} keyspace Name of currently logged keyspace at Client level.
 * @param {ExecutionOptions|null} executionOptions The information related to the execution of the request.
 * @param {Function} callback The function to be invoked with the error as first parameter and the host iterator as
 * second parameter.
 */
TokenAwarePolicy.prototype.newQueryPlan = function (keyspace, executionOptions, callback) {
  let routingKey;
  if (executionOptions) {
    routingKey = executionOptions.getRoutingKey();
    if (executionOptions.getKeyspace()) {
      keyspace = executionOptions.getKeyspace();
    }
  }
  let replicas;
  if (routingKey) {
    replicas = this.client.getReplicas(keyspace, routingKey);
  }
  if (!routingKey || !replicas) {
    return this.childPolicy.newQueryPlan(keyspace, executionOptions, callback);
  }
  const iterator = new TokenAwareIterator(keyspace, executionOptions, replicas, this.childPolicy);
  iterator.iterate(callback);
};

/**
 * An iterator that holds the context for the subsequent next() calls
 * @param {String} keyspace
 * @param {ExecutionOptions} execOptions
 * @param {Array} replicas
 * @param childPolicy
 * @constructor
 * @ignore
 */
function TokenAwareIterator(keyspace, execOptions, replicas, childPolicy) {
  this.keyspace = keyspace;
  this.childPolicy = childPolicy;
  this.options = execOptions;
  this.localReplicas = [];
  this.replicaIndex = 0;
  this.replicaMap = {};
  this.childIterator = null;
  // Memoize the local replicas
  // The amount of local replicas should be defined before start iterating, in order to select an
  // appropriate (pseudo random) startIndex
  for (let i = 0; i < replicas.length; i++) {
    const host = replicas[i];
    if (this.childPolicy.getDistance(host) !== types.distance.local) {
      continue;
    }
    this.replicaMap[host.address] = true;
    this.localReplicas.push(host);
  }
  // We use a PRNG to set the replica index
  // We only care about proportional fair scheduling between replicas of a given token
  // Math.random() has an extremely short permutation cycle length but we don't care about collisions
  this.startIndex = Math.floor(Math.random() * this.localReplicas.length);
}

TokenAwareIterator.prototype.iterate = function (callback) {
  //Load the child policy hosts
  const self = this;
  this.childPolicy.newQueryPlan(this.keyspace, this.options, function (err, iterator) {
    if (err) {
      return callback(err);
    }
    //get the iterator of the child policy in case is needed
    self.childIterator = iterator;
    callback(null, {
      next: function () { return self.computeNext(); }
    });
  });
};

TokenAwareIterator.prototype.computeNext = function () {
  let host;
  if (this.replicaIndex < this.localReplicas.length) {
    host = this.localReplicas[(this.startIndex + (this.replicaIndex++)) % this.localReplicas.length];
    return { value: host, done: false };
  }
  // Return hosts from child policy
  let item;
  while ((item = this.childIterator.next()) && !item.done) {
    if (this.replicaMap[item.value.address]) {
      // Avoid yielding local replicas from the child load balancing policy query plan
      continue;
    }
    return item;
  }
  return doneIteratorObject;
};

/**
 * Gets an associative array containing the policy options.
 */
TokenAwarePolicy.prototype.getOptions = function () {
  const map = new Map([
    ['childPolicy', this.childPolicy.constructor !== undefined ? this.childPolicy.constructor.name : null ]
  ]);

  if (this.childPolicy instanceof DCAwareRoundRobinPolicy) {
    map.set('localDataCenter', this.childPolicy.localDc);
  }

  return map;
};

/**
 * Create a new policy that wraps the provided child policy but only "allow" hosts
 * from the provided while list.
 * @class
 * @classdesc
 * A load balancing policy wrapper that ensure that only hosts from a provided
 * white list will ever be returned.
 *

 * This policy wraps another load balancing policy and will delegate the choice
 * of hosts to the wrapped policy with the exception that only hosts contained
 * in the white list provided when constructing this policy will ever be
 * returned. Any host not in the while list will be considered ignored
 * and thus will not be connected to.
 *

 * This policy can be useful to ensure that the driver only connects to a
 * predefined set of hosts. Keep in mind however that this policy defeats
 * somewhat the host auto-detection of the driver. As such, this policy is only
 * useful in a few special cases or for testing, but is not optimal in general.
 * If all you want to do is limiting connections to hosts of the local
 * data-center then you should use DCAwareRoundRobinPolicy and *not* this policy
 * in particular.
 * @param {LoadBalancingPolicy} childPolicy the wrapped policy.
 * @param {Array.<string>}  whiteList the white listed hosts address in the format ipAddress:port.
 * Only hosts from this list may get connected
 * to (whether they will get connected to or not depends on the child policy).
 * @extends LoadBalancingPolicy
 * @constructor
 */
function WhiteListPolicy (childPolicy, whiteList) {
  if (!childPolicy) {
    throw new Error("You must specify a child load balancing policy");
  }
  if (!util.isArray(whiteList)) {
    throw new Error("You must provide the white list of host addresses");
  }
  this.childPolicy = childPolicy;
  const map = {};
  whiteList.forEach(function (address) {
    map[address] = true;
  });
  this.whiteList = map;
}

util.inherits(WhiteListPolicy, LoadBalancingPolicy);

WhiteListPolicy.prototype.init = function (client, hosts, callback) {
  this.childPolicy.init(client, hosts, callback);
};

/**
 * Uses the child policy to return the distance to the host if included in the white list.
 * Any host not in the while list will be considered ignored.
 * @param host
 */
WhiteListPolicy.prototype.getDistance = function (host) {
  if (!this._contains(host)) {
    return types.distance.ignored;
  }
  return this.childPolicy.getDistance(host);
};

/**
 * @param {Host} host
 * @returns {boolean}
 * @private
 */
WhiteListPolicy.prototype._contains = function (host) {
  return !!this.whiteList[host.address];
};

/**
 * Returns the hosts to use for a new query filtered by the white list.
 */
WhiteListPolicy.prototype.newQueryPlan = function (keyspace, info, callback) {
  const self = this;
  this.childPolicy.newQueryPlan(keyspace, info, function (err, iterator) {
    if (err) {
      return callback(err);
    }
    callback(null, self._filter(iterator));
  });
};

WhiteListPolicy.prototype._filter = function (childIterator) {
  const self = this;
  return {
    next: function () {
      const item = childIterator.next();
      if (!item.done && !self._contains(item.value)) {
        return this.next();
      }
      return item;
    }
  };
};

/**
 * Gets an associative array containing the policy options.
 */
WhiteListPolicy.prototype.getOptions = function () {
  return new Map([
    ['childPolicy', this.childPolicy.constructor !== undefined ? this.childPolicy.constructor.name : null ],
    ['whitelist', Object.keys(this.whiteList)]
  ]);
};

exports.DCAwareRoundRobinPolicy = DCAwareRoundRobinPolicy;
exports.LoadBalancingPolicy = LoadBalancingPolicy;
exports.RoundRobinPolicy = RoundRobinPolicy;
exports.TokenAwarePolicy = TokenAwarePolicy;
exports.WhiteListPolicy = WhiteListPolicy;


'use strict';

const util = require('util');
const Long = require('../types').Long;
const errors = require('../errors');

/** @module policies/timestampGeneration */

/**
 * Defines the maximum date in milliseconds that can be represented in microseconds using Number ((2 ^ 53) / 1000)
 * @const
 * @private
 */
const _maxSafeNumberDate = 9007199254740;

/**
 * A long representing the value 1000
 * @const
 * @private
 */
const _longOneThousand = Long.fromInt(1000);

/**
 * Creates a new instance of {@link TimestampGenerator}.
 * @classdesc
 * Generates client-side, microsecond-precision query timestamps.
 *

 *   Given that Cassandra uses those timestamps to resolve conflicts, implementations should generate
 *   monotonically increasing timestamps for successive invocations of {@link TimestampGenerator.next()}.
 *

 * @constructor
 */
function TimestampGenerator() {

}

/**
 * Returns the next timestamp.
 *

 *   Implementors should enforce increasing monotonicity of timestamps, that is,
 *   a timestamp returned should always be strictly greater that any previously returned
 *   timestamp.
 *

 *

 *   Implementors should strive to achieve microsecond precision in the best possible way,
 *   which is usually largely dependent on the underlying operating system's capabilities.
 *

 * @param {Client} client The {@link Client} instance to generate timestamps to.
 * @returns {Long|Number|null} the next timestamp (in microseconds). If it's equals to null, it won't be
 * sent by the driver, letting the server to generate the timestamp.
 * @abstract
 */
TimestampGenerator.prototype.next = function (client) {
  throw new Error('next() must be implemented');
};

/**
 * A timestamp generator that guarantees monotonically increasing timestamps and logs warnings when timestamps
 * drift in the future.
 *

 *   {@link Date} has millisecond precision and client timestamps require microsecond precision. This generator
 *   keeps track of the last generated timestamp, and if the current time is within the same millisecond as the last,
 *   it fills the microsecond portion of the new timestamp with the value of an incrementing counter.
 *

 * @param {Number} [warningThreshold] Determines how far in the future timestamps are allowed to drift before a
 * warning is logged, expressed in milliseconds. Default: 1000.
 * @param {Number} [minLogInterval] In case of multiple log events, it determines the time separation between log
 * events, expressed in milliseconds. Use 0 to disable. Default: 1000.
 * @extends {TimestampGenerator}
 * @constructor
 */
function MonotonicTimestampGenerator(warningThreshold, minLogInterval) {
  if (warningThreshold < 0) {
    throw new errors.ArgumentError('warningThreshold can not be lower than 0');
  }
  this._warningThreshold = warningThreshold || 1000;
  this._minLogInterval = 1000;
  if (typeof minLogInterval === 'number') {
    // A value under 1 will disable logging
    this._minLogInterval = minLogInterval;
  }
  this._micros = -1;
  this._lastDate = 0;
  this._lastLogDate = 0;
}

util.inherits(MonotonicTimestampGenerator, TimestampGenerator);

/**
 * Returns the current time in milliseconds since UNIX epoch
 * @returns {Number}
 */
MonotonicTimestampGenerator.prototype.getDate = function () {
  return Date.now();
};

MonotonicTimestampGenerator.prototype.next = function (client) {
  let date = this.getDate();
  let drifted = 0;
  if (date > this._lastDate) {
    this._micros = 0;
    this._lastDate = date;
    return this._generateMicroseconds();
  }

  if (date < this._lastDate) {
    drifted = this._lastDate - date;
    date = this._lastDate;
  }
  if (++this._micros === 1000) {
    this._micros = 0;
    if (date === this._lastDate) {
      // Move date 1 millisecond into the future
      date++;
      drifted++;
    }
  }
  const lastDate = this._lastDate;
  this._lastDate = date;
  const result = this._generateMicroseconds();
  if (drifted >= this._warningThreshold) {
    // Avoid logging an unbounded amount of times within a clock-skew event or during an interval when more than 1
    // query is being issued by microsecond
    const currentLogDate = Date.now();
    if (this._minLogInterval > 0 && this._lastLogDate + this._minLogInterval <= currentLogDate){
      const message = util.format(
        'Timestamp generated using current date was %d milliseconds behind the last generated timestamp (which ' +
        'millisecond portion was %d), the returned value (%s) is being artificially incremented to guarantee ' +
        'monotonicity.',
        drifted, lastDate, result);
      this._lastLogDate = currentLogDate;
      client.log('warning', message);
    }
  }
  return result;
};

/**
 * @private
 * @returns {Number|Long}
 */
MonotonicTimestampGenerator.prototype._generateMicroseconds = function () {
  if (this._lastDate < _maxSafeNumberDate) {
    // We are safe until Jun 06 2255, its faster to perform this operations on Number than on Long
    // We hope to have native int64 by then :)
    return this._lastDate * 1000 + this._micros;
  }
  return Long
    .fromNumber(this._lastDate)
    .multiply(_longOneThousand)
    .add(Long.fromInt(this._micros));
};

exports.TimestampGenerator = TimestampGenerator;
exports.MonotonicTimestampGenerator = MonotonicTimestampGenerator;


'use strict';
const util = require('util');

/** @module policies/reconnection */
/**
 * Base class for Reconnection Policies
 * @constructor
 */
function ReconnectionPolicy() {

}

/**
 * A new reconnection schedule.
 * @returns {{next: function}} An infinite iterator
 */
ReconnectionPolicy.prototype.newSchedule = function () {
  throw new Error('You must implement a new schedule for the Reconnection class');
};

/**
 * Gets an associative array containing the policy options.
 */
ReconnectionPolicy.prototype.getOptions = function () {
  return new Map();
};

/**
 * A reconnection policy that waits a constant time between each reconnection attempt.
 * @param {Number} delay Delay in ms
 * @constructor
 */
function ConstantReconnectionPolicy(delay) {
  this.delay = delay;
}

util.inherits(ConstantReconnectionPolicy, ReconnectionPolicy);

/**
 * A new reconnection schedule that returns the same next delay value
 * @returns {{next: next}} An infinite iterator
 */
ConstantReconnectionPolicy.prototype.newSchedule = function () {
  const self = this;
  return {
    next: function () {
      return {value: self.delay, done: false};
    }
  };
};

/**
 * Gets an associative array containing the policy options.
 */
ConstantReconnectionPolicy.prototype.getOptions = function () {
  return new Map([['delay', this.delay ]]);
};

/**
 * A reconnection policy that waits exponentially longer between each
 * reconnection attempt (but keeps a constant delay once a maximum delay is reached).
 * <p>
 *   A random amount of jitter (+/- 15%) will be added to the pure exponential delay value to avoid situations
 *   where many clients are in the reconnection process at exactly the same time. The jitter will never cause the
 *   delay to be less than the base delay, or more than the max delay.
 * </p>
 * @param {Number} baseDelay The base delay in milliseconds to use for the schedules created by this policy.
 * @param {Number} maxDelay The maximum delay in milliseconds to wait between two reconnection attempt.
 * @param {Boolean} startWithNoDelay Determines if the first attempt should be zero delay
 * @constructor
 */
function ExponentialReconnectionPolicy(baseDelay, maxDelay, startWithNoDelay) {
  this.baseDelay = baseDelay;
  this.maxDelay = maxDelay;
  this.startWithNoDelay = startWithNoDelay;
}

util.inherits(ExponentialReconnectionPolicy, ReconnectionPolicy);

/**
 * A new schedule that uses an exponentially growing delay between reconnection attempts.
 * @returns {{next: next}} An infinite iterator.
 */
ExponentialReconnectionPolicy.prototype.newSchedule = function* () {
  let index = this.startWithNoDelay ? -1 : 0;

  while (true) {
    let delay = 0;

    if (index >= 64) {
      delay = this.maxDelay;
    } else if (index !== -1) {
      delay = Math.min(Math.pow(2, index) * this.baseDelay, this.maxDelay);
    }

    index++;

    yield this._addJitter(delay);
  }
};

/**
 * Adds a random portion of +-15% to the delay provided.
 * Initially, its adds a random value of 15% to avoid reconnection before reaching the base delay.
 * When the schedule reaches max delay, only subtracts a random portion of 15%.
 */
ExponentialReconnectionPolicy.prototype._addJitter = function (value) {
  if (value === 0) {
    // Instant reconnection without jitter
    return value;
  }

  // Use the formula: 85% + rnd() * 30% to calculate the percentage of the original delay
  let minPercentage = 0.85;
  let range = 0.30;

  if (!this.startWithNoDelay && value === this.baseDelay) {
    // Between 100% to 115% of the original value
    minPercentage = 1;
    range = 0.15;
  } else if (value === this.maxDelay) {
    // Between 85% to 100% of the original value
    range = 0.15;
  }

  return Math.floor(value * (Math.random() * range + minPercentage));
};

/**
 * Gets an associative array containing the policy options.
 */
ExponentialReconnectionPolicy.prototype.getOptions = function () {
  return new Map([
    ['baseDelay', this.baseDelay ],
    ['maxDelay', this.maxDelay ],
    ['startWithNoDelay', this.startWithNoDelay ]
  ]);
};

exports.ReconnectionPolicy = ReconnectionPolicy;
exports.ConstantReconnectionPolicy = ConstantReconnectionPolicy;
exports.ExponentialReconnectionPolicy = ExponentialReconnectionPolicy;


"use strict";
const events = require('events');
const util = require('util');
const net = require('net');

const errors = require('./errors');
const Host = require('./host').Host;
const HostMap = require('./host').HostMap;
const Metadata = require('./metadata');
const EventDebouncer = require('./metadata/event-debouncer');
const Connection = require('./connection');
const requests = require('./requests');
const utils = require('./utils');
const types = require('./types');
const f = util.format;
// eslint-disable-next-line prefer-const
let dns = require('dns');

const selectPeers = "SELECT * FROM system.peers";
const selectLocal = "SELECT * FROM system.local WHERE key='local'";
const newNodeDelay = 1000;
const metadataQueryAbortTimeout = 2000;
const schemaChangeTypes = {
  created: 'CREATED',
  updated: 'UPDATED',
  dropped: 'DROPPED'
};

/**
 * Creates a new instance of ControlConnection.
 * @classdesc
 * Represents a connection used by the driver to receive events and to check the status of the cluster.
 *
It uses an existing connection from the hosts' connection pool to maintain the driver metadata up-to-date.

 * @param {Object} options
 * @param {ProfileManager} profileManager
 * @param {{borrowHostConnection: function, createConnection: function}} [context] An object containing methods to
 * allow dependency injection.
 * @extends EventEmitter
 * @constructor
 */
function ControlConnection(options, profileManager, context) {
  this.protocolVersion = null;
  this.hosts = new HostMap();
  this.setMaxListeners(0);
  Object.defineProperty(this, "options", { value: options, enumerable: false, writable: false});
  /**
   * Cluster metadata that is going to be shared between the Client and ControlConnection
   */
  this.metadata = new Metadata(this.options, this);
  this.addressTranslator = this.options.policies.addressResolution;
  this.reconnectionPolicy = this.options.policies.reconnection;
  this.reconnectionSchedule = this.reconnectionPolicy.newSchedule();
  this.initialized = false;
  this.isShuttingDown = false;
  /**
   * Host used by the control connection
   * @type {Host|null}
   */
  this.host = null;
  /**
   * Connection used to retrieve metadata and subscribed to events
   * @type {Connection|null}
   */
  this.connection = null;
  /**
   * Reference to the encoder of the last valid connection
   * @type {Encoder|null}
   */
  this.encoder = null;
  this.debouncer = new EventDebouncer(options.refreshSchemaDelay, this.log.bind(this));
  this.profileManager = profileManager;
  /** Timeout used for delayed handling of topology changes */
  this.topologyChangeTimeout = null;
  /** Timeout used for delayed handling of node status changes */
  this.nodeStatusChangeTimeout = null;
  this.reconnectionTimeout = null;
  this.hostIterator = null;
  this.triedHosts = null;
  this._resolvedContactPoints = new Map();
  this._contactPoints = new Set();

  if (context && context.borrowHostConnection) {
    this.borrowHostConnection = context.borrowHostConnection;
  }

  if (context && context.createConnection) {
    this.createConnection = context.createConnection;
  }
}

util.inherits(ControlConnection, events.EventEmitter);

/**
 * Stores the contact point information and what it resolved to.
 * @param {String|null} address
 * @param {String} port
 * @param {String} name
 * @param {Boolean} isIPv6
 */
ControlConnection.prototype.addContactPoint = function(address, port, name, isIPv6) {
  if (address === null) {
    // Contact point could not be resolved, store that the resolution came back empty
    this._resolvedContactPoints.set(name, utils.emptyArray);
    return;
  }

  const portNumber = parseInt(port, 10) || this.options.protocolOptions.port;
  const endpoint = `${address}:${portNumber}`;
  this._contactPoints.add(endpoint);

  // Use RFC 3986 for IPv4 and IPv6
  const standardEndpoint = !isIPv6 ? endpoint : `[${address}]:${portNumber}`;

  let resolvedAddressedByName = this._resolvedContactPoints.get(name);
  if (resolvedAddressedByName === undefined) {
    resolvedAddressedByName = [];
    this._resolvedContactPoints.set(name, resolvedAddressedByName);
  }

  resolvedAddressedByName.push(standardEndpoint);
};

ControlConnection.prototype.parseEachContactPoint = function(name, next) {
  let addressOrName = name;
  let port = null;

  if (name.indexOf('[') === 0 && name.indexOf(']:') > 1) {
    // IPv6 host notation [ip]:port (RFC 3986 section 3.2.2)
    const index = name.lastIndexOf(']:');
    addressOrName = name.substr(1, index - 1);
    port = name.substr(index + 2);
  } else if (name.indexOf(':') > 0) {
    // IPv4 or host name with port notation
    const parts = name.split(':');
    if (parts.length === 2) {
      addressOrName = parts[0];
      port = parts[1];
    }
  }

  if (net.isIP(addressOrName)) {
    this.addContactPoint(addressOrName, port, name, net.isIPv6(addressOrName));
    return next();
  }

  resolveAll(addressOrName, (err, addresses) => {
    if (err) {
      this.log('error', `Host with name ${addressOrName} could not be resolved`, err);
      this.addContactPoint(null, null, name, false);
      return next();
    }

    addresses.forEach(addressInfo => this.addContactPoint(addressInfo.address, port, name, addressInfo.isIPv6));

    next();
  });
};

/**
 * Tries to determine a suitable protocol version to be used.
 * Tries to retrieve the hosts in the Cluster.
 * @param {Function} callback
 */
ControlConnection.prototype.init = function (callback) {
  if (this.initialized) {
    // Prevent multiple serial initializations
    return callback();
  }

  const contactPointsResolutionCb = (err) => {
    if (!err && this._contactPoints.size === 0) {
      err = new errors.NoHostAvailableError({}, 'No host could be resolved');
    }

    if (err) {
      return callback(err);
    }

    this.refresh(false, err => {
      this.initialized = !err;
      callback(err);
    });
  };

  utils.each(
    this.options.contactPoints,
    (name, eachNext) => this.parseEachContactPoint(name, eachNext),
    contactPointsResolutionCb);
};

ControlConnection.prototype.setHealthListeners = function (host, connection) {
  const self = this;
  let wasRefreshCalled = 0;

  function removeListeners() {
    host.removeListener('down', downOrIgnoredHandler);
    host.removeListener('ignore', downOrIgnoredHandler);
    connection.removeListener('socketClose', socketClosedHandler);
  }

  function startReconnecting(hostDown) {
    if (wasRefreshCalled++ !== 0) {
      // Prevent multiple calls to reconnect
      return;
    }

    removeListeners();

    if (self.isShuttingDown) {
      // Don't attempt to reconnect when the ControlConnection is being shutdown
      return;
    }

    if (hostDown) {
      self.log('warning',
        `Host ${host.address} used by the ControlConnection DOWN, ` +
        `connection to ${connection.endpointFriendlyName} will not longer by used`);
    } else {
      self.log('warning', `Connection to ${connection.endpointFriendlyName} used by the ControlConnection was closed`);
    }

    self.refresh();
  }

  function downOrIgnoredHandler() {
    startReconnecting(true);
  }

  function socketClosedHandler() {
    startReconnecting(false);
  }

  host.once('down', downOrIgnoredHandler);
  host.once('ignore', downOrIgnoredHandler);
  connection.once('socketClose', socketClosedHandler);
};

/**
 * Iterates through the hostIterator and gets the following open connection.
 * @param callback
 */
ControlConnection.prototype.borrowAConnection = function (callback) {
  const self = this;
  let host;
  let connection = null;

  utils.whilst(
    function condition() {
      // while there isn't a valid connection
      if (connection) {
        return false;
      }
      const item = self.hostIterator.next();
      host = item.value;
      return (!item.done);
    },
    function whileIterator(next) {
      if (self.initialized) {
        // Only check distance once the load-balancing policies have been initialized
        const distance = self.profileManager.getDistance(host);
        if (!host.isUp() || distance === types.distance.ignored) {
          return next();
        }

        self.borrowHostConnection(host, function (err, c) {
          self.triedHosts[host.address] = err;
          connection = c;
          next();
        });
      } else {
        // Host is an endpoint string
        self.createConnection(host, (err, c) => {
          self.triedHosts[host] = err;
          connection = c;
          next();
        });
      }
    },
    function whilstEnded() {
      if (!connection) {
        return callback(new errors.NoHostAvailableError(self.triedHosts));
      }

      if (!self.initialized) {
        self.protocolVersion = connection.protocolVersion;
        self.encoder = connection.encoder;
      }

      self.connection = connection;
      callback();
    });
};

/** Default implementation for borrowing connections, that can be injected at constructor level */
ControlConnection.prototype.borrowHostConnection = function (host, callback) {
  // Borrow any open connection, regardless of the keyspace
  host.borrowConnection(null, null, callback);
};

/**
 * Default implementation for creating initial connections, that can be injected at constructor level
 * @param {String} contactPoint
 * @param {Function} callback
 */
ControlConnection.prototype.createConnection = function (contactPoint, callback) {
  const c = new Connection(contactPoint, null, this.options);
  c.open(err => {
    if (err) {
      setImmediate(() => c.close());
      return callback(err);
    }

    callback(null, c);
  });
};

/**
 * Gets the info from local and peer metadata, reloads the keyspaces metadata and rebuilds tokens.
 * @param {Boolean} initializing Determines whether this function was called in order to initialize the control
 * connection the first time
 * @param {Boolean} setCurrentHost
 * @param {Function} [callback]
 */
ControlConnection.prototype.refreshHosts = function (initializing, setCurrentHost, callback) {
  callback = callback || utils.noop;

  // Get a reference to the current connection as it might change from external events
  const c = this.connection;

  if (!c) {
    // it's possible that this was called as a result of a topology change, but the connection was lost
    // between scheduling time and now. This will be called again when there is a new connection.
    return callback();
  }

  const self = this;
  this.log('info', 'Refreshing local and peers info');

  utils.series([
    function getLocalInfo(next) {
      const request = new requests.QueryRequest(selectLocal, null, null);
      c.sendStream(request, null, function (err, result) {
        self.setLocalInfo(initializing, setCurrentHost, c, result);

        if (!err && !self.host) {
          return next(new errors.DriverInternalError('Information from system.local could not be retrieved'));
        }

        next(err);
      });
    },
    function getPeersInfo(next) {
      const request = new requests.QueryRequest(selectPeers, null, null);
      c.sendStream(request, null, function (err, result) {
        self.setPeersInfo(initializing, err, result, next);
      });
    },
    function resolveAndSetProtocolVersion(next) {
      if (!self.initialized) {
        // resolve protocol version from highest common version among hosts.
        const highestCommon = types.protocolVersion.getHighestCommon(c, self.hosts);
        const reconnect = highestCommon !== self.protocolVersion;

        // set protocol version on each host.
        self.protocolVersion = highestCommon;
        self.hosts.forEach(h => h.setProtocolVersion(self.protocolVersion));

        // if protocol version changed, reconnect the control connection with new version.
        if (reconnect) {
          self.log('info', `Reconnecting since the protocol version changed to 0x${highestCommon.toString(16)}`);
          c.decreaseVersion(self.protocolVersion);
          c.close(() =>
            setImmediate(() => c.open(err => {
              if (err) {
                c.close();
              }

              next(err);
            })));
          return;
        }
      }
      next();
    },
    function getKeyspaces(next) {
      // to acquire metadata we need to specify the cassandra version
      self.metadata.setCassandraVersion(self.host.getCassandraVersion());
      self.metadata.buildTokens(self.hosts);
      if (!self.options.isMetadataSyncEnabled) {
        self.metadata.initialized = true;
        return next();
      }
      self.metadata._refreshKeyspaces(false, true, () => {
        self.metadata.initialized = true;
        next();
      });
    }
  ], callback);
};

/**
 * Acquires a connection and refreshes topology and keyspace metadata.
 *
If it fails obtaining a connection:

 *

 *

 *     When its initializing, it should:
 *

 *
Continue iterating through the hosts

 *
When there aren't any more hosts, it should invoke callback with the inner errors

 *

 *

 *

 *     When its running in the background, it should:
 *

 *
Continue iterating through the hosts

 *

 *         When there aren't any more hosts, it should:
 *

 *
Schedule reconnection

 *
Invoke callback with the inner errors

 *

 *

 *

 *

 *

 *
If it fails obtaining the metadata, it should:

 *

 *
It should mark connection and/or host unusable

 *
Retry using the same iterator from query plan / host list

 *

 * @param {Boolean} [reuseQueryPlan]
 * @param {Function} [callback]
 */
ControlConnection.prototype.refresh = function (reuseQueryPlan, callback) {
  const initializing = !this.initialized;
  callback = callback || utils.noop;

  if (this.isShuttingDown) {
    this.log('info', 'The ControlConnection will not be refreshed as the Client is being shutdown');
    return callback(new errors.NoHostAvailableError({}, 'ControlConnection is shutting down'));
  }

  // Reset host and connection
  this.host = null;
  this.connection = null;

  const self = this;

  utils.series([
    function getHostIterator(next) {
      if (reuseQueryPlan) {
        return next();
      }

      self.triedHosts = {};

      if (initializing) {
        self.log('info', 'Getting first connection');
        const hosts = Array.from(self._contactPoints);
        // Randomize order of contact points resolved.
        utils.shuffleArray(hosts);
        self.hostIterator = hosts[Symbol.iterator]();
        return next();
      }

      self.log('info', 'Trying to acquire a connection to a new host');

      self.profileManager.getDefaultLoadBalancing().newQueryPlan(null, null, function onNewPlan(err, iterator) {
        if (err) {
          self.log('error', 'ControlConnection could not retrieve a query plan to determine which hosts to use', err);
          return next(err);
        }
        self.hostIterator = iterator;
        next();
      });
    },
    function getConnectionTask(next) {
      self.borrowAConnection(next);
    },
    function getLocalAndPeersInfo(next) {
      self.log('info',
        (initializing
          ? `ControlConnection using protocol version 0x${self.protocolVersion.toString(16)},`
          : `ControlConnection`) +
        ` connected to ${self.connection.endpointFriendlyName}`);

      self.refreshHosts(initializing, true, next);
    },
    function subscribeConnectionEvents(next) {
      self.connection.on('nodeTopologyChange', self.nodeTopologyChangeHandler.bind(self));
      self.connection.on('nodeStatusChange', self.nodeStatusChangeHandler.bind(self));
      self.connection.on('nodeSchemaChange', self.nodeSchemaChangeHandler.bind(self));
      const request = new requests.RegisterRequest(['TOPOLOGY_CHANGE', 'STATUS_CHANGE', 'SCHEMA_CHANGE']);
      self.connection.sendStream(request, null, next);
    }
  ], function refreshSeriesEnd(err) {
    // Refresh ended, possible scenarios:
    // - There was a failure obtaining a connection
    // - There was a failure in metadata retrieval
    // - There wasn't a failure but connection is now disconnected at this time
    // - Everything succeeded
    if (!err) {
      if (!self.connection.connected) {
        // Before refreshSeriesEnd() was invoked, the connection changed to a "not connected" state.
        // We have to avoid subscribing to 'down' or 'socketClosed' events after it was down / connection closed.
        // The connection is no longer valid and we should retry the whole thing
        self.log('info', f('Connection to %s was closed before finishing refresh', self.host.address));
        return self.refresh(false, callback);
      }

      if (initializing) {
        // The healthy connection used to initialize should be part of the Host pool
        self.host.pool.addExistingConnection(self.connection);
      }

      self.setHealthListeners(self.host, self.connection);
      self.reconnectionSchedule = self.reconnectionPolicy.newSchedule();
      self.emit('newConnection', null, self.connection, self.host);
      self.log('info', `ControlConnection connected to ${self.connection.endpointFriendlyName} and up to date`);

      return callback();
    }

    if (!self.connection) {
      self.log('error', 'ControlConnection failed to acquire a connection', err);
      if (!initializing && !self.isShuttingDown) {
        self.noOpenConnectionHandler();
        self.emit('newConnection', err);
      }

      return callback(err);
    }

    self.log('error', 'ControlConnection failed to retrieve topology and keyspaces information', err);
    self.triedHosts[self.connection.endpoint] = err;

    if (err && err.isSocketError && !initializing && self.host) {
      self.host.removeFromPool(self.connection);
    }

    // Retry the whole thing with the same query plan, in the background or foreground
    self.refresh(true, callback);
  });
};

/**
 * There isn't an open connection at the moment, try again later.
 */
ControlConnection.prototype.noOpenConnectionHandler = function () {
  const delay = this.reconnectionSchedule.next().value;
  this.log('warning', f('ControlConnection could not reconnect, scheduling reconnection in %dms', delay));
  const self = this;
  setTimeout(() => self.refresh(), delay);
};

/**
 * @param {String} type
 * @param {String} info
 * @param [furtherInfo]
 */
ControlConnection.prototype.log = utils.log;

/**
 * Handles a TOPOLOGY_CHANGE event
 */
ControlConnection.prototype.nodeTopologyChangeHandler = function (event) {
  this.log('info', 'Received topology change', event);
  // all hosts information needs to be refreshed as tokens might have changed
  const self = this;
  clearTimeout(this.topologyChangeTimeout);
  // Use an additional timer to make sure that the refresh hosts is executed only AFTER the delay
  // In this case, the event debouncer doesn't help because it could not honor the sliding delay (ie: processNow)
  this.topologyChangeTimeout = setTimeout(() => self.scheduleRefreshHosts(), newNodeDelay);
};

/**
 * Handles a STATUS_CHANGE event
 */
ControlConnection.prototype.nodeStatusChangeHandler = function (event) {
  const self = this;
  const addressToTranslate = event.inet.address.toString();
  const port = this.options.protocolOptions.port;
  this.addressTranslator.translate(addressToTranslate, port, function translateCallback(endPoint) {
    const host = self.hosts.get(endPoint);
    if (!host) {
      self.log('warning', 'Received status change event but host was not found: ' + addressToTranslate);
      return;
    }
    const distance = self.profileManager.getDistance(host);
    if (event.up) {
      if (distance === types.distance.ignored) {
        return host.setUp(true);
      }
      clearTimeout(self.nodeStatusChangeTimeout);
      // Waits a couple of seconds before marking it as UP
      self.nodeStatusChangeTimeout = setTimeout(() => host.checkIsUp(), newNodeDelay);
      return;
    }
    // marked as down
    if (distance === types.distance.ignored) {
      return host.setDown();
    }
    self.log('warning', 'Received status change to DOWN for host ' + host.address);
  });
};

/**
 * Handles a SCHEMA_CHANGE event
 */
ControlConnection.prototype.nodeSchemaChangeHandler = function (event) {
  this.log('info', 'Schema change', event);
  if (!this.options.isMetadataSyncEnabled) {
    return;
  }
  this.handleSchemaChange(event, false);
};

/**
 * @param {{keyspace: string, isKeyspace: boolean, schemaChangeType, table, udt, functionName, aggregate}} event
 * @param {Boolean} processNow
 * @param {Function} [callback]
 */
ControlConnection.prototype.handleSchemaChange = function (event, processNow, callback) {
  const self = this;
  let handler, cqlObject;
  if (event.isKeyspace) {
    if (event.schemaChangeType === schemaChangeTypes.dropped) {
      handler = function removeKeyspace() {
        // if on the same event queue there is a creation, this handler is not going to be executed
        // it is safe to remove the keyspace metadata
        delete self.metadata.keyspaces[event.keyspace];
      };
      return this.scheduleObjectRefresh(handler, event.keyspace, null, processNow, callback);
    }
    return this.scheduleKeyspaceRefresh(event.keyspace, processNow, callback);
  }
  const ksInfo = this.metadata.keyspaces[event.keyspace];
  if (!ksInfo) {
    // it hasn't been loaded and it is not part of the metadata, don't mind
    return;
  }
  if (event.table) {
    cqlObject = event.table;
    handler = function clearTableState() {
      delete ksInfo.tables[event.table];
      delete ksInfo.views[event.table];
    };
  }
  else if (event.udt) {
    cqlObject = event.udt;
    handler = function clearUdtState() {
      delete ksInfo.udts[event.udt];
    };
  }
  else if (event.functionName) {
    cqlObject = event.functionName;
    handler = function clearFunctionState() {
      delete ksInfo.functions[event.functionName];
    };
  }
  else if (event.aggregate) {
    cqlObject = event.aggregate;
    handler = function clearKeyspaceState() {
      delete ksInfo.aggregates[event.aggregate];
    };
  }
  if (handler) {
    // is a cql object change clean the internal cache
    this.scheduleObjectRefresh(handler, event.keyspace, cqlObject, processNow, callback);
  }
};

/**
 * @param {Function} handler
 * @param {String} keyspaceName
 * @param {String} cqlObject
 * @param {Boolean} processNow
 * @param {Function} [callback]
 */
ControlConnection.prototype.scheduleObjectRefresh = function (handler, keyspaceName, cqlObject, processNow, callback) {
  this.debouncer.eventReceived({ handler, keyspace: keyspaceName, cqlObject: cqlObject, callback }, processNow);
};

/**
 * @param {String} keyspaceName
 * @param {Boolean} processNow
 * @param {Function} [callback]
 */
ControlConnection.prototype.scheduleKeyspaceRefresh = function (keyspaceName, processNow, callback) {
  this.debouncer.eventReceived({
    handler: cb => this.metadata.refreshKeyspace(keyspaceName, cb),
    keyspace: keyspaceName,
    callback
  }, processNow);
};

/**
 * @param {Function} [callback]
 */
ControlConnection.prototype.scheduleRefreshHosts = function (callback) {
  this.debouncer.eventReceived({
    handler: cb => this.refreshHosts(false, false, cb),
    all: true,
    callback
  }, false);
};

/**
 * Sets the information for the host used by the control connection.
 * @param {Boolean} initializing
 * @param {Connection} c
 * @param {Boolean} setCurrentHost Determines if the host retrieved must be set as the current host
 * @param result
 */
ControlConnection.prototype.setLocalInfo = function (initializing, setCurrentHost, c, result) {
  if (!result || !result.rows || !result.rows.length) {
    this.log('warning', 'No local info could be obtained');
    return;
  }

  const row = result.rows[0];

  let localHost;

  const endpoint = c.endpoint;

  if (initializing) {
    localHost = new Host(endpoint, this.protocolVersion, this.options, this.metadata);
    this.hosts.set(endpoint, localHost);
    this.log('info', `Adding host ${endpoint}`);
  } else {
    localHost = this.hosts.get(endpoint);

    if (!localHost) {
      this.log('error', 'Localhost could not be found');
      return;
    }
  }

  localHost.datacenter = row['data_center'];
  localHost.rack = row['rack'];
  localHost.tokens = row['tokens'];
  localHost.hostId = row['host_id'];
  localHost.cassandraVersion = row['release_version'];
  this.metadata.setPartitioner(row['partitioner']);
  this.log('info', 'Local info retrieved');

  if (setCurrentHost) {
    // Set the host as the one being used by the ControlConnection.
    this.host = localHost;
  }
};

/**
 * @param {Boolean} initializing Determines whether this function was called in order to initialize the control
 * connection the first time.
 * @param {Error} err
 * @param {ResultSet} result
 * @param {Function} callback
 */
ControlConnection.prototype.setPeersInfo = function (initializing, err, result, callback) {
  if (!result || !result.rows || err) {
    return callback(err);
  }

  // A map of peers, could useful for in case there are discrepancies
  const peers = {};
  const port = this.options.protocolOptions.port;
  const foundDataCenters = new Set();

  if (this.host && this.host.datacenter) {
    foundDataCenters.add(this.host.datacenter);
  }

  const self = this;

  utils.eachSeries(result.rows, function eachPeer(row, next) {
    self.getAddressForPeerHost(row, port, function getAddressForPeerCallback(endPoint) {
      if (!endPoint) {
        return next();
      }

      peers[endPoint] = true;
      let host = self.hosts.get(endPoint);
      let isNewHost = !host;

      if (isNewHost) {
        host = new Host(endPoint, self.protocolVersion, self.options, self.metadata);
        self.log('info', 'Adding host ' + endPoint);
        isNewHost = true;
      }

      host.datacenter = row['data_center'];
      host.rack = row['rack'];
      host.tokens = row['tokens'];
      host.hostId = row['host_id'];
      host.cassandraVersion = row['release_version'];

      if (host.datacenter) {
        foundDataCenters.add(host.datacenter);
      }

      if (isNewHost) {
        // Add it to the map (and trigger events) after all the properties
        // were set to avoid race conditions
        self.hosts.set(endPoint, host);

        if (!initializing) {
          // Set the distance at Host level, that way the connection pool is created with the correct settings
          self.profileManager.getDistance(host);

          // When we are not initializing, we start with the node set as DOWN
          host.setDown();
        }
      }

      next();
    });
  }, function (err) {
    if (err) {
      return callback(err);
    }
    //Is there a difference in number between peers + local != hosts
    if (self.hosts.length > result.rows.length + 1) {
      //There are hosts in the current state that don't belong (nodes removed or wrong contactPoints)
      self.log('info', 'Removing nodes from the pool');
      const toRemove = [];
      self.hosts.forEach(function (h) {
        //It is not a peer and it is not local host
        if (!peers[h.address] && h !== self.host) {
          self.log('info', 'Removing host ' + h.address);
          toRemove.push(h.address);
          h.shutdown(true);
        }
      });
      self.hosts.removeMultiple(toRemove);
    }
    if (initializing && self.options.localDataCenter) {
      const localDc = self.options.localDataCenter;

      if (!foundDataCenters.has(localDc)) {
        return callback(new errors.ArgumentError('localDataCenter was configured as \'' + localDc + '\', but only found' +
          ' hosts in data centers: [' + Array.from(foundDataCenters).join(', ') + ']'));
      }
    }
    self.log('info', 'Peers info retrieved');
    callback();
  });
};

/**
 * @param {Object|Row} row
 * @param {Number} defaultPort
 * @param {Function} callback The callback to invoke with the string representation of the host endpoint,
 *  containing the ip address and port.
 */
ControlConnection.prototype.getAddressForPeerHost = function (row, defaultPort, callback) {
  let address = row['rpc_address'];
  const peer = row['peer'];
  const bindAllAddress = '0.0.0.0';
  if (!address) {
    this.log('error', f('No rpc_address found for host %s in %s\'s peers system table. %s will be ignored.',
      peer, this.host.address, peer));
    return callback(null);
  }
  if (address.toString() === bindAllAddress) {
    this.log('warning', f('Found host with 0.0.0.0 as rpc_address, using listen_address (%s) to contact it instead.' +
      ' If this is incorrect you should avoid the use of 0.0.0.0 server side.', peer));
    address = peer;
  }
  this.addressTranslator.translate(address.toString(), defaultPort, callback);
};

/**
 * Waits for a connection to be available. If timeout expires before getting a connection it callbacks in error.
 * @param {Function} callback
 */
ControlConnection.prototype.waitForReconnection = function (callback) {
  // eslint-disable-next-line prefer-const
  let timeout;
  const self = this;
  function newConnectionListener(err) {
    clearTimeout(timeout);
    callback(err);
  }
  this.once('newConnection', newConnectionListener);
  timeout = setTimeout(function waitTimeout() {
    self.removeListener('newConnection', newConnectionListener);
    callback(new errors.OperationTimedOutError('A connection could not be acquired before timeout.'));
  }, metadataQueryAbortTimeout);
};

/**
 * Executes a query using the active connection
 * @param {String|Request} cqlQuery
 * @param {Boolean} [waitReconnect] Determines if it should wait for reconnection in case the control connection is not
 * connected at the moment. Default: true.
 * @param {Function} callback
 */
ControlConnection.prototype.query = function (cqlQuery, waitReconnect, callback) {
  if (typeof waitReconnect === 'function') {
    callback = waitReconnect;
    waitReconnect = true;
  }

  const self = this;

  function queryOnConnection() {
    if (!self.connection || self.isShuttingDown) {
      return callback(new errors.NoHostAvailableError({}, 'ControlConnection is not connected at the time'));
    }

    const request = typeof cqlQuery === 'string' ? new requests.QueryRequest(cqlQuery, null, null) : cqlQuery;
    self.connection.sendStream(request, null, callback);
  }

  if (!this.connection && waitReconnect) {
    // Wait until its reconnected (or timer elapses)
    return this.waitForReconnection(function waitCallback(err) {
      if (err) {
        //it was not able to reconnect in time
        return callback(err);
      }
      queryOnConnection();
    });
  }

  queryOnConnection();
};

/** @returns {Encoder} The encoder used by the current connection */
ControlConnection.prototype.getEncoder = function () {
  if (!this.encoder) {
    throw new errors.DriverInternalError('Encoder is not defined');
  }
  return this.encoder;
};

ControlConnection.prototype.shutdown = function () {
  // no need for callback as it all sync
  this.isShuttingDown = true;
  this.debouncer.shutdown();
  // Emit a "newConnection" event with Error, as it may clear timeouts that were waiting new connections
  this.emit('newConnection', new errors.DriverError('ControlConnection is being shutdown'));
  // Cancel timers
  clearTimeout(this.topologyChangeTimeout);
  clearTimeout(this.nodeStatusChangeTimeout);
  clearTimeout(this.reconnectionTimeout);
};

/**
 * Resets the Connection to its initial state.
 */
ControlConnection.prototype.reset = function (callback) {
  // Reset the internal state of the ControlConnection for future initialization attempts
  const currentHosts = this.hosts.clear();
  // Set the shutting down flag temporarily to avoid reconnects.
  this.isShuttingDown = true;
  const self = this;
  // Ignore any shutdown error
  utils.each(currentHosts, (h, next) => h.shutdown(false, () => next()), function shuttingDownFinished() {
    self.initialized = false;
    self.isShuttingDown = false;
    callback();
  });
};

/**
 * Gets a Map containing the original contact points and the addresses that each one resolved to.
 */
ControlConnection.prototype.getResolvedContactPoints = function () {
  return this._resolvedContactPoints;
};

/**
 * Gets the local IP address to which the control connection socket is bound to.
 * @returns {String|undefined}
 */
ControlConnection.prototype.getLocalAddress = function () {
  if (!this.connection) {
    return undefined;
  }

  return this.connection.getLocalAddress();
};

/**
 * Gets the address and port of host the control connection is connected to.
 * @returns {String|undefined}
 */
ControlConnection.prototype.getEndpoint = function () {
  if (!this.connection) {
    return undefined;
  }

  return this.connection.endpoint;
};

/**
 * Uses the DNS protocol to resolve a IPv4 and IPv6 addresses (A and AAAA records) for the hostname
 * @private
 * @param name
 * @param callback
 */
function resolveAll(name, callback) {
  const addresses = [];
  utils.parallel([
    function resolve4(next) {
      dns.resolve4(name, function resolve4Callback(err, arr) {
        if (arr) {
          arr.forEach(address => addresses.push({ address, isIPv6: false }));
        }
        // Ignore error
        next();
      });
    },
    function resolve6(next) {
      dns.resolve6(name, function resolve6Callback(err, arr) {
        if (arr) {
          arr.forEach(address => addresses.push({ address, isIPv6: true }));
        }
        // Ignore error
        next();
      });
    }
  ], function resolveAllCallback() {
    if (addresses.length === 0) {
      // In case dns.resolve*() methods don't yield a valid address for the host name
      // Use system call getaddrinfo() that might resolve according to host system definitions
      return dns.lookup(name, function (err, address, family) {
        if (err) {
          return callback(err);
        }

        addresses.push({ address, isIPv6: family === 6 });
        callback(null, addresses);
      });
    }
    callback(null, addresses);
  });
}

module.exports = ControlConnection;


'use strict';

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

/**
 * Creates a new instance of ClientState.
 * @classdesc
 * Represents the state of a {@link Client}.
 *

 * Exposes information on the connections maintained by a Client at a specific time.
 *

 * @alias module:metadata~ClientState
 * @param {Array<Host>} hosts
 * @param {Object.<String, Number>} openConnections
 * @param {Object.<String, Number>} inFlightQueries
 * @constructor
 */
function ClientState(hosts, openConnections, inFlightQueries) {
  this._hosts = hosts;
  this._openConnections = openConnections;
  this._inFlightQueries = inFlightQueries;
}

/**
 * Creates a new instance from the provided client.
 * @param {Client} client
 * @internal
 * @ignore
 */
ClientState.from = function (client) {
  const openConnections = {};
  const inFlightQueries = {};
  const hostArray = [];
  client.hosts.forEach(function each(host) {
    if (host.pool.connections.length === 0) {
      return;
    }

    hostArray.push(host);
    openConnections[host.address] = host.pool.connections.length;
    inFlightQueries[host.address] = host.getInFlight();
  });
  return new ClientState(hostArray, openConnections, inFlightQueries);
};

/**
 * Get an array of hosts to which the client is connected to.
 * @return {Array<Host>}
 */
ClientState.prototype.getConnectedHosts = function () {
  return this._hosts;
};

/**
 * Gets the amount of open connections to a given host.
 * @param {Host} host
 * @return {Number}
 */
ClientState.prototype.getOpenConnections = function (host) {
  if (!host) {
    throw new errors.ArgumentError('Host is not defined');
  }
  return this._openConnections[host.address] || 0;
};

/**
 * Gets the amount of queries that are currently being executed through a given host.
 *

 * This corresponds to the number of queries that have been sent by the Client to server Host on one of its connections
 * but haven't yet obtained a response.
 *

 * @param {Host} host
 * @return {Number}
 */
ClientState.prototype.getInFlightQueries = function (host) {
  if (!host) {
    throw new errors.ArgumentError('Host is not defined');
  }
  return this._inFlightQueries[host.address] || 0;
};

/**
 * Returns the string representation of the instance.
 */
ClientState.prototype.toString = function () {
  return util.format(
    '{"hosts": %j, "openConnections": %j, "inFlightQueries": %j}',
    this._hosts.map(function (h) { return h.address; }),
    this._openConnections,
    this._inFlightQueries
  );
};

module.exports = ClientState;


"use strict";

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

const _queueOverflowThreshold = 1000;

/**
 * Debounce protocol events by acting on those events with a sliding delay.
 * @param {Number} delay
 * @param {Function} logger
 * @ignore
 * @constructor
 */
function EventDebouncer(delay, logger) {
  this._delay = delay;
  this._logger = logger;
  this._queue = null;
  this._timeout = null;
}

/**
 * Adds a new event to the queue and moves the delay.
 * @param {{ handler: Function, all: boolean|undefined, keyspace: String|undefined, cqlObject: String|null|undefined,
 * callback: Function|undefined }} event
 * @param {Boolean} processNow
 */
EventDebouncer.prototype.eventReceived = function (event, processNow) {
  event.callback = event.callback || utils.noop;
  this._queue = this._queue || { callbacks: [], keyspaces: {} };
  const delay = !processNow ? this._delay : 0;
  if (event.all) {
    // when an event marked with all is received, it supersedes all the rest of events
    // a full update (hosts + keyspaces + tokens) is going to be made
    this._queue.mainEvent = event;
  }
  if (this._queue.callbacks.length === _queueOverflowThreshold) {
    // warn once
    this._logger('warn', util.format('Event debouncer queue exceeded %d events', _queueOverflowThreshold));
  }
  this._queue.callbacks.push(event.callback);
  if (this._queue.mainEvent) {
    // a full refresh is scheduled and the callback was added, nothing else to do.
    return this._slideDelay(delay);
  }
  // Insert at keyspace level
  let keyspaceEvents = this._queue.keyspaces[event.keyspace];
  if (!keyspaceEvents) {
    keyspaceEvents = this._queue.keyspaces[event.keyspace] = { events: [] };
  }
  if (event.cqlObject === undefined) {
    // a full refresh of the keyspace, supersedes all child keyspace events
    keyspaceEvents.mainEvent = event;
  }
  keyspaceEvents.events.push(event);
  this._slideDelay(delay);
};

/**
 * @param {Number} delay
 * @private
 * */
EventDebouncer.prototype._slideDelay = function (delay) {
  const self = this;
  function process() {
    const q = self._queue;
    self._queue = null;
    self._timeout = null;
    processQueue(q);
  }
  if (delay === 0) {
    // no delay, process immediately
    if (this._timeout) {
      clearTimeout(this._timeout);
    }
    return process();
  }
  const previousTimeout = this._timeout;
  // add the new timeout before removing the previous one performs better
  this._timeout = setTimeout(process, delay);
  if (previousTimeout) {
    clearTimeout(previousTimeout);
  }
};

/**
 * Clears the timeout and invokes all pending callback.
 */
EventDebouncer.prototype.shutdown = function () {
  if (!this._queue) {
    return;
  }
  this._queue.callbacks.forEach(function (cb) {
    cb();
  });
  this._queue = null;
  clearTimeout(this._timeout);
  this._timeout = null;
};

/**
 * @param {{callbacks: Array, keyspaces: Object, mainEvent: Object}} q
 * @private
 */
function processQueue (q) {
  if (q.mainEvent) {
    // refresh all by invoking 1 handler and invoke all pending callbacks
    return q.mainEvent.handler(function invokeCallbacks(err) {
      for (let i = 0; i < q.callbacks.length; i++) {
        q.callbacks[i](err);
      }
    });
  }
  utils.each(Object.keys(q.keyspaces), function eachKeyspace(name, next) {
    const keyspaceEvents = q.keyspaces[name];
    if (keyspaceEvents.mainEvent) {
      // refresh a keyspace
      return keyspaceEvents.mainEvent.handler(function mainEventCallback(err) {
        for (let i = 0; i < keyspaceEvents.events.length; i++) {
          keyspaceEvents.events[i].callback(err);
        }
        next();
      });
    }
    // deal with individual handlers and callbacks
    keyspaceEvents.events.forEach(function eachEvent(event) {
      // sync handlers
      event.handler();
      event.callback();
    });
    next();
  });
}

module.exports = EventDebouncer;