"use strict";

var Logger = require('../connection/logger')
  , f = require('util').format
  , ObjectId = require('bson').ObjectId
  , MongoError = require('../error');

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

/**
 * Creates a new Replicaset State object
 * @class
 * @property {object} primary Primary property
 * @property {array} secondaries List of secondaries
 * @property {array} arbiters List of arbiters
 * @return {State} A cursor instance
 */
var State = function(replSet, options) {
  this.replSet = replSet;
  this.options = options;
  this.secondaries = [];
  this.arbiters = [];
  this.passives = [];
  this.primary = null;
  // Initial state is disconnected
  this.state = DISCONNECTED;
  // Current electionId
  this.electionId = null;
  // Get a logger instance
  this.logger = Logger('ReplSet', options);
  // Unpacked options
  this.id = options.id;
  this.setName = options.setName;
  this.connectingServers = {};
  this.secondaryOnlyConnectionAllowed = options.secondaryOnlyConnectionAllowed;
  // Description of the Replicaset
  this.replicasetDescription = null;
}

/**
 * Is there a secondary connected
 * @method
 * @return {boolean}
 */
State.prototype.resetDescription = function() {
  this.replicasetDescription = {
    "topologyType": "Unknown",
    "servers": []
  }
}

function diff(previous, current) {
  // Difference document
  var diff = {
    servers: []
  }

  // Got through all the servers
  for(var i = 0; i < previous.servers.length; i++) {
    var prevServer = previous.servers[i];

    // Go through all current servers
    for(var j = 0; j < current.servers.length; j++) {
      var currServer = current.servers[j];

      // Matching server
      if(prevServer.address === currServer.address) {
        // We had a change in state
        if(prevServer.type != currServer.type) {
          diff.servers.push({
            address: prevServer.address,
            from: prevServer.type,
            to: currServer.type
          });
        }
      }
    }
  }

  // Return difference
  return diff;
}

function emitTopologyDescriptionChanged(self) {
  if(self.replSet.listeners('topologyDescriptionChanged').length > 0) {
    var topology = 'Unknown';
    var setName = self.setName;

    if(self.isPrimaryConnected() && self.isSecondaryConnected()) {
      topology = 'ReplicaSetWithPrimary';
    } else if(!self.isPrimaryConnected() && self.isSecondaryConnected()) {
      topology = 'ReplicaSetNoPrimary';
    }

    // Generate description
    var description = {
      topologyType: topology,
      setName: setName,
      servers: []
    }

    // Add the primary to the list
    if(self.isPrimaryConnected()) {
      var desc = self.primary.getDescription();
      desc.type = 'RSPrimary';
      description.servers.push(desc);
    }

    // Add all the secondaries
    description.servers = description.servers.concat(self.secondaries.map(function(x) {
      var description = x.getDescription();
      description.type = 'RSSecondary';
      return description;
    }));

    // Add all the arbiters
    description.servers = description.servers.concat(self.arbiters.map(function(x) {
      var description = x.getDescription();
      return description;
    }));

    // Add all the passives
    description.servers = description.servers.concat(self.passives.map(function(x) {
      var description = x.getDescription();
      description.type = 'RSSecondary';
      return description;
    }));

    // Create the result
    var result = {
      topologyId: self.id,
      previousDescription: self.replicasetDescription,
      newDescription: description,
      diff: diff(self.replicasetDescription, description)
    };

    // Emit the topologyDescription change
    self.replSet.emit('topologyDescriptionChanged', result);

    // Set the new description
    self.replicasetDescription = description;
  }
}

/**
 * Is there a secondary connected
 * @method
 * @return {boolean}
 */
State.prototype.isSecondaryConnected = function() {
  for(var i = 0; i < this.secondaries.length; i++) {
    if(this.secondaries[i].isConnected()) return true;
  }

  return false;
}

/**
 * Is there a primary connection
 * @method
 * @return {boolean}
 */
State.prototype.isPrimaryConnected = function() {
  return this.primary != null && this.primary.isConnected();
}

/**
 * Is the given address the primary
 * @method
 * @param {string} address Server address
 * @return {boolean}
 */
State.prototype.isPrimary = function(address) {
  if(this.primary == null) return false;
  return this.primary && this.primary.equals(address);
}

/**
 * Is the given address a secondary
 * @method
 * @param {string} address Server address
 * @return {boolean}
 */
State.prototype.isSecondary = function(address) {
  // Check if the server is a secondary at the moment
  for(var i = 0; i < this.secondaries.length; i++) {
    if(this.secondaries[i].equals(address)) {
      return true;
    }
  }

  return false;
}

/**
 * Is the given address a secondary
 * @method
 * @param {string} address Server address
 * @return {boolean}
 */
