'use strict';

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

/**
 * Information on the formatting of the returned rows
 */
const resultFlag = {
  globalTablesSpec:   0x0001,
  hasMorePages:       0x0002,
  noMetadata:         0x0004
};

/**
 * Buffer forward reader of CQL binary frames
 * @param {FrameHeader} header
 * @param {Buffer} body
 * @param {Number} [offset]
 */
function FrameReader(header, body, offset) {
  this.header = header;
  this.opcode = header.opcode;
  this.offset = offset || 0;
  this.buf = body;
}

FrameReader.prototype.remainingLength = function () {
  return this.buf.length - this.offset;
};

FrameReader.prototype.getBuffer = function () {
  return this.buf;
};

/**
 * Slices the underlining buffer
 * @param {Number} begin
 * @param {Number} [end]
 * @returns {Buffer}
 */
FrameReader.prototype.slice = function (begin, end) {
  if (typeof end === 'undefined') {
    end = this.buf.length;
  }
  return this.buf.slice(begin, end);
};

/**
 * Modifies the underlying buffer, it concatenates the given buffer with the original (internalBuffer = concat(bytes, internalBuffer)
 */
FrameReader.prototype.unshift = function (bytes) {
  if (this.offset > 0) {
    throw new Error('Can not modify the underlying buffer if already read');
  }
  this.buf = Buffer.concat([bytes, this.buf], bytes.length + this.buf.length);
};

/**
 * Reads any number of bytes and moves the offset.
 * if length not provided or it's larger than the remaining bytes, reads to end.
 * @param length
 * @returns {Buffer}
 */
FrameReader.prototype.read = function (length) {
  let end = this.buf.length;
  if (typeof length !== 'undefined' && this.offset + length < this.buf.length) {
    end = this.offset + length;
  }
  const bytes = this.slice(this.offset, end);
  this.offset = end;
  return bytes;
};

/**
 * Moves the reader cursor to the end
 */
FrameReader.prototype.toEnd = function () {
  this.offset = this.buf.length;
};

/**
 * Reads a BE Int and moves the offset
 * @returns {Number}
 */
FrameReader.prototype.readInt = function() {
  const result = this.buf.readInt32BE(this.offset);
  this.offset += 4;
  return result;
};

/** @returns {Number} */
FrameReader.prototype.readShort = function () {
  const result = this.buf.readUInt16BE(this.offset);
  this.offset += 2;
  return result;
};

FrameReader.prototype.readByte = function () {
  const result = this.buf.readUInt8(this.offset);
  this.offset += 1;
  return result;
};

FrameReader.prototype.readString = function () {
  const length = this.readShort();
  this.checkOffset(length);
  const result = this.buf.toString('utf8', this.offset, this.offset+length);
  this.offset += length;
  return result;
};

/**
 * Checks that the new length to read is within the range of the buffer length. Throws a RangeError if not.
 * @param {Number} newLength
 */
FrameReader.prototype.checkOffset = function (newLength) {
  if (this.offset + newLength > this.buf.length) {
    const err = new RangeError('Trying to access beyond buffer length');
    err.expectedLength = newLength;
    throw err;
  }
};

/**
 * Reads a protocol string list
 * @returns {Array}
 */
FrameReader.prototype.readStringList = function () {
  const length = this.readShort();
  const list = new Array(length);
  for (let i = 0; i < length; i++) {
    list[i] = this.readString();
  }
  return list;
};

/**
 * Reads the amount of bytes that the field has and returns them (slicing them).
 * @returns {Buffer}
 */
FrameReader.prototype.readBytes = function () {
  const length = this.readInt();
  if (length < 0) {
    return null;
  }
  this.checkOffset(length);
  return this.read(length);
};

FrameReader.prototype.readShortBytes = function () {
  const length = this.readShort();
  if (length < 0) {
    return null;
  }
  this.checkOffset(length);
  return this.read(length);
};

/**
 * Reads an associative array of strings as keys and bytes as values
 * @returns {Object}
 */
FrameReader.prototype.readBytesMap = function () {
  //A [short] n, followed by n pair <k><v> where <k> is a
  //[string] and <v> is a [bytes].
  const length = this.readShort();
  if (length < 0) {
    return null;
  }
  const map = {};
  for (let i = 0; i < length; i++) {
    map[this.readString()] = this.readBytes();
  }
  return map;
};

/**
 * Reads a data type definition
 * @returns {{code: Number, info: Object|null}} An array of 2 elements
 */
FrameReader.prototype.readType = function () {
  let i;
  const type = {
    code: this.readShort(),
    type: null
  };
  switch (type.code) {
    case types.dataTypes.custom:
      type.info = this.readString();
      break;
    case types.dataTypes.list:
    case types.dataTypes.set:
      type.info = this.readType();
      break;
    case types.dataTypes.map:
      type.info = [this.readType(), this.readType()];
      break;
    case types.dataTypes.udt:
      type.info = {
        keyspace: this.readString(),
        name: this.readString(),
        fields: new Array(this.readShort())
      };
      for (i = 0; i < type.info.fields.length; i++) {
        type.info.fields[i] = {
          name: this.readString(),
          type: this.readType()
        };
      }
      break;
    case types.dataTypes.tuple:
      type.info = new Array(this.readShort());
      for (i = 0; i < type.info.length; i++) {
        type.info[i] = this.readType();
      }
      break;
  }
  return type;
};

/**
 * Reads an Ip address and port
 * @returns {{address: exports.InetAddress, port: Number}}
 */
FrameReader.prototype.readInet = function () {
  const length = this.readByte();
  const address = this.read(length);
  return {address: new types.InetAddress(address), port: this.readInt()};
};

/**
 * Reads the body bytes corresponding to the flags
 * @returns {{traceId: Uuid, warnings: Array, customPayload}}
 * @throws {RangeError}
 */
FrameReader.prototype.readFlagsInfo = function () {
  if (this.header.flags === 0) {
    return utils.emptyObject;
  }
  const result = {};
  if (this.header.flags & types.frameFlags.tracing) {
    this.checkOffset(16);
    result.traceId = new types.Uuid(utils.copyBuffer(this.read(16)));
  }
  if (this.header.flags & types.frameFlags.warning) {
    result.warnings = this.readStringList();
  }
  if (this.header.flags & types.frameFlags.customPayload) {
    result.customPayload = this.readBytesMap();
  }
  return result;
};

