"use strict";

var inherits = require('util').inherits
  , EventEmitter = require('events').EventEmitter
  , Connection = require('./connection')
  , Query = require('./commands').Query
  , Logger = require('./logger')
  , f = require('util').format;

var DISCONNECTED = 'disconnected';
var CONNECTING = 'connecting';
var CONNECTED = 'connected';
var DESTROYED = 'destroyed';

var _id = 0;

/**
 * Creates a new Pool instance
 * @class
 * @param {string} options.host The server host
 * @param {number} options.port The server port
 * @param {number} [options.size=1] Max server connection pool size
 * @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
 * @param {number} [options.keepAliveInitialDelay=0] Initial delay before TCP keep alive enabled
 * @param {boolean} [options.noDelay=true] TCP Connection no delay
 * @param {number} [options.connectionTimeout=0] TCP Connection timeout setting
 * @param {number} [options.socketTimeout=0] TCP Socket timeout setting
 * @param {boolean} [options.singleBufferSerializtion=true] Serialize into single buffer, trade of peak memory for serialization speed
 * @param {boolean} [options.ssl=false] Use SSL for connection
 * @param {boolean|function} [options.checkServerIdentity=true] Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. You can pass in a boolean or your own checkServerIdentity override function.
 * @param {Buffer} [options.ca] SSL Certificate store binary buffer
 * @param {Buffer} [options.cert] SSL Certificate binary buffer
 * @param {Buffer} [options.key] SSL Key file binary buffer
 * @param {string} [options.passPhrase] SSL Certificate pass phrase
 * @param {boolean} [options.rejectUnauthorized=false] Reject unauthorized server certificates
 * @param {boolean} [options.promoteLongs=true] Convert Long values from the db into Numbers if they fit into 53 bits
 * @fires Pool#connect
 * @fires Pool#close
 * @fires Pool#error
 * @fires Pool#timeout
 * @fires Pool#parseError
 * @return {Pool} A cursor instance
 */
var Pool = function(options) {
  var self = this;
  // Add event listener
  EventEmitter.call(this);
  // Set empty if no options passed
  this.options = options || {};
  this.size = typeof options.size == 'number' && !isNaN(options.size) ? options.size : 5;
  this.waitMS = typeof options.waitMS == 'number' && !isNaN(options.waitMS) ? options.waitMS : 1000;

  // Save host and port
  this.host = options.host;
  this.port = options.port;

  // Message handler
  this.messageHandler = options.messageHandler;
  // No bson parser passed in
  if(!options.bson) throw new Error("must pass in valid bson parser");
  // Contains all connections
  this.connections = [];
  // Contains all available connections
  this.availableConnections = [];
  this.inUseConnections = [];
  this.newConnections = [];
  this.connectingConnections = [];
  // Current status of the pool
  this.state = DISCONNECTED;
  // Round robin index
  this.index = 0;
  this.dead = false;
  // Logger instance
  this.logger = Logger('Pool', options);
  // If we are monitoring this server we will create an exclusive reserved socket for that
  this.monitoring = typeof options.monitoring == 'boolean' ? options.monitoring : false;
  // Maintain the monitoring connection
  this.monitorConnection = null;
  // Pool id
  this.id = _id++;
  // Grouping tag used for debugging purposes
  this.tag = options.tag;
  // Operation work queue
  this.queue = [];
  // Currently executing
  this.executing = false;
}

inherits(Pool, EventEmitter);

var removeConnection = function(self, connection) {
  // console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! number Of connection :: " + self.getAll().length)
  var remove = function(connections) {
    for(var i = 0; i < connections.length; i++) {
      if(connections[i] === connection) {
        // console.log("!!!!!!!!!!!!! removing the connection")
        connections.splice(i, 1);
        return true;
      }
    }
  }

  // self.availableConnections.forEach(function(x) {
  //   console.log(x === connection)
  // })
  //
  // console.log("availableConnections :: " + self.availableConnections.length);
  // console.log("inUseConnections :: " + self.inUseConnections.length);
  // console.log("newConnections :: " + self.newConnections.length);
  // console.log("connectingConnections :: " + self.connectingConnections.length);

  // Set the monitoring connection to undefined
  if(self.monitorConnection === connection) {
    self.monitorConnection = null;
  }

  // Clean out the connection
  if(remove(self.availableConnections)) return;
  if(remove(self.inUseConnections)) return;
  if(remove(self.newConnections)) return;
  if(remove(self.connectingConnections)) return;
}

