var ReadPreference = require('./read_preference').ReadPreference
  , Base = require('./base').Base
  , inherits = require('util').inherits;

/**
 * Mongos constructor provides a connection to a mongos proxy including failover to additional servers
 *
 * Options
 *  - **socketOptions** {Object, default:null}, an object containing socket options to use (noDelay:(boolean), keepAlive:(number), connectTimeoutMS:(number), socketTimeoutMS:(number))
 *  - **ha** {Boolean, default:true}, turn on high availability, attempts to reconnect to down proxies
 *  - **haInterval** {Number, default:2000}, time between each replicaset status check.
 *
 * @class Represents a Mongos connection with failover to backup proxies
 * @param {Array} list of mongos server objects
 * @param {Object} [options] additional options for the mongos connection
 */
var Mongos = function Mongos(servers, options) {
  // Set up basic
  if(!(this instanceof Mongos))
    return new Mongos(servers, options);

  // Set up event emitter
  Base.call(this);

  // Throw error on wrong setup
  if(servers == null || !Array.isArray(servers) || servers.length == 0)
    throw new Error("At least one mongos proxy must be in the array");

  // Ensure we have at least an empty options object
  this.options = options == null ? {} : options;
  // Set default connection pool options
  this.socketOptions = this.options.socketOptions != null ? this.options.socketOptions : {};
  // Enabled ha
  this.haEnabled = this.options['ha'] == null ? true : this.options['ha'];
  // How often are we checking for new servers in the replicaset
  this.mongosStatusCheckInterval = this.options['haInterval'] == null ? 2000 : this.options['haInterval'];
  // Save all the server connections
  this.servers = servers;
  // Servers we need to attempt reconnect with
  this.downServers = [];
  // Just contains the current lowest ping time and server
  this.lowestPingTimeServer = null;
  this.lowestPingTime = 0;

  // Add options to servers
  for(var i = 0; i < this.servers.length; i++) {
    var server = this.servers[i];
    // Default empty socket options object
    var socketOptions = {host: server.host, port: server.port};
    // If a socket option object exists clone it
    if(this.socketOptions != null) {
      var keys = Object.keys(this.socketOptions);
      for(var k = 0; k < keys.length;k++) socketOptions[keys[i]] = this.socketOptions[keys[i]];
    }
    // Set socket options
    server.socketOptions = socketOptions;
  }
}

/**
 * @ignore
 */
inherits(Mongos, Base);

/**
 * @ignore
 */
Mongos.prototype.isMongos = function() {
  return true;
}

/**
 * @ignore
 */
Mongos.prototype.connect = function(db, options, callback) {
  if('function' === typeof options) callback = options, options = {};
  if(options == null) options = {};
  if(!('function' === typeof callback)) callback = null;
  var self = this;

  // Keep reference to parent
  this.db = db;
  // Set server state to connecting
  this._serverState = 'connecting';
  // Number of total servers that need to initialized (known servers)
  this._numberOfServersLeftToInitialize = this.servers.length;
  // Default to the first proxy server as the first one to use
  this._currentMongos = this.servers[0];

  // Connect handler
  var connectHandler = function(_server) {
    return function(err, result) {
      self._numberOfServersLeftToInitialize = self._numberOfServersLeftToInitialize - 1;

      if(self._numberOfServersLeftToInitialize == 0) {
        // Start ha function if it exists
        if(self.haEnabled) {
          // Setup the ha process
          self._replicasetTimeoutId = setTimeout(self.mongosCheckFunction, self.mongosStatusCheckInterval);
        }

        // Set the mongos to connected
        self._serverState = "connected";
        // Emit the open event
        self.db.emit("open", null, self.db);
        // Callback
        callback(null, self.db);
      }
    }
  };

  // Error handler
  var errorOrCloseHandler = function(_server) {
    return function(err, result) {
      // Create current mongos comparision
      var currentUrl = self._currentMongos.host + ":" + self._currentMongos.port;
      var serverUrl = this.host + ":" + this.port;
      // We need to check if the server that closed is the actual current proxy we are using, otherwise
      // just ignore
      if(currentUrl == serverUrl) {
        // Remove the server from the list
        if(self.servers.indexOf(_server) != -1) {
          self.servers.splice(self.servers.indexOf(_server), 1);
        }

        // Pick the next one on the list if there is one
        for(var i = 0; i < self.servers.length; i++) {
          // Grab the server out of the array (making sure there is no servers in the list if none available)
          var server = self.servers[i];
          // Generate url for comparision
          var serverUrl = server.host + ":" + server.port;
          // It's not the current one and connected set it as the current db
          if(currentUrl != serverUrl && server.isConnected()) {
            self._currentMongos = server;
            break;
          }
        }
      }

      // Ensure we don't store the _server twice
      if(self.downServers.indexOf(_server) == -1) {
        // Add the server instances
        self.downServers.push(_server);
      }
    }
  }

  // Mongo function
  this.mongosCheckFunction = function() {
    // If we have down servers let's attempt a reconnect
    if(self.downServers.length > 0) {
      var numberOfServersLeft = self.downServers.length;
      // Attempt to reconnect
      for(var i = 0; i < self.downServers.length; i++) {
        var downServer = self.downServers.pop();
        // Attemp to reconnect
        downServer.connect(self.db, {returnIsMasterResults: true}, function(_server) {
          // Return a function to check for the values
          return function(err, result) {
            // Adjust the number of servers left
            numberOfServersLeft = numberOfServersLeft - 1;

            if(err != null) {
              self.downServers.push(_server);
            } else {
              // Add server event handlers
              _server.on("close", errorOrCloseHandler(_server));
              _server.on("error", errorOrCloseHandler(_server));
              // Add to list of servers
              self.servers.push(_server);
            }

            if(numberOfServersLeft <= 0) {
              // Perfom another ha
              self._replicasetTimeoutId = setTimeout(self.mongosCheckFunction, self.mongosStatusCheckInterval);
            }
          }
        }(downServer));
      }
    } else if(self.servers.length > 0) {
      var numberOfServersLeft = self.servers.length;
      var _s = new Date().getTime()

      // Else let's perform a ping command
      for(var i = 0; i < self.servers.length; i++) {
        var executePing = function(_server) {
          // Get a read connection
          var _connection = _server.checkoutReader();
          // Execute ping command
          self.db.command({ping:1}, {connection:_connection}, function(err, result) {
            var pingTime = new Date().getTime() - _s;
            // If no server set set the first one, otherwise check
            // the lowest ping time and assign the server if it's got a lower ping time
            if(self.lowestPingTimeServer == null) {
              self.lowestPingTimeServer = _server;
              self.lowestPingTime = pingTime;
              self._currentMongos = _server;
            } else if(self.lowestPingTime > pingTime
                && (_server.host != self.lowestPingTimeServer.host || _server.port != self.lowestPingTimeServer.port)) {
              self.lowestPingTimeServer = _server;
              self.lowestPingTime = pingTime;
              self._currentMongos = _server;
            }

            // Number of servers left
            numberOfServersLeft = numberOfServersLeft - 1;
            // All active mongos's pinged
            if(numberOfServersLeft == 0) {
              // Perfom another ha
              self._replicasetTimeoutId = setTimeout(self.mongosCheckFunction, self.mongosStatusCheckInterval);
            }
          })
        }
        // Execute the function
        executePing(self.servers[i]);
      }
    } else {
      self._replicasetTimeoutId = setTimeout(self.mongosCheckFunction, self.mongosStatusCheckInterval);
    }
  }

  // Connect all the server instances
  for(var i = 0; i < this.servers.length; i++) {
    // Get the connection
    var server = this.servers[i];
    server.mongosInstance = this;
    // Add server event handlers
    server.on("close", errorOrCloseHandler(server));
    server.on("timeout", errorOrCloseHandler(server));
    server.on("error", errorOrCloseHandler(server));
    // Connect the instance
    server.connect(self.db, {returnIsMasterResults: true}, connectHandler(server));
  }
}