/**
 * Reads the metadata from a row or a prepared result response
 * @param {Number} kind
 * @returns {Object}
 * @throws {RangeError}
 */
FrameReader.prototype.readMetadata = function (kind) {
  let i;
  //Determines if its a prepared metadata
  const isPrepared = (kind === types.resultKind.prepared);
  const meta = {};
  //as used in Rows and Prepared responses
  const flags = this.readInt();

  const columnLength = this.readInt();
  if (types.protocolVersion.supportsPreparedPartitionKey(this.header.version) && isPrepared) {
    //read the pk columns
    meta.partitionKeys = new Array(this.readInt());
    for (i = 0; i < meta.partitionKeys.length; i++) {
      meta.partitionKeys[i] = this.readShort();
    }
  }
  if (flags & resultFlag.hasMorePages) {
    meta.pageState = utils.copyBuffer(this.readBytes());
  }
  if (flags & resultFlag.globalTablesSpec) {
    meta.global_tables_spec = true;
    meta.keyspace = this.readString();
    meta.table = this.readString();
  }
  meta.columns = new Array(columnLength);
  meta.columnsByName = utils.emptyObject;
  if (isPrepared) {
    //for prepared metadata, we will need a index of the columns (param) by name
    meta.columnsByName = {};
  }
  for (i = 0; i < columnLength; i++) {
    const col = {};
    if(!meta.global_tables_spec) {
      col.ksname = this.readString();
      col.tablename = this.readString();
    }
    col.name = this.readString();
    col.type = this.readType();
    meta.columns[i] = col;
    if (isPrepared) {
      meta.columnsByName[col.name] = i;
    }
  }

  return meta;
};

// templates for derived error messages.
const _writeTimeoutQueryMessage = 'Server timeout during write query at consistency %s (%d peer(s) acknowledged the write over %d required)';
const _writeTimeoutBatchLogMessage = 'Server timeout during batchlog write at consistency %s (%d peer(s) acknowledged the write over %d required)';
const _writeFailureMessage = 'Server failure during write query at consistency %s (%d responses were required but only %d replicas responded, %d failed)';
const _unavailableMessage = 'Not enough replicas available for query at consistency %s (%d required but only %d alive)';
const _readTimeoutMessage = 'Server timeout during read query at consistency %s (%s)';
const _readFailureMessage = 'Server failure during read query at consistency %s (%d responses were required but only %d replicas responded, %d failed)';

/**
 * Reads the error from the frame
 * @throws {RangeError}
 * @returns {ResponseError}
 */
FrameReader.prototype.readError = function () {
  const code = this.readInt();
  const message = this.readString();
  const err = new errors.ResponseError(code, message);
  //read extra info
  switch (code) {
    case types.responseErrorCodes.unavailableException:
      err.consistencies = this.readShort();
      err.required = this.readInt();
      err.alive = this.readInt();
      err.message = util.format(_unavailableMessage, types.consistencyToString[err.consistencies], err.required, err.alive);
      break;
    case types.responseErrorCodes.readTimeout:
    case types.responseErrorCodes.readFailure:
      err.consistencies = this.readShort();
      err.received = this.readInt();
      err.blockFor = this.readInt();
      if (code === types.responseErrorCodes.readFailure) {
        err.failures = this.readInt();
      }
      err.isDataPresent = this.readByte();
      if (code === types.responseErrorCodes.readTimeout) {
        let details;
        if (err.received < err.blockFor) {
          details = util.format('%d replica(s) responded over %d required', err.received, err.blockFor);
        } else if (!err.isDataPresent) {
          details = 'the replica queried for the data didn\'t respond';
        } else {
          details = 'timeout while waiting for repair of inconsistent replica';
        }
        err.message = util.format(_readTimeoutMessage, types.consistencyToString[err.consistencies], details);
      } else {
        err.message = util.format(_readFailureMessage, types.consistencyToString[err.consistencies],
          err.blockFor, err.received, err.failures);
      }
      break;
    case types.responseErrorCodes.writeTimeout:
    case types.responseErrorCodes.writeFailure:
      err.consistencies = this.readShort();
      err.received = this.readInt();
      err.blockFor = this.readInt();
      if (code === types.responseErrorCodes.writeFailure) {
        err.failures = this.readInt();
      }
      err.writeType = this.readString();

      if (code === types.responseErrorCodes.writeTimeout) {
        const template = err.writeType === 'BATCH_LOG' ? _writeTimeoutBatchLogMessage : _writeTimeoutQueryMessage;
        err.message = util.format(template, types.consistencyToString[err.consistencies], err.received, err.blockFor);
      } else {
        err.message = util.format(_writeFailureMessage, types.consistencyToString[err.consistencies],
          err.blockFor, err.received, err.failures);
      }
      break;
    case types.responseErrorCodes.unprepared:
      err.queryId = utils.copyBuffer(this.readShortBytes());
      break;
    case types.responseErrorCodes.functionFailure:
      err.keyspace = this.readString();
      err.functionName = this.readString();
      err.argTypes = this.readStringList();
      break;
    case types.responseErrorCodes.alreadyExists: {
      err.keyspace = this.readString();
      const table = this.readString();
      if(table.length > 0) {
        err.table = table;
      }
      break;
    }
  }
  return err;
};

/**
 * Reads an event from Cassandra and returns the detail
 * @returns {{eventType: String, inet: {address: Buffer, port: Number}}, *}
 */
FrameReader.prototype.readEvent = function () {
  const eventType = this.readString();
  switch (eventType) {
    case types.protocolEvents.topologyChange:
      return {
        added: this.readString() === 'NEW_NODE',
        inet: this.readInet(),
        eventType: eventType};
    case types.protocolEvents.statusChange:
      return {
        up: this.readString() === 'UP',
        inet: this.readInet(),
        eventType: eventType};
    case types.protocolEvents.schemaChange:
      return this.parseSchemaChange();
  }
  //Forward compatibility
  return { eventType: eventType};
};