var errorHandler = function(self) {
  return function(err, connection) {
    // console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! error POOL")
    if(self.logger.isDebug()) self.logger.debug(f('pool [%s] errored out [%s] with connection [%s]', this.dead, JSON.stringify(err), JSON.stringify(connection)));
    // Remove the connection
    removeConnection(self, connection);
    // Emit error
    if(self.listeners('error').length > 0) {
      self.emit('error', err, connection);
    }
  }
}

var timeoutHandler = function(self) {
  return function(err, connection) {
    // console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! timeout POOL :: " + self.port)
    // console.log("----------------------- pool.write _execute 0 :: availableConnections = " + self.availableConnections.length)
    // console.log("----------------------- pool.write _execute 0 :: inUseConnections = " + self.inUseConnections.length)
    // console.log("----------------------- pool.write _execute 0 :: newConnections = " + self.newConnections.length)
    // console.log("----------------------- pool.write _execute 0 :: queue = " + self.queue.length)

    // // Get all connections
    // self.availableConnections.forEach(function(x) {
    //   if(x === self.monitorConnection) {
    //     // console.log("!!!!!!!!!!!!!!!!!!!!!!!!! MONITORING CONNECTION")
    //   }
    // })


    if(self.logger.isDebug()) self.logger.debug(f('pool [%s] timed out [%s] with connection [%s]', this.dead, JSON.stringify(err), JSON.stringify(connection)));
    // Remove the connection
    removeConnection(self, connection);
    // Emit connection timeout to server instance
    self.emit('timeout', err, connection);
  }
}

var closeHandler = function(self) {
  return function(err, connection) {
    // console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! close POOL")
    // if(connection === self.monitorConnection){
    //   console.log(" monitorConnection closed")
    // }

    if(self.logger.isDebug()) self.logger.debug(f('pool [%s] closed [%s] with connection [%s]', this.dead, JSON.stringify(err), JSON.stringify(connection)));
    // Remove the connection
    removeConnection(self, connection);
    // Emit connection close to server instance
    self.emit('close', err, connection);
  }
}

var parseErrorHandler = function(self) {
  return function(err, connection) {
    // console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! parseError POOL")
    if(self.logger.isDebug()) self.logger.debug(f('pool [%s] errored out [%s] with connection [%s]', this.dead, JSON.stringify(err), JSON.stringify(connection)));
    // Remove the connection
    removeConnection(self, connection);
    // Emit error to server instance
    self.emit('parseError', err, connection);
  }
}

/**
 * Destroy pool
 * @method
 */
Pool.prototype.destroy = function() {
  this.state = DESTROYED;
  // Set dead
  this.dead = true;
  // Get all the connections
  var connections = this.getAll();
  // Destroy all the connections
  connections.forEach(function(c) {
    // Destroy all event emitters
    ["close", "message", "error", "timeout", "parseError", "connect"].forEach(function(e) {
      c.removeAllListeners(e);
    });

    // Destroy the connection
    c.destroy();
  });

  // Do we have a monitoring connection
  if(this.monitorConnection) {
    this.monitorConnection.destroy();
  }
}

var execute = null;

try {
  execute = setImmediate;
} catch(err) {
  execute = process.nextTick;
}

// execute = function(x) {
//   setTimeout(x, 1)
// }

// execute = setTimeout;

/**
 * Connect pool
 * @method
 */
Pool.prototype.connect = function(_options) {
  // console.log("---------------------- CONNECT")
  var self = this;
  // Set to connecting
  this.state = CONNECTING
  // No dead
  this.dead = false;

  // Ensure we allow for a little time to setup connections
  var wait = 1;

  // Number of initial connections to perform
  var numberOfConnections = this.monitoring ? 2 : 1;

  // Connect all sockets
  for(var i = 0; i < numberOfConnections; i++) {
    setTimeout(function() {
      execute(function() {
        self.options.messageHandler = self.messageHandler;
        var connection = new Connection(self.options);

        // Add all handlers
        connection.once('close', closeHandler(self));
        connection.once('error', errorHandler(self));
        connection.once('timeout', timeoutHandler(self));
        connection.once('parseError', parseErrorHandler(self));
        connection.on('connect', function(connection) {
          // Add the connection to the list of available connections
          self.availableConnections.push(connection);

          // Have we finished the initial connection
          if(self.availableConnections.length == numberOfConnections) {
            // Reserve a monitoring socket
            if(self.monitoring) {
              self.monitorConnection = self.availableConnections.pop();
            }

            // Done connecting
            self.emit("connect", self);
          }
        });

        // Start connection
        connection.connect(_options);
      });
    }, wait);

    // wait for 1 miliseconds before attempting to connect, spacing out connections
    wait = wait + 1;
  }
}