State.prototype.isPassive = function(address) {
  // Check if the server is a secondary at the moment
  for(var i = 0; i < this.passives.length; i++) {
    if(this.passives[i].equals(address)) {
      return true;
    }
  }

  return false;
}

State.prototype.clearConnectingServers = function() {
  for(var name in this.connectingServers) {
    if(typeof this.connectingServers[name].destroy == 'function') {
      this.connectingServers[name].destroy();
    }
  }

  this.connectingServers = {};
}

State.prototype.removeConnectingServer = function(address) {
  if(this.connectingServers[address]) {
    if(this.connectingServers[address].destroy) {
      this.connectingServers[address].destroy();
    }
  }

  delete this.connectingServers[address];
}

State.prototype.addConnectingServer = function(host, object) {
  this.connectingServers[host] = object;
}

State.prototype.isConnectingServer = function(host) {
  return this.connectingServers[host] != null;
}

State.prototype.connectingServersCount = function() {
  return Object.keys(this.connectingServers).length;
}

/**
 * Does the replicaset contain this server
 * @method
 * @param {string} address Server address
 * @return {boolean}
 */
State.prototype.contains = function(address) {
  if(this.primary && this.primary.equals(address)) return true;
  for(var i = 0; i < this.secondaries.length; i++) {
    if(this.secondaries[i].equals(address)) return true;
  }

  for(var i = 0; i < this.arbiters.length; i++) {
    if(this.arbiters[i].equals(address)) return true;
  }

  for(var i = 0; i < this.passives.length; i++) {
    if(this.passives[i].equals(address)) return true;
  }

  return false;
}

/**
 * Return all the valid and non passive secondaries
 * @method
 * @return {Array[Server]}
 */
State.prototype.getSecondaries = function() {
  // Filter out any non connected servers
  this.secondaries = this.secondaries.filter(function(server) {
    return server.isConnected();
  });

  // Filter out any hidden secondaries
  return this.secondaries.filter(function(server) {
    return server.lastIsMaster().hidden ? false : true;
  });
}

/**
 * Clean out all dead connections
 * @method
 */
State.prototype.clean = function() {
  var self = this;
  var disconnectedServers = [];

  if(this.primary != null && !this.primary.isConnected()) {
    disconnectedServers.push(this.primary);
    this.primary = null;
  }

  // Filter out disconnected servers
  this.secondaries = this.secondaries.filter(function(s) {
    if(!s.isConnected()) disconnectedServers.push(s);
    return s.isConnected();
  });

  // Filter out disconnected servers
  this.arbiters = this.arbiters.filter(function(s) {
    if(!s.isConnected()) disconnectedServers.push(s);
    return s.isConnected();
  });

  // Filter out disconnected servers
  this.passives = this.passives.filter(function(s) {
    if(!s.isConnected()) disconnectedServers.push(s);
    return s.isConnected();
  });

  return disconnectedServers;
}

/**
 * Unref state
 * @method
 */
State.prototype.unref = function() {
  if(this.primary) this.primary.unref();
  this.secondaries.forEach(function(s) {
    s.unref();
  });
  this.arbiters.forEach(function(s) {
    s.unref();
  });
}

// Remove listeners
var events = ['timeout', 'error', 'close', 'joined', 'left',
  'serverOpening', 'serverDescriptionChanged', 'serverHeartbeatStarted',
  'serverHeartbeatSucceeded', 'serverHearbeatFailed', 'serverClosed'];

var removeEvents = function(s) {
  events.forEach(function(e) {
    s.removeAllListeners(e);
  });
}

/**
 * Destroy state
 * @method
 */
State.prototype.destroy = function() {
  this.state = DESTROYED;

  if(this.primary) {
    this.primary.destroy();
    removeEvents(this.primary);
  }

  this.secondaries.forEach(function(s) {
    s.destroy();
    removeEvents(s);
  });

  this.arbiters.forEach(function(s) {
    s.destroy();
    removeEvents(s);
  });
}

/**
 * Remove server from state
 * @method
 * @param {Server} Server to remove
 * @return {string} Returns type of server removed (primary|secondary)
 */
State.prototype.remove = function(server) {
  if(this.primary && this.primary.equals(server)) {
    this.primary = null;
  }

  var length = this.arbiters.length;
  // Filter out the server from the arbiters
  this.arbiters = this.arbiters.filter(function(s) {
    return !s.equals(server);
  });
  if(this.arbiters.length < length) return 'arbiter';

  var length = this.passives.length;
  // Filter out the server from the passives
  this.passives = this.passives.filter(function(s) {
    return !s.equals(server);
  });

  // We have removed a passive
  if(this.passives.length < length)  {
    // Ensure we removed it from the list of secondaries as well if it exists
    this.secondaries = this.secondaries.filter(function(s) {
      return !s.equals(server);
    });
  }

  // Filter out the server from the secondaries
  this.secondaries = this.secondaries.filter(function(s) {
    return !s.equals(server);
  });

  // Get the isMaster
  var isMaster = server.lastIsMaster();
  // Return primary if the server was primary
  if(isMaster.ismaster && isMaster.hosts) return 'primary';
  if(isMaster.ismaster) return 'secondary';
  if(isMaster.secondary) return 'secondary';
  if(isMaster.passive) return 'passive';
  return 'arbiter';
}