FrameReader.prototype.parseSchemaChange = function () {
  let result;
  if (!types.protocolVersion.supportsSchemaChangeFullMetadata(this.header.version)) {
    //v1/v2: 3 strings, the table value can be empty
    result = {
      eventType: types.protocolEvents.schemaChange,
      schemaChangeType: this.readString(),
      keyspace: this.readString(),
      table: this.readString()
    };
    result.isKeyspace = !result.table;
    return result;
  }
  //v3+: 3 or 4 strings: change_type, target, keyspace and (table, type, functionName or aggregate)
  result = {
    eventType: types.protocolEvents.schemaChange,
    schemaChangeType: this.readString(),
    target: this.readString(),
    keyspace: this.readString(),
    table: null,
    udt: null,
    signature: null
  };
  result.isKeyspace = result.target === 'KEYSPACE';
  switch (result.target) {
    case 'TABLE':
      result.table = this.readString();
      break;
    case 'TYPE':
      result.udt = this.readString();
      break;
    case 'FUNCTION':
      result.functionName = this.readString();
      result.signature = this.readStringList();
      break;
    case 'AGGREGATE':
      result.aggregate = this.readString();
      result.signature = this.readStringList();
  }
  return result;
};

exports.FrameReader = FrameReader;


"use strict";

const events = require('events');
const util = require('util');
const tls = require('tls');

const Encoder = require('./encoder.js');
const WriteQueue = require('./writers').WriteQueue;
const requests = require('./requests');
const streams = require('./streams');
const utils = require('./utils');
const types = require('./types');
const errors = require('./errors');
const StreamIdStack = require('./stream-id-stack');
const OperationState = require('./operation-state');
const ExecutionOptions = require('./execution-options').ExecutionOptions;

// Allow injection of net module
// eslint-disable-next-line prefer-const
let net = require('net');

/**
 * Represents a connection to a Cassandra node
 * @param {String} endpoint An string containing ip address and port of the host
 * @param {Number|null} protocolVersion
 * @param {ClientOptions} options
 * @extends EventEmitter
 * @constructor
 */
function Connection(endpoint, protocolVersion, options) {
  events.EventEmitter.call(this);
  this.setMaxListeners(0);

  if (!options) {
    throw new Error('options is not defined');
  }

  /**
   * Gets the ip and port of the server endpoint.
   * @type {String}
   */
  this.endpoint = endpoint;

  /**
   * Gets the friendly name of the host, used to identify the connection in log messages.
   * With direct connect, this is the address and port.
   * @type {String}
   */
  this.endpointFriendlyName = endpoint;

  if (!this.endpoint || this.endpoint.indexOf(':') < 0) {
    throw new Error('EndPoint must contain the ip address and port separated by : symbol');
  }

  const portSeparatorIndex = this.endpoint.lastIndexOf(':');
  this.address = this.endpoint.substr(0, portSeparatorIndex);
  this.port = this.endpoint.substr(portSeparatorIndex + 1);

  Object.defineProperty(this, "options", { value: options, enumerable: false, writable: false});

  if (protocolVersion === null) {
    // Set initial protocol version
    protocolVersion = types.protocolVersion.maxSupported;
    if (options.protocolOptions.maxVersion) {
      // User provided the protocol version
      protocolVersion = options.protocolOptions.maxVersion;
    }
    // Allow to check version using this connection instance
    this._checkingVersion = true;
  }

  this.protocolVersion = protocolVersion;
  /** @type {Object.<String, OperationState>} */
  this._operations = {};
  this._pendingWrites = [];
  this._preparing = {};

  /**
   * The timeout state for the idle request (heartbeat)
   */
  this._idleTimeout = null;
  this.timedOutOperations = 0;
  this._streamIds = new StreamIdStack(this.protocolVersion);
  this._metrics = options.metrics;

  this.encoder = new Encoder(protocolVersion, options);
  this.keyspace = null;
  this.emitDrain = false;
  /**
   * Determines if the socket is open and startup succeeded, whether the connection can be used to send requests /
   * receive events
   */
  this.connected = false;
  /**
   * Determines if the socket can be considered as open
   */
  this.isSocketOpen = false;
}

util.inherits(Connection, events.EventEmitter);

Connection.prototype.log = utils.log;

/**
 * Binds the necessary event listeners for the socket
 */
Connection.prototype.bindSocketListeners = function() {
  //Remove listeners that were used for connecting
  this.netClient.removeAllListeners('connect');
  this.netClient.removeAllListeners('timeout');
  // The socket is expected to be open at this point
  this.isSocketOpen = true;
  this.netClient.on('close', () => {
    this.log('info', `Connection to ${this.endpointFriendlyName} closed`);
    this.isSocketOpen = false;
    const wasConnected = this.connected;
    this.close();
    if (wasConnected) {
      // Emit only when it was closed unexpectedly
      this.emit('socketClose');
    }
  });

  this.protocol = new streams.Protocol({ objectMode: true });
  this.parser = new streams.Parser({ objectMode: true }, this.encoder);
  const resultEmitter = new streams.ResultEmitter({objectMode: true});
  resultEmitter.on('result', this.handleResult.bind(this));
  resultEmitter.on('row', this.handleRow.bind(this));
  resultEmitter.on('frameEnded', this.freeStreamId.bind(this));
  resultEmitter.on('nodeEvent', this.handleNodeEvent.bind(this));

  this.netClient
    .pipe(this.protocol)
    .pipe(this.parser)
    .pipe(resultEmitter);

  this.writeQueue = new WriteQueue(this.netClient, this.encoder, this.options);
};

/**
 * Connects a socket and sends the startup protocol messages.
 * Note that when open() callbacks in error, the caller should immediately call {@link Connection#close}.
 */
Connection.prototype.open = function (callback) {
  const self = this;
  this.log('info', `Connecting to ${this.endpointFriendlyName}`);

  if (!this.options.sslOptions) {
    this.netClient = new net.Socket({ highWaterMark: this.options.socketOptions.coalescingThreshold });
    this.netClient.connect(this.port, this.address, function connectCallback() {
      self.log('verbose', `Socket connected to ${self.endpointFriendlyName}`);
      self.bindSocketListeners();
      self.startup(callback);
    });
  } else {
    // Use TLS
    const sslOptions = utils.extend({ rejectUnauthorized: false }, this.options.sslOptions);

    this.netClient = tls.connect(this.port, this.address, sslOptions, function tlsConnectCallback() {
      self.log('verbose', `Secure socket connected to ${self.endpointFriendlyName}`);
      self.bindSocketListeners();
      self.startup(callback);
    });

    // TLSSocket will validate for values from 512 to 16K (depending on the SSL protocol version)
    this.netClient.setMaxSendFragment(this.options.socketOptions.coalescingThreshold);
  }

  this.netClient.once('error', function socketError(err) {
    self.errorConnecting(err, false, callback);
  });

  this.netClient.once('timeout', function connectTimedOut() {
    const err = new types.DriverError('Connection timeout');
    self.errorConnecting(err, true, callback);
  });

  this.netClient.setTimeout(this.options.socketOptions.connectTimeout);

  // Improve failure detection with TCP keep-alives
  if (this.options.socketOptions.keepAlive) {
    this.netClient.setKeepAlive(true, this.options.socketOptions.keepAliveDelay);
  }

  this.netClient.setNoDelay(!!this.options.socketOptions.tcpNoDelay);
};