var _createConnection = function(self) {
  self.options.messageHandler = self.messageHandler;
  var connection = new Connection(self.options);

  // Push the connection
  self.connectingConnections.push(connection);

  // Handle any errors
  var tempErrorHandler = function(err) {
    // self.emit('error', err);
  }

  // All event handlers
  var handlers = ["close", "message", "error", "timeout", "parseError", "connect"];

  // Handle successful connection
  var tempConnectHandler = function() {
    // console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! tempConnectHandler 0")
    // Destroy all event emitters
    handlers.forEach(function(e) {
      connection.removeAllListeners(e);
    });
    // console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! tempConnectHandler 1")

    // Add the final handlers
    connection.once('close', closeHandler(self));
    // console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! tempConnectHandler 3")
    connection.once('error', errorHandler(self));
    // console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! tempConnectHandler 4")
    connection.once('timeout', timeoutHandler(self));
    // console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! tempConnectHandler 5")
    connection.once('parseError', parseErrorHandler(self));

    // Remove the connection from the connectingConnections
    var index = self.connectingConnections.indexOf(connection);
    if(index != -1) {
      self.connectingConnections.splice(index, 1);
    }

    // connection.destroy()

    // Add to queue of new connection
    self.newConnections.push(connection);
    // console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! tempConnectHandler 8")
    // Emit connection to server instance
    // alowing it to apply any needed authentication
    self.emit('connection', connection);

    // Execute any work waiting
    _execute(self)();
  }

  // Add all handlers
  connection.once('close', tempErrorHandler);
  connection.once('error', tempErrorHandler);
  connection.once('timeout', tempErrorHandler);
  connection.once('parseError', tempErrorHandler);
  connection.once('connect', tempConnectHandler);

  // Start connection
  connection.connect();
}

var _execute = function(self) {
  return function() {
    if(self.state == 'DESTROYED') return;
    // Already executing, skip
    if(self.executing) return;
    // Set pool as executing
    self.executing = true;
    // console.log("----------------------- _execute")
    // console.log("----------------------- pool.write _execute 0 :: availableConnections = " + self.availableConnections.length)
    // console.log("----------------------- pool.write _execute 0 :: inUseConnections = " + self.inUseConnections.length)
    // console.log("----------------------- pool.write _execute 0 :: newConnections = " + self.newConnections.length)
    // console.log("----------------------- pool.write _execute 0 :: queue = " + self.queue.length)

    // Total availble connections
    var totalConnections = self.availableConnections.length
      + self.connectingConnections.length
      + self.inUseConnections.length
      + self.newConnections.length;

    // Have we not reached the max connection size yet
    if(self.availableConnections.length == 0
      && self.connectingConnections.length == 0
      && totalConnections < self.size
      && self.queue.length > 0) {
      // Create a new connection
      _createConnection(self);
      // Attempt to execute again
      self.executing = false;
      return;
    }

    // Number of ops to do
    var numberOfOps = self.availableConnections.length > self.queue.length
      ? self.queue.length : self.availableConnections.length;

    // As long as we have available connections
    while(true) {
      // No available connections available
      if(self.availableConnections.length == 0) break;
      if(self.queue.length == 0) break;

      // Get a connection
      var connection = self.availableConnections.pop();
      if(connection.isConnected()) {
        var workItem = self.queue.shift();

        // Add connection to callback so we can flush out
        // only ops for that connection on a socket closure
        if(workItem.cb) {
          workItem.cb.connection = connection;
        }

        // Get actual binary commands
        var buffer = workItem.buffer;

        // Add connection to workers in flight
        if(connection !== self.monitorConnection) {
          self.inUseConnections.push(connection);
        }

        // console.log("---------- write to connection " + connection.id)

        if(Array.isArray(buffer)) {
          for(var i = 0; i < buffer.length; i++) {
            connection.write(buffer[i]);
          }
        } else {
          connection.write(buffer);
        }

        // Fire and forgot message
        if(workItem.immediateRelease) {
          self.availableConnections.push(connection);
        }
      }
    }

    self.executing = false;
  }
}