/**
 * Get the server by name
 * @method
 * @param {string} address Server address
 * @return {Server}
 */
State.prototype.get = function(server) {
  var found = false;
  // All servers to search
  var servers = this.primary ? [this.primary] : [];
  servers = servers.concat(this.secondaries);
  // Locate the server
  for(var i = 0; i < servers.length; i++) {
    if(servers[i].equals(server)) {
      return servers[i];
    }
  }
}

/**
 * Get all the servers in the set
 * @method
 * @param {boolean} [options.includeArbiters] Include Arbiters in returned server list
 * @return {array}
 */
State.prototype.getAll = function(options) {
  options = options || {};
  var servers = [];
  if(this.primary) servers.push(this.primary);
  servers = servers.concat(this.secondaries);

  // Include the arbiters
  if(options.includeArbiters) {
    servers = servers.concat(this.arbiters);
  }

  return servers;
}

/**
 * All raw connections
 * @method
 * @param {boolean} [options.includeArbiters] Include Arbiters in returned server list
 * @return {array}
 */
State.prototype.getAllConnections = function(options) {
  options = options || {};
  var connections = [];
  if(this.primary) connections = connections.concat(this.primary.connections());
  this.secondaries.forEach(function(s) {
    connections = connections.concat(s.connections());
  })

  // Include the arbiters
  if(options.includeArbiters) {
    this.arbiters.forEach(function(s) {
      connections = connections.concat(s.connections());
    })
  }

  return connections;
}

/**
 * Return JSON object
 * @method
 * @return {object}
 */
State.prototype.toJSON = function() {
  return {
      primary: this.primary ? this.primary.lastIsMaster().me : null
    , secondaries: this.secondaries.map(function(s) {
      return s.lastIsMaster().me
    })
  }
}

/**
 * Returns the last known ismaster document for this server
 * @method
 * @return {object}
 */
State.prototype.lastIsMaster = function() {
  if(this.primary) return this.primary.lastIsMaster();
  if(this.secondaries.length > 0) return this.secondaries[0].lastIsMaster();
  return {};
}

/**
 * Promote server to primary
 * @method
 * @param {Server} server Server we wish to promote
 */
State.prototype.promotePrimary = function(server) {
  var currentServer = this.get(server);
  // Server does not exist in the state, add it as new primary
  if(currentServer == null) {
    this.primary = server;
    return;
  }

  // We found a server, make it primary and remove it from the secondaries
  // Remove the server first
  this.remove(currentServer);
  // Set as primary
  this.primary = currentServer;
}

var add = function(self, list, server) {
  // Check if the server is contained in the list
  for(var i = 0; i < list.length; i++) {
    if(list[i].equals(server)) return false;
  }

  // Add serer to list
  list.push(server);

  // Return true
  return true;
}

/**
 * Add server to list of secondaries
 * @method
 * @param {Server} server Server we wish to add
 */
State.prototype.addSecondary = function(server) {
  return add(this, this.secondaries, server);
}

/**
 * Add server to list of arbiters
 * @method
 * @param {Server} server Server we wish to add
 */
State.prototype.addArbiter = function(server) {
  return add(this, this.arbiters, server);
}

/**
 * Add server to list of passives
 * @method
 * @param {Server} server Server we wish to add
 */
State.prototype.addPassive = function(server) {
  return add(this, this.passives, server);
}

var compareObjectIds = function(id1, id2) {
  var a = new Buffer(id1.toHexString(), 'hex');
  var b = new Buffer(id2.toHexString(), 'hex');

  if(a === b) {
    return 0;
  }

  if(typeof Buffer.compare === 'function') {
    return Buffer.compare(a, b);
  }

  var x = a.length;
  var y = b.length;
  var len = Math.min(x, y);

  for (var i = 0; i < len; i++) {
    if (a[i] !== b[i]) {
      break;
    }
  }

  if (i !== len) {
    x = a[i];
    y = b[i];
  }

  return x < y ? -1 : y < x ? 1 : 0;
}

/**
 * Update the state given a specific ismaster result
 * @method
 * @param {object} ismaster IsMaster result
 * @param {Server} server IsMaster Server source
 */