/**
 * Determines the protocol version to use and sends the STARTUP request
 * @param {Function} callback
 */
Connection.prototype.startup = function (callback) {
  if (this._checkingVersion) {
    this.log('info', 'Trying to use protocol version 0x' + this.protocolVersion.toString(16));
  }

  const self = this;
  const request = new requests.StartupRequest({ noCompact: this.options.protocolOptions.noCompact });

  this.sendStream(request, null, function responseCallback(err, response) {
    if (err && self._checkingVersion) {
      let invalidProtocol = (err instanceof errors.ResponseError &&
        err.code === types.responseErrorCodes.protocolError &&
        err.message.indexOf('Invalid or unsupported protocol version') >= 0);
      if (!invalidProtocol && types.protocolVersion.canStartupResponseErrorBeWrapped(self.protocolVersion)) {
        //For some versions of Cassandra, the error is wrapped into a server error
        //See CASSANDRA-9451
        invalidProtocol = (err instanceof errors.ResponseError &&
          err.code === types.responseErrorCodes.serverError &&
          err.message.indexOf('ProtocolException: Invalid or unsupported protocol version') > 0);
      }
      if (invalidProtocol) {
        // The server can respond with a message using the lower protocol version supported
        // or using the same version as the one provided
        let lowerVersion = self.protocol.version;
        if (lowerVersion === self.protocolVersion) {
          lowerVersion = types.protocolVersion.getLowerSupported(self.protocolVersion);
        }
        if (!lowerVersion) {
          return startupCallback(
            new Error('Connection was unable to STARTUP using protocol version ' + self.protocolVersion));
        }
        self.log('info', 'Protocol 0x' + self.protocolVersion.toString(16) + ' not supported, using 0x' + lowerVersion.toString(16));
        self.decreaseVersion(lowerVersion);
        // The host closed the connection, close the socket and start the connection flow again
        setImmediate(function decreasingVersionClosing() {
          self.close(function decreasingVersionOpening() {
            // Attempt to open with the correct protocol version
            self.open(callback);
          });
        });
        return;
      }
    }
    if (response && response.mustAuthenticate) {
      return self.startAuthenticating(response.authenticatorName, startupCallback);
    }
    startupCallback(err);
  });

  function startupCallback(err) {
    if (err) {
      return self.errorConnecting(err, false, callback);
    }
    //The socket is connected and the connection is authenticated
    return self.connectionReady(callback);
  }
};

Connection.prototype.errorConnecting = function (err, destroy, callback) {
  this.log('warning', `There was an error when trying to connect to the host ${this.endpointFriendlyName}`, err);
  if (destroy) {
    //there is a TCP connection that should be killed.
    this.netClient.destroy();
  }

  this._metrics.onConnectionError(err);

  callback(err);
};

/**
 * Sets the connection to ready/connected status
 */
Connection.prototype.connectionReady = function (callback) {
  this.emit('connected');
  this.connected = true;
  // Remove existing error handlers as the connection is now ready.
  this.netClient.removeAllListeners('error');
  this.netClient.on('error', this.handleSocketError.bind(this));
  callback();
};

/** @param {Number} lowerVersion */
Connection.prototype.decreaseVersion = function (lowerVersion) {
  // The response already has the max protocol version supported by the Cassandra host.
  this.protocolVersion = lowerVersion;
  this.encoder.setProtocolVersion(lowerVersion);
  this._streamIds.setVersion(lowerVersion);
};

/**
 * Handle socket errors, if the socket is not readable invoke all pending callbacks
 */
Connection.prototype.handleSocketError = function (err) {
  this._metrics.onConnectionError(err);
  this.clearAndInvokePending(err);
};

/**
 * Cleans all internal state and invokes all pending callbacks of sent streams
 */
Connection.prototype.clearAndInvokePending = function (innerError) {
  if (this._idleTimeout) {
    //Remove the idle request
    clearTimeout(this._idleTimeout);
    this._idleTimeout = null;
  }
  this._streamIds.clear();
  if (this.emitDrain) {
    this.emit('drain');
  }
  const err = new types.DriverError('Socket was closed');
  err.isSocketError = true;
  if (innerError) {
    err.innerError = innerError;
  }

  //copy all handlers
  const operations = utils.objectValues(this._operations);

  //remove it from the map
  this._operations = {};
  if (operations.length > 0) {
    this.log('info', 'Invoking ' + operations.length + ' pending callbacks');
  }

  // Invoke all handlers
  utils.each(operations, function (operation, next) {
    operation.setResult(err);
    next();
  });

  const pendingWritesCopy = this._pendingWrites;
  this._pendingWrites = [];
  utils.each(pendingWritesCopy, function (operation, next) {
    operation.setResult(err);
    next();
  });
};

/**
 * Starts the SASL flow
 * @param {String} authenticatorName
 * @param {Function} callback
 */
Connection.prototype.startAuthenticating = function (authenticatorName, callback) {
  if (!this.options.authProvider) {
    return callback(new errors.AuthenticationError('Authentication provider not set'));
  }
  const authenticator = this.options.authProvider.newAuthenticator(this.endpoint, authenticatorName);
  const self = this;
  authenticator.initialResponse(function initialResponseCallback(err, token) {
    // Start the flow with the initial token
    if (err) {
      return self.onAuthenticationError(callback, err);
    }
    self.authenticate(authenticator, token, callback);
  });
};

/**
 * Handles authentication requests and responses.
 * @param {Authenticator} authenticator
 * @param {Buffer} token
 * @param {Function} callback
 */
