"use strict";

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

/**
 * Creates a new Ping read preference strategy instance
 * @class
 * @param {number} [options.pingInterval=5000] Ping interval to check the response time to the different servers
 * @param {number} [options.acceptableLatency=250] Acceptable latency for selecting a server for reading (in milliseconds)
 * @return {Ping} A cursor instance
 */
var Ping = function(options) {
  // Add event listener
  EventEmitter.call(this);

  // Contains the ping state
  this.s = {
    // Contains all the ping data
      pings: {}
    // Set no options if none provided
    , options: options || {}
    // Logger
    , logger: Logger('Ping', options)
    // Ping interval
    , pingInterval: options.pingInterval || 10000
    , acceptableLatency: options.acceptableLatency || 15
    // Debug options
    , debug: typeof options.debug == 'boolean' ? options.debug : false
    // Index
    , index: 0
    // Current ping time
    , lastPing: null

  }

  // Log the options set
  if(this.s.logger.isDebug()) this.s.logger.debug(f('ping strategy interval [%s], acceptableLatency [%s]', this.s.pingInterval, this.s.acceptableLatency));

  // If we have enabled debug 
  if(this.s.debug) {
    // Add access to the read Preference Strategies
    Object.defineProperty(this, 'data', {
      enumerable: true, get: function() { return this.s.pings; }
    });    
  }
}

inherits(Ping, EventEmitter);

/**
 * @ignore
 */
var filterByTags = function(readPreference, servers) {
  if(readPreference.tags == null) return servers;
  var filteredServers = [];
  var tags = readPreference.tags;

  // Iterate over all the servers
  for(var i = 0; i < servers.length; i++) {
    var serverTag = servers[i].lastIsMaster().tags || {};
    // Did we find the a matching server
    var found = true;
    // Check if the server is valid
    for(var name in tags) {
      if(serverTag[name] != tags[name]) found = false;
    }

    // Add to candidate list
    if(found) filteredServers.push(servers[i]);
  }

  // Returned filtered servers
  return filteredServers;
}

/**
 * Pick a server
 * @method
 * @param {State} set The current replicaset state object 
 * @param {ReadPreference} readPreference The current readPreference object
 * @param {readPreferenceResultCallback} callback The callback to return the result from the function
 * @return {object}
 */
Ping.prototype.pickServer = function(set, readPreference) {
  var self = this;
  // Only get primary and secondaries as seeds
  var seeds = {};
  var servers = [];
  if(set.primary) {
    servers.push(set.primary);
  }

  for(var i = 0; i < set.secondaries.length; i++) {
    servers.push(set.secondaries[i]);
  }

  // Filter by tags
  servers = filterByTags(readPreference, servers);

  // Transform the list
  var serverList = [];
  // for(var name in seeds) {
  for(var i = 0; i < servers.length; i++) {
    serverList.push({name: servers[i].name, time: self.s.pings[servers[i].name] || 0});
  }

  // Sort by time
  serverList.sort(function(a, b) {
    return a.time > b.time;
  });

  // Locate lowest time (picked servers are lowest time + acceptable Latency margin)
  var lowest = serverList.length > 0 ? serverList[0].time : 0;

  // Filter by latency
  serverList = serverList.filter(function(s) {
    return s.time <= lowest + self.s.acceptableLatency;
  });

  // No servers, default to primary
  if(serverList.length == 0 && set.primary) {
    if(self.s.logger.isInfo()) self.s.logger.info(f('picked primary server [%s]', set.primary.name));
    return set.primary;
  } else if(serverList.length == 0) {
    return null
  }

  // We picked first server
  if(self.s.logger.isInfo()) self.s.logger.info(f('picked server [%s] with ping latency [%s]', serverList[0].name, serverList[0].time));

  // Add to the index
  self.s.index = self.s.index + 1;
  // Select the index
  self.s.index = self.s.index % serverList.length;
  // Return the first server of the sorted and filtered list
  return set.get(serverList[self.s.index].name);
}

/**
 * Start of an operation
 * @method
 * @param {Server} server The server the operation is running against
 * @param {object} query The operation running
 * @param {Date} date The start time of the operation
 * @return {object}
 */
Ping.prototype.startOperation = function(server, query, date) {
}

/**
 * End of an operation
 * @method
 * @param {Server} server The server the operation is running against
 * @param {error} err An error from the operation
 * @param {object} result The result from the operation
 * @param {Date} date The start time of the operation
 * @return {object}
 */
Ping.prototype.endOperation = function(server, err, result, date) {
}

/**
 * High availability process running
 * @method
 * @param {State} set The current replicaset state object 
 * @param {resultCallback} callback The callback to return the result from the function
 * @return {object}
 */
Ping.prototype.ha = function(topology, state, callback) {
  var self = this;
  var servers = state.getAll();
  var count = servers.length;

  // No servers return
  if(servers.length == 0) return callback(null, null);

  // Return if we have not yet reached the ping interval
  if(self.s.lastPing != null) {
    var diff = new Date().getTime() - self.s.lastPing.getTime();
    if(diff < self.s.pingInterval) return callback(null, null);
  }

  // Execute operation
  var operation = function(_server) {
    var start = new Date();
    // Execute ping against server
    _server.command('system.$cmd', {ismaster:1}, function(err, r) {
      count = count - 1;
      var time = new Date().getTime() - start.getTime();
      self.s.pings[_server.name] = time;
      // Log info for debug
      if(self.s.logger.isDebug()) self.s.logger.debug(f('ha latency for server [%s] is [%s] ms', _server.name, time));
      // We are done with all the servers
      if(count == 0) {
        // Emit ping event
        topology.emit('ping', err, r ? r.result : null);
        // Update the last ping time
        self.s.lastPing = new Date();
        // Return
        callback(null, null);
      }
    });
  }

  // Let's ping all servers
  while(servers.length > 0) {
    operation(servers.shift());
  }
}

var removeServer = function(self, server) {
  delete self.s.pings[server.name];
}

/**
 * Server connection closed
 * @method
 * @param {Server} server The server that closed
 */
Ping.prototype.close = function(server) {
  removeServer(this, server);
}

/**
 * Server connection errored out
 * @method
 * @param {Server} server The server that errored out
 */
Ping.prototype.error = function(server) {
  removeServer(this, server);
}

/**
 * Server connection timeout
 * @method
 * @param {Server} server The server that timed out
 */
Ping.prototype.timeout = function(server) {
  removeServer(this, server);
}

/**
 * Server connection happened
 * @method
 * @param {Server} server The server that connected
 * @param {resultCallback} callback The callback to return the result from the function
 */
Ping.prototype.connect = function(server, callback) {
  var self = this;
  // Get the command start date
  var start = new Date();
  // Execute ping against server
  server.command('system.$cmd', {ismaster:1}, function(err, r) {
    var time = new Date().getTime() - start.getTime();
    self.s.pings[server.name] = time;
    // Log info for debug
    if(self.s.logger.isDebug()) self.s.logger.debug(f('connect latency for server [%s] is [%s] ms', server.name, time));
    // Set last ping
    self.s.lastPing = new Date();
    // Done, return
    callback(null, null);
  });    
}

/**
 * This is a result from a readPreference strategy
 *
 * @callback readPreferenceResultCallback
 * @param {error} error An error object. Set to null if no error present
 * @param {Server} server The server picked by the strategy
 */

module.exports = Ping;