State.prototype.update = function(ismaster, server) {
  var self = this;

  // Perform a cleanup before performing the update
  this.clean();

  // Not in a known connection valid state
  if((!ismaster.ismaster && !ismaster.secondary && !ismaster.arbiterOnly) || !Array.isArray(ismaster.hosts)) {
    // Remove the state
    var result = self.remove(server);
    if(self.state == CONNECTED)  {
      if(self.logger.isInfo()) self.logger.info(f('[%s] removing %s from set', self.id, ismaster.me));
      self.replSet.emit('left', result, server);
    }

    emitTopologyDescriptionChanged(this);
    return false;
  }

  // Set the setName if it's not set from the first server
  if(self.setName == null && ismaster.setName) {
    if(self.logger.isInfo()) self.logger.info(f('[%s] setting setName to %s', self.id, ismaster.setName));
    self.setName = ismaster.setName;
  }

  // Check if the replicaset name matches the provided one
  if(ismaster.setName && self.setName != ismaster.setName) {
    if(self.logger.isError()) self.logger.error(f('[%s] server in replset %s is not part of the specified setName %s', self.id, ismaster.setName, self.setName));
    self.remove(server);
    self.replSet.emit('error', new MongoError("provided setName for Replicaset Connection does not match setName found in server seedlist"));
    emitTopologyDescriptionChanged(this);
    return false;
  }

  // Log information
  if(self.logger.isInfo()) self.logger.info(f('[%s] updating replicaset state %s', self.id, JSON.stringify(this)));

  // It's a master set it
  if(ismaster.ismaster && self.setName == ismaster.setName && !self.isPrimary(ismaster.me)) {
    // Check if the electionId is not null
    if(ismaster.electionId instanceof ObjectId && self.electionId instanceof ObjectId) {
      if(compareObjectIds(self.electionId, ismaster.electionId) == -1) {
        self.electionId = ismaster.electionId;
      } else if(compareObjectIds(self.electionId, ismaster.electionId) == 0) {
        self.electionId = ismaster.electionId;
      } else {
        return false;
      }
    }

    // Initial electionId
    if(ismaster.electionId instanceof ObjectId && self.electionId == null) {
      self.electionId = ismaster.electionId;
    }

    // Promote to primary
    self.promotePrimary(server);
    // Log change of primary
    if(self.logger.isInfo()) self.logger.info(f('[%s] promoting %s to primary', self.id, ismaster.me));
    // Emit primary
    self.replSet.emit('joined', 'primary', this.primary);

    // Emit the description change
    emitTopologyDescriptionChanged(this);

    // We are connected
    if(self.state == CONNECTING) {
      self.state = CONNECTED;
      self.replSet.emit('connect', self.replSet);
    } else {
      self.state = CONNECTED;
      self.replSet.emit('reconnect', server);
    }
  } else if(!ismaster.ismaster && self.setName == ismaster.setName
    && ismaster.arbiterOnly) {
      if(self.addArbiter(server)) {
        if(self.logger.isInfo()) self.logger.info(f('[%s] promoting %s to arbiter', self.id, ismaster.me));
        self.replSet.emit('joined', 'arbiter', server);
        emitTopologyDescriptionChanged(this);
        return true;
      };

      return false;
  } else if(!ismaster.ismaster && self.setName == ismaster.setName
    && ismaster.secondary && ismaster.passive) {
      if(self.addPassive(server) && self.addSecondary(server)) {
        if(self.logger.isInfo()) self.logger.info(f('[%s] promoting %s to passive', self.id, ismaster.me));
        self.replSet.emit('joined', 'passive', server);

        // If we have secondaryOnlyConnectionAllowed and just a passive it's
        // still a valid connection
        if(self.secondaryOnlyConnectionAllowed && self.state == CONNECTING) {
          self.state = CONNECTED;
          self.replSet.emit('connect', self.replSet);
        }

        emitTopologyDescriptionChanged(this);
        return true;
      };

      return false;
  } else if(!ismaster.ismaster && self.setName == ismaster.setName
    && ismaster.secondary) {
      if(self.addSecondary(server)) {
        if(self.logger.isInfo()) self.logger.info(f('[%s] promoting %s to secondary', self.id, ismaster.me));
        self.replSet.emit('joined', 'secondary', server);

        // Is this the primary right now
        if(self.primary && self.primary.name == server.name) {
          self.primary = null;
        }

        if(self.secondaryOnlyConnectionAllowed && self.state == CONNECTING) {
          self.state = CONNECTED;
          self.replSet.emit('connect', self.replSet);
        }

        emitTopologyDescriptionChanged(this);
        return true;
      };

      return false;
  }

  // Return update applied
  return true;
}

module.exports = State;