Connection.prototype.authenticate = function(authenticator, token, callback) {
  const self = this;
  let request = new requests.AuthResponseRequest(token);
  if (this.protocolVersion === 1) {
    //No Sasl support, use CREDENTIALS
    if (!authenticator.username) {
      return self.onAuthenticationError(
        callback, new errors.AuthenticationError('Only plain text authenticator providers allowed under protocol v1'));
    }

    request = new requests.CredentialsRequest(authenticator.username, authenticator.password);
  }

  this.sendStream(request, null, function authResponseCallback(err, result) {
    if (err) {
      if (err instanceof errors.ResponseError && err.code === types.responseErrorCodes.badCredentials) {
        const authError = new errors.AuthenticationError(err.message);
        authError.additionalInfo = err;
        err = authError;
      }
      return self.onAuthenticationError(callback, err);
    }

    if (result.ready) {
      authenticator.onAuthenticationSuccess();
      return callback();
    }

    if (result.authChallenge) {
      return authenticator.evaluateChallenge(result.token, function evaluateCallback(err, t) {
        if (err) {
          return self.onAuthenticationError(callback, err);
        }
        //here we go again
        self.authenticate(authenticator, t, callback);
      });
    }

    callback(new errors.DriverInternalError('Unexpected response from Cassandra: ' + util.inspect(result)));
  });
};

Connection.prototype.onAuthenticationError = function(callback, err) {
  this._metrics.onAuthenticationError(err);
  callback(err);
};

/**
 * Executes a 'USE ' query, if keyspace is provided and it is different from the current keyspace
 * @param {?String} keyspace
 * @param {Function} callback
 */
Connection.prototype.changeKeyspace = function (keyspace, callback) {
  if (!keyspace || this.keyspace === keyspace) {
    return callback();
  }
  this.once('keyspaceChanged', callback);
  if (this.toBeKeyspace === keyspace) {
    // It will be invoked once the keyspace is changed
    return;
  }
  this.toBeKeyspace = keyspace;
  const query = util.format('USE "%s"', keyspace);
  const self = this;
  this.sendStream(
    new requests.QueryRequest(query, null, null),
    null,
    function changeKeyspaceResponseCallback(err) {
      if (err) {
        self.log('error', `Connection to ${self.endpointFriendlyName} could not switch active keyspace`, err);
      }
      else {
        self.keyspace = keyspace;
      }
      self.toBeKeyspace = null;
      self.emit('keyspaceChanged', err, keyspace);
    });
};

/**
 * Prepares a query on a given connection. If its already being prepared, it queues the callback.
 * @param {String} query
 * @param {function} callback
 */
Connection.prototype.prepareOnce = function (query, callback) {
  const name = ( this.keyspace || '' ) + query;
  let info = this._preparing[name];
  if (this._preparing[name]) {
    //Its being already prepared
    return info.once('prepared', callback);
  }
  info = new events.EventEmitter();
  info.setMaxListeners(0);
  info.once('prepared', callback);
  this._preparing[name] = info;
  const self = this;
  this.sendStream(new requests.PrepareRequest(query), null, function (err, response) {
    info.emit('prepared', err, response);
    delete self._preparing[name];
  });
};

/**
 * Queues the operation to be written to the wire and invokes the callback once the response was obtained or with an
 * error (socket error or OperationTimedOutError or serialization-related error).
 * @param {Request} request
 * @param {ExecutionOptions|null} execOptions
 * @param {function} callback Function to be called once the response has been received
 * @return {OperationState}
 */
Connection.prototype.sendStream = function (request, execOptions, callback) {
  execOptions = execOptions || ExecutionOptions.empty();

  // Create a new operation that will contain the request, callback and timeouts
  const operation = new OperationState(request, execOptions.getRowCallback(), (err, response, length) => {
    if (!err || !err.isSocketError) {
      // Emit that a response was obtained when there is a valid response
      // or when the error is not a socket error
      this.emit('responseDequeued');
    }
    callback(err, response, length);
  });

  const streamId = this._getStreamId();

  // Start the request timeout without waiting for the request to be written
  operation.setRequestTimeout(execOptions, this.options.socketOptions.readTimeout, this.endpoint,
    () => this.timedOutOperations++,
    () => this.timedOutOperations--);

  if (streamId === null) {
    this.log('info',
      'Enqueuing ' +
      this._pendingWrites.length +
      ', if this message is recurrent consider configuring more connections per host or lowering the pressure');
    this._pendingWrites.push(operation);
    return operation;
  }
  this._write(operation, streamId);
  return operation;
};

/**
 * Pushes the item into the queue.
 * @param {OperationState} operation
 * @param {Number} streamId
 * @private
 */
Connection.prototype._write = function (operation, streamId) {
  operation.streamId = streamId;
  const self = this;
  this.writeQueue.push(operation, function writeCallback (err) {
    if (err) {
      // The request was not written.
      // There was a serialization error or the operation has already timed out or was cancelled
      self._streamIds.push(streamId);
      return operation.setResult(err);
    }
    self.log('verbose', 'Sent stream #' + streamId + ' to ' + self.endpointFriendlyName);
    if (operation.isByRow()) {
      self.parser.setOptions(streamId, { byRow: true });
    }
    self._setIdleTimeout();
    self._operations[streamId] = operation;
  });
};

Connection.prototype._setIdleTimeout = function () {
  if (!this.options.pooling.heartBeatInterval) {
    return;
  }
  const self = this;
  // Scheduling the new timeout before de-scheduling the previous performs significantly better
  // than de-scheduling first, see nodejs implementation: https://github.com/nodejs/node/blob/master/lib/timers.js
  const previousTimeout = this._idleTimeout;
  self._idleTimeout = setTimeout(() => self._idleTimeoutHandler(), self.options.pooling.heartBeatInterval);
  if (previousTimeout) {
    //remove the previous timeout for the idle request
    clearTimeout(previousTimeout);
  }
};

/**
 * Function that gets executed once the idle timeout has passed to issue a request to keep the connection alive
 */