/**
 * Write a message to MongoDB
 * @method
 * @return {Connection}
 */
Pool.prototype.write = function(buffer, cb, options) {
  // We have a monitoring operation and need to use
  // the dedicated connection
  if(options && options.monitoring && !this.monitorConnection) {
    this.monitorConnection = this.availableConnections.pop();
  }

  if(options && options.monitoring && this.monitorConnection) {
    return this.monitorConnection.write(buffer);
  }

  // Do we have an operation
  var operation = {buffer:buffer, cb: cb};
  // Do we immediately release the connection back to available (fire and forget)
  if(options && options.immediateRelease) {
    operation.immediateRelease = true;
  }

  // Push the operation to the queue of operations in progress
  this.queue.push(operation);
  // Attempt to write all buffers out
  _execute(this)();
}

/**
 * Make a passed connection available
 * @method
 * @return {Connection}
 */
Pool.prototype.connectionAvailable = function(connection) {
  // console.log("@@@@@@ connectionAvailable :: " + connection.id)
  // Get the connection from the newConnections
  var index = this.newConnections.indexOf(connection);
  if(index != -1) {
    this.newConnections.splice(index, 1);
  }

  // If it's in the inUseConnections
  index = this.inUseConnections.indexOf(connection);
  if(index != -1) {
    this.inUseConnections.splice(index, 1);
  }

  // Add the connection to available connections if it's not a monitoring threads
  if(this.availableConnections.indexOf(connection) == -1
    && connection !== this.monitorConnection) {
      this.availableConnections.push(connection);
  }

  // Fire execute loop
  _execute(this)();
}

/**
 * Get a pool connection (round-robin)
 * @method
 * @return {Connection}
 */
Pool.prototype.get = function(options) {
  options = options || {};

  // Set the current index
  this.index = this.index + 1;

  // Get all connections
  var connections = this.availableConnections.slice(0);

  if(connections.length == 1) {
    return connections[0];
  } else {
    this.index = this.index % connections.length;
    return connections[this.index];
  }
}

/**
 * Reduce the poolSize to the provided max connections value
 * @method
 * @param {number} maxConnections reduce the poolsize to maxConnections
 */
Pool.prototype.capConnections = function(maxConnections) {
  // Do we have more connections than specified slice it
  if(this.connections.length > maxConnections) {
    // Get the rest of the connections
    var connections = this.connections.slice(maxConnections);
    // Cap the active connections
    this.connections = this.connections.slice(0, maxConnections);

    if (this.index >= maxConnections){
      // Go back to the beggining of the pool if capping connections
      this.index = 0;
    }

    // Remove all listeners
    for(var i = 0; i < connections.length; i++) {
      connections[i].removeAllListeners('close');
      connections[i].removeAllListeners('error');
      connections[i].removeAllListeners('timeout');
      connections[i].removeAllListeners('parseError');
      connections[i].removeAllListeners('connect');
      connections[i].destroy();
    }
  }
}

/**
 * Get all pool connections
 * @method
 * @return {array}
 */
Pool.prototype.getAll = function() {
  return this.availableConnections
    .concat(this.inUseConnections)
    .concat(this.connectingConnections)
    .concat(this.newConnections);
}

/**
 * Is the pool connected
 * @method
 * @return {boolean}
 */
Pool.prototype.isConnected = function() {
  // Available connections
  for(var i = 0; i < this.availableConnections.length; i++) {
    if(this.availableConnections[i].isConnected()) return true;
  }

  // inUseConnections
  for(var i = 0; i < this.inUseConnections.length; i++) {
    if(this.inUseConnections[i].isConnected()) return true;
  }

  return this.state == CONNECTED;
}

/**
 * Was the pool destroyed
 * @method
 * @return {boolean}
 */
Pool.prototype.isDestroyed = function() {
  return this.state == DESTROYED;
}


/**
 * A server connect event, used to verify that the connection is up and running
 *
 * @event Pool#connect
 * @type {Pool}
 */

/**
 * The server connection closed, all pool connections closed
 *
 * @event Pool#close
 * @type {Pool}
 */

/**
 * The server connection caused an error, all pool connections closed
 *
 * @event Pool#error
 * @type {Pool}
 */

/**
 * The server connection timed out, all pool connections closed
 *
 * @event Pool#timeout
 * @type {Pool}
 */

/**
 * The driver experienced an invalid message, all pool connections closed
 *
 * @event Pool#parseError
 * @type {Pool}
 */

module.exports = Pool;