/**
 * @ignore
 * Just return the currently picked active connection
 */
Mongos.prototype.allServerInstances = function() {
  return this.servers;
}

/**
 * Always ourselves
 * @ignore
 */
Mongos.prototype.setReadPreference = function() {}

/**
 * @ignore
 */
Mongos.prototype.allRawConnections = function() {
  // Neeed to build a complete list of all raw connections, start with master server
  var allConnections = [];
  // Add all connections
  for(var i = 0; i < this.servers.length; i++) {
    allConnections = allConnections.concat(this.servers[i].allRawConnections());
  }

  // Return all the conections
  return allConnections;
}

/**
 * @ignore
 */
Mongos.prototype.isConnected = function() {
  return this._serverState == "connected";
}

/**
 * @ignore
 */
Mongos.prototype.checkoutWriter = function() {
  // No current mongo, just pick first server
  if(this._currentMongos == null && this.servers.length > 0) {
    return this.servers[0].checkoutWriter();
  }
  return this._currentMongos.checkoutWriter();
}

/**
 * @ignore
 */
Mongos.prototype.checkoutReader = function(read) {
  // If we have a read preference object unpack it
  if(typeof read == 'object' && read['_type'] == 'ReadPreference') {
    // Validate if the object is using a valid mode
    if(!read.isValid()) throw new Error("Illegal readPreference mode specified, " + read.mode);
  } else if(!ReadPreference.isValid(read)) {
    throw new Error("Illegal readPreference mode specified, " + read);
  }

  // No current mongo, just pick first server
  if(this._currentMongos == null && this.servers.length > 0) {
    return this.servers[0].checkoutReader();
  }
  return this._currentMongos.checkoutReader();
}

/**
 * @ignore
 */
Mongos.prototype.close = function(callback) {
  var self = this;
  // Set server status as disconnected
  this._serverState = 'disconnected';
  // Number of connections to close
  var numberOfConnectionsToClose = self.servers.length;
  // If we have a ha process running kill it
  if(self._replicasetTimeoutId != null) clearTimeout(self._replicasetTimeoutId);
  // Close all proxy connections
  for(var i = 0; i < self.servers.length; i++) {
    self.servers[i].close(function(err, result) {
      numberOfConnectionsToClose = numberOfConnectionsToClose - 1;
      // Callback if we have one defined
      if(numberOfConnectionsToClose == 0 && typeof callback == 'function') {
        callback(null);
      }
    });
  }
}

/**
 * @ignore
 * Return the used state
 */
Mongos.prototype._isUsed = function() {
  return this._used;
}

exports.Mongos = Mongos;