Connection.prototype._idleTimeoutHandler = function () {
  if (this.sendingIdleQuery) {
    //don't issue another
    //schedule for next time
    this._idleTimeout = setTimeout(() => this._idleTimeoutHandler(), this.options.pooling.heartBeatInterval);
    return;
  }

  this.log('verbose', `Connection to ${this.endpointFriendlyName} idling, issuing a request to prevent disconnects`);
  this.sendingIdleQuery = true;
  this.sendStream(requests.options, null, (err) => {
    this.sendingIdleQuery = false;
    if (!err) {
      //The sending succeeded
      //There is a valid response but we don't care about the response
      return;
    }
    this.log('warning', 'Received heartbeat request error', err);
    this.emit('idleRequestError', err, this);
  });
};

/**
 * Returns an available streamId or null if there isn't any available
 * @returns {Number}
 */
Connection.prototype._getStreamId = function() {
  return this._streamIds.pop();
};

Connection.prototype.freeStreamId = function(header) {
  const streamId = header.streamId;
  if (streamId < 0) {
    return;
  }
  delete this._operations[streamId];
  this._streamIds.push(streamId);
  if (this.emitDrain && this._streamIds.inUse === 0 && this._pendingWrites.length === 0) {
    this.emit('drain');
  }
  this._writeNext();
};

Connection.prototype._writeNext = function () {
  if (this._pendingWrites.length === 0) {
    return;
  }
  const streamId = this._getStreamId();
  if (streamId === null) {
    // No streamId available
    return;
  }
  const self = this;
  let operation;
  while ((operation = this._pendingWrites.shift()) && !operation.canBeWritten()) {
    // Trying to obtain an pending operation that can be written
  }

  if (!operation) {
    // There isn't a pending operation that can be written
    this._streamIds.push(streamId);
    return;
  }

  // Schedule after current I/O callbacks have been executed
  setImmediate(function writeNextPending() {
    self._write(operation, streamId);
  });
};

/**
 * Returns the number of requests waiting for response
 * @returns {Number}
 */
Connection.prototype.getInFlight = function () {
  return this._streamIds.inUse;
};

/**
 * Handles a result and error response
 */
Connection.prototype.handleResult = function (header, err, result) {
  const streamId = header.streamId;
  if(streamId < 0) {
    return this.log('verbose', 'event received', header);
  }
  const operation = this._operations[streamId];
  if (!operation) {
    return this.log('error', 'The server replied with a wrong streamId #' + streamId);
  }
  this.log('verbose', 'Received frame #' + streamId + ' from ' + this.endpointFriendlyName);
  operation.setResult(err, result, header.bodyLength);
};

Connection.prototype.handleNodeEvent = function (header, event) {
  switch (event.eventType) {
    case types.protocolEvents.schemaChange:
      this.emit('nodeSchemaChange', event);
      break;
    case types.protocolEvents.topologyChange:
      this.emit('nodeTopologyChange', event);
      break;
    case types.protocolEvents.statusChange:
      this.emit('nodeStatusChange', event);
      break;
  }
};

/**
 * Handles a row response
 */
Connection.prototype.handleRow = function (header, row, meta, rowLength, flags) {
  const streamId = header.streamId;
  if(streamId < 0) {
    return this.log('verbose', 'Event received', header);
  }
  const operation = this._operations[streamId];
  if (!operation) {
    return this.log('error', 'The server replied with a wrong streamId #' + streamId);
  }
  operation.setResultRow(row, meta, rowLength, flags, header);
};

/**
 * Closes the socket (if not already closed) and cancels all in-flight requests.
 * Multiple calls to this method have no additional side-effects.
 * @param {Function} [callback]
 */
Connection.prototype.close = function (callback) {
  callback = callback || utils.noop;

  if (!this.connected && !this.isSocketOpen) {
    return callback();
  }

  this.connected = false;
  // Drain is never going to be emitted, once it is set to closed
  this.removeAllListeners('drain');
  this.clearAndInvokePending();

  if (!this.isSocketOpen) {
    return callback();
  }

  // Set the socket as closed now (before socket.end() is called) to avoid being invoked more than once
  this.isSocketOpen = false;
  this.log('verbose', `Closing connection to ${this.endpointFriendlyName}`);
  const self = this;

  // If server doesn't acknowledge the half-close within connection timeout, destroy the socket.
  const endTimeout = setTimeout(() => {
    this.log('info', `${this.endpointFriendlyName} did not respond to connection close within ` +
      `${this.options.socketOptions.connectTimeout}ms, destroying connection`);
    this.netClient.destroy();
  }, this.options.socketOptions.connectTimeout);

  this.netClient.once('close', function (hadError) {
    clearTimeout(endTimeout);
    if (hadError) {
      self.log('info', 'The socket closed with a transmission error');
    }
    setImmediate(callback);
  });

  // At this point, the error event can be triggered because:
  // - It's connected and writes haven't completed yet
  // - The server abruptly closed its end of the connection (ECONNRESET) as a result of protocol error / auth error
  // We need to remove any listeners and make sure we callback are pending writes
  this.netClient.removeAllListeners('error');
  this.netClient.on('error', err => this.clearAndInvokePending(err));

  // Half-close the socket, it will result in 'close' event being fired
  this.netClient.end();
};

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

  return this.netClient.localAddress;
};

module.exports = Connection;


'use strict';
const util = require('util');
const crypto = require('crypto');
const Long = require('long');

const Uuid = require('./uuid');
const utils = require('../utils');

/** @module types */
/**
 * Oct 15, 1582 in milliseconds since unix epoch
 * @const
 * @private
 */
const _unixToGregorian = 12219292800000;
/**
 * 10,000 ticks in a millisecond
 * @const
 * @private
 */
const _ticksInMs = 10000;

const minNodeId = utils.allocBufferFromString('808080808080', 'hex');
const minClockId = utils.allocBufferFromString('8080', 'hex');
const maxNodeId = utils.allocBufferFromString('7f7f7f7f7f7f', 'hex');
const maxClockId = utils.allocBufferFromString('7f7f', 'hex');

/**
 * Counter used to generate up to 10000 different timeuuid values with the same Date
 * @private
 * @type {number}
 */
let _ticks = 0;
/**
 * Counter used to generate ticks for the current time
 * @private
 * @type {number}
 */
let _ticksForCurrentTime = 0;
/**
 * Remember the last time when a ticks for the current time so that it can be reset
 * @private
 * @type {number}
 */
let _lastTimestamp = 0;

/**
 * Creates a new instance of Uuid based on the parameters provided according to rfc4122.
 * If any of the arguments is not provided, it will be randomly generated, except for the date that will use the current
 * date.
 *

 *   Note that when nodeId and/or clockId portions are not provided, the constructor will generate them using
 *   crypto.randomBytes(). As it's possible that crypto.randomBytes() might block, it's
 *   recommended that you use the callback-based version of the static methods fromDate() or
 *   now() in that case.
 *

 * @class
 * @classdesc Represents an immutable version 1 universally unique identifier (UUID). A UUID represents a 128-bit value.
 *
Usage: TimeUuid.now()

 * @extends module:types~Uuid
 * @param {Date} [value] The datetime for the instance, if not provided, it will use the current Date.
 * @param {Number} [ticks] A number from 0 to 10000 representing the 100-nanoseconds units for this instance to fill in the information not available in the Date,
 * as Ecmascript Dates have only milliseconds precision.
 * @param {String|Buffer} [nodeId] A 6-length Buffer or string of 6 ascii characters representing the node identifier, ie: 'host01'.
 * @param {String|Buffer} [clockId] A 2-length Buffer or string of 6 ascii characters representing the clock identifier.
 * @constructor
 */
function TimeUuid(value, ticks, nodeId, clockId) {
  let buffer;
  if (value instanceof Buffer) {
    if (value.length !== 16) {
      throw new Error('Buffer for v1 uuid not valid');
    }
    buffer = value;
  }
  else {
    buffer = generateBuffer(value, ticks, nodeId, clockId);
  }
  Uuid.call(this, buffer);
}

util.inherits(TimeUuid, Uuid);

/**
 * Generates a TimeUuid instance based on the Date provided using random node and clock values.
 * @param {Date} date Date to generate the v1 uuid.
 * @param {Number} [ticks] A number from 0 to 10000 representing the 100-nanoseconds units for this instance to fill in the information not available in the Date,
 * as Ecmascript Dates have only milliseconds precision.
 * @param {String|Buffer} [nodeId] A 6-length Buffer or string of 6 ascii characters representing the node identifier, ie: 'host01'.
 * If not provided, a random nodeId will be generated.
 * @param {String|Buffer} [clockId] A 2-length Buffer or string of 6 ascii characters representing the clock identifier.
 * If not provided a random clockId will be generated.
 * @param {Function} [callback] An optional callback to be invoked with the error as first parameter and the created
 * TimeUuid as second parameter. When a callback is provided, the random portions of the
 * TimeUuid instance are created asynchronously.
 *

 *   When nodeId and/or clockId portions are not provided, this method will generate them using
 *   crypto.randomBytes(). As it's possible that crypto.randomBytes() might block, it's
 *   recommended that you use the callback-based version of this method in that case.
 *

 * @example
Generate a TimeUuid from a ECMAScript Date

 * const timeuuid = TimeUuid.fromDate(new Date());
 * @example
Generate a TimeUuid from a Date with ticks portion

 * const timeuuid = TimeUuid.fromDate(new Date(), 1203);
 * @example
Generate a TimeUuid from a Date without any random portion

 * const timeuuid = TimeUuid.fromDate(new Date(), 1203, 'host01', '02');
 * @example
Generate a TimeUuid from a Date with random node and clock identifiers

 * TimeUuid.fromDate(new Date(), 1203, function (err, timeuuid) {
 *   // do something with the generated timeuuid
 * });
 */
TimeUuid.fromDate = function (date, ticks, nodeId, clockId, callback) {
  if (typeof ticks === 'function') {
    callback = ticks;
    ticks = nodeId = clockId = null;
  } else if (typeof nodeId === 'function') {
    callback = nodeId;
    nodeId = clockId = null;
  } else if (typeof clockId === 'function') {
    callback = clockId;
    clockId = null;
  }

  if (!callback) {
    return new TimeUuid(date, ticks, nodeId, clockId);
  }

  utils.parallel([
    next => getOrGenerateRandom(nodeId, 6, (err, buffer) => next(err, nodeId = buffer)),
    next => getOrGenerateRandom(clockId, 2, (err, buffer) => next(err, clockId = buffer)),
  ], (err) => {
    if (err) {
      return callback(err);
    }

    let timeUuid;
    try {
      timeUuid = new TimeUuid(date, ticks, nodeId, clockId);
    }
    catch (e) {
      return callback(e);
    }

    callback(null, timeUuid);
  });
};

/**
 * Parses a string representation of a TimeUuid
 * @param {String} value
 * @returns {TimeUuid}
 */
TimeUuid.fromString = function (value) {
  return new TimeUuid(Uuid.fromString(value).getBuffer());
};

/**
 * Returns the smaller possible type 1 uuid with the provided Date.
 */
TimeUuid.min = function (date, ticks) {
  return new TimeUuid(date, ticks, minNodeId, minClockId);
};

/**
 * Returns the biggest possible type 1 uuid with the provided Date.
 */
TimeUuid.max = function (date, ticks) {
  return new TimeUuid(date, ticks, maxNodeId, maxClockId);
};

/**
 * Generates a TimeUuid instance based on the current date using random node and clock values.
 * @param {String|Buffer} [nodeId] A 6-length Buffer or string of 6 ascii characters representing the node identifier, ie: 'host01'.
 * If not provided, a random nodeId will be generated.
 * @param {String|Buffer} [clockId] A 2-length Buffer or string of 6 ascii characters representing the clock identifier.
 * If not provided a random clockId will be generated.
 * @param {Function} [callback] An optional callback to be invoked with the error as first parameter and the created
 * TimeUuid as second parameter. When a callback is provided, the random portions of the
 * TimeUuid instance are created asynchronously.
 *

 *   When nodeId and/or clockId portions are not provided, this method will generate them using
 *   crypto.randomBytes(). As it's possible that crypto.randomBytes() might block, it's
 *   recommended that you use the callback-based version of this method in that case.
 *

 * @example
Generate a TimeUuid from a Date without any random portion

 * const timeuuid = TimeUuid.now('host01', '02');
 * @example
Generate a TimeUuid with random node and clock identifiers

 * TimeUuid.now(function (err, timeuuid) {
 *   // do something with the generated timeuuid
 * });
 * @example
Generate a TimeUuid based on the current date (might block)

 * const timeuuid = TimeUuid.now();
 */
TimeUuid.now = function (nodeId, clockId, callback) {
  return TimeUuid.fromDate(null, null, nodeId, clockId, callback);
};

/**
 * Gets the Date and 100-nanoseconds units representation of this instance.
 * @returns {{date: Date, ticks: Number}}
 */
TimeUuid.prototype.getDatePrecision = function () {
  const timeLow = this.buffer.readUInt32BE(0);

  let timeHigh = 0;
  timeHigh |= ( this.buffer[4] & 0xff ) << 8;
  timeHigh |= this.buffer[5] & 0xff;
  timeHigh |= ( this.buffer[6] & 0x0f ) << 24;
  timeHigh |= ( this.buffer[7] & 0xff ) << 16;

  const val = Long.fromBits(timeLow, timeHigh);
  const ticksInMsLong = Long.fromNumber(_ticksInMs);
  const ticks = val.modulo(ticksInMsLong);
  const time = val
    .div(ticksInMsLong)
    .subtract(Long.fromNumber(_unixToGregorian));
  return { date: new Date(time.toNumber()), ticks: ticks.toNumber()};
};

/**
 * Gets the Date representation of this instance.
 * @returns {Date}
 */
TimeUuid.prototype.getDate = function () {
  return this.getDatePrecision().date;
};

/**
 * Returns the node id this instance
 * @returns {Buffer}
 */
TimeUuid.prototype.getNodeId = function () {
  return this.buffer.slice(10);
};

/**
 * Returns the clock id this instance, with the variant applied (first 2 msb being 1 and 0).
 * @returns {Buffer}
 */
TimeUuid.prototype.getClockId = function () {
  return this.buffer.slice(8, 10);
};

/**
 * Returns the node id this instance as an ascii string
 * @returns {String}
 */
TimeUuid.prototype.getNodeIdString = function () {
  return this.buffer.slice(10).toString('ascii');
};

function writeTime(buffer, time, ticks) {
  //value time expressed in ticks precision
  const val = Long
    .fromNumber(time + _unixToGregorian)
    .multiply(Long.fromNumber(10000))
    .add(Long.fromNumber(ticks));
  const timeHigh = val.getHighBitsUnsigned();
  buffer.writeUInt32BE(val.getLowBitsUnsigned(), 0);
  buffer.writeUInt16BE(timeHigh & 0xffff, 4);
  buffer.writeUInt16BE(timeHigh >>> 16 & 0xffff, 6);
}

/**
 * Returns a buffer of length 2 representing the clock identifier
 * @param {String|Buffer} clockId
 * @returns {Buffer}
 * @private
 */
function getClockId(clockId) {
  let buffer = clockId;
  if (typeof clockId === 'string') {
    buffer = utils.allocBufferFromString(clockId, 'ascii');
  }
  if (!(buffer instanceof Buffer)) {
    //Generate
    buffer = getRandomBytes(2);
  }
  else if (buffer.length !== 2) {
    throw new Error('Clock identifier must have 2 bytes');
  }
  return buffer;
}

/**
 * Returns a buffer of length 6 representing the clock identifier
 * @param {String|Buffer} nodeId
 * @returns {Buffer}
 * @private
 */
function getNodeId(nodeId) {
  let buffer = nodeId;
  if (typeof nodeId === 'string') {
    buffer = utils.allocBufferFromString(nodeId, 'ascii');
  }
  if (!(buffer instanceof Buffer)) {
    //Generate
    buffer = getRandomBytes(6);
  }
  else if (buffer.length !== 6) {
    throw new Error('Node identifier must have 6 bytes');
  }
  return buffer;
}

/**
 * Returns the ticks portion of a timestamp.  If the ticks are not provided an internal counter is used that gets reset at 10000.
 * @private
 * @param {Number} [ticks]
 * @returns {Number}
 */
function getTicks(ticks) {
  if (typeof ticks !== 'number'|| ticks >= _ticksInMs) {
    _ticks++;
    if (_ticks >= _ticksInMs) {
      _ticks = 0;
    }
    ticks = _ticks;
  }
  return ticks;
}

/**
 * Returns an object with the time representation of the date expressed in milliseconds since unix epoch
 * and a ticks property for the 100-nanoseconds precision.
 * @private
 * @returns {{time: Number, ticks: Number}}
 */
function getTimeWithTicks(date, ticks) {
  if (!(date instanceof Date) || isNaN(date.getTime())) {
    // time with ticks for the current time
    date = new Date();
    const time = date.getTime();
    _ticksForCurrentTime++;
    if(_ticksForCurrentTime > _ticksInMs || time > _lastTimestamp) {
      _ticksForCurrentTime = 0;
      _lastTimestamp = time;
    }
    ticks = _ticksForCurrentTime;
  }
  return {
    time: date.getTime(),
    ticks: getTicks(ticks)
  };
}

function getRandomBytes(length) {
  return crypto.randomBytes(length);
}

function getOrGenerateRandom(id, length, callback) {
  if (id) {
    return callback(null, id);
  }
  crypto.randomBytes(length, callback);
}

/**
 * Generates a 16-length Buffer instance
 * @private
 * @param {Date} date
 * @param {Number} ticks
 * @param {String|Buffer} nodeId
 * @param {String|Buffer} clockId
 * @returns {Buffer}
 */
function generateBuffer(date, ticks, nodeId, clockId) {
  const timeWithTicks = getTimeWithTicks(date, ticks);
  nodeId = getNodeId(nodeId);
  clockId = getClockId(clockId);
  const buffer = utils.allocBufferUnsafe(16);
  //Positions 0-7 Timestamp
  writeTime(buffer, timeWithTicks.time, timeWithTicks.ticks);
  //Position 8-9 Clock
  clockId.copy(buffer, 8, 0);
  //Positions 10-15 Node
  nodeId.copy(buffer, 10, 0);
  //Version Byte: Time based
  //0001xxxx
  //turn off first 4 bits
  buffer[6] = buffer[6] & 0x0f;
  //turn on fifth bit
  buffer[6] = buffer[6] | 0x10;

  //IETF Variant Byte: 1.0.x
  //10xxxxxx
  //turn off first 2 bits
  buffer[8] = buffer[8] & 0x3f;
  //turn on first bit
  buffer[8] = buffer[8] | 0x80;
  return buffer;
}

module.exports = TimeUuid;