// Copyright 2012 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert');
var EventEmitter = require('events').EventEmitter;
var util = require('util');

var pooling = require('pooling');

var ConnectionError = require('../errors').ConnectionError;
var BindResponse = require('../messages').BindResponse;

var Client = require('./client');



///--- Globals

var STD_OPS = [
  'add',
  'del',
  'modify',
  'modifyDN'
];

var RETURN_VAL_OPS = [
  'compare',
  'exop'
];



///--- Internal Functions

function createPool(options) {
  assert.ok(options);

  return pooling.createPool({
    checkInterval: options.checkInterval,
    log: options.log,
    name: 'ldapjs_' + (options.url || options.socketPath),
    max: options.maxConnections,
    maxIdleTime: options.maxIdleTime,

    create: function createClient(callback) {
      var client = new Client(options);

      client.on('error', function (err) {
        client.removeAllListeners('connect');
        return callback(err);
      });

      client.once('connect', function onConnect() {
        client.removeAllListeners('error');

        if (!options.bindDN || !options.bindCredentials)
          return callback(null, client);

        function bindCallback(err, res) {
          if (err)
            return callback(err, null);

          return callback(null, client);
        }

        return client.bind(options.bindDN,
                           options.bindCredentials,
                           options.bindControls || [],
                           bindCallback);
      });
    },

    check: function check(client, callback) {
      // just do a root dse search
      client.search('', '(objectclass=*)', function (err, res) {
        if (err)
          return callback(err);

        res.on('error', function (e) {
          client.removeAllListeners('end');
          callback(e);
        });

        res.on('end', function () {
          client.removeAllListeners('error');
          callback(null);
        });

        return undefined;
      });
    },

    destroy: function destroy(client) {
      client.unbind(function () {});
    }
  });
}



///--- API

function ClientPool(options) {
  assert.ok(options);
  EventEmitter.call(this, options);

  this.log = options.log.child({clazz: 'ClientPool'}, true);
  this.options = {
    bindDN: options.bindDN,
    bindCredentials: options.bindCredentials,
    bindControls: options.bindControls || [],
    checkInterval: options.checkInterval,
    connectTimeout: (options.connectTimeout || 0),
    maxIdleTime: options.maxIdleTime,
    maxConnections: options.maxConnections,
    log: options.log,
    socketPath: options.socketPath,
    timeout: (options.timeout || 0),
    url: options.url
  };
  this.pool = createPool(options);
}
util.inherits(ClientPool, EventEmitter);
module.exports = ClientPool;



STD_OPS.forEach(function (op) {
  ClientPool.prototype[op] = function clientProxy() {
    var args = Array.prototype.slice.call(arguments);
    var cb = args.pop();
    if (typeof (cb) !== 'function')
      throw new TypeError('callback (Function) required');
    var self = this;

    return this.pool.acquire(function onAcquire(err, client) {
      if (err)
        return cb(err);

      args.push(function proxyCallback(err, res) {
        self.pool.release(client);
        return cb(err, res);
      });

      try {
        return Client.prototype[op].apply(client, args);
      } catch (e) {
        self.pool.release(client);
        return cb(e);
      }
    });
  };
});


RETURN_VAL_OPS.forEach(function (op) {
  ClientPool.prototype[op] = function clientProxy() {
    var args = Array.prototype.slice.call(arguments);
    var cb = args.pop();
    if (typeof (cb) !== 'function')
      throw new TypeError('callback (Function) required');
    var self = this;

    return this.pool.acquire(function onAcquire(poolErr, client) {
      if (poolErr)
        return cb(poolErr);

      args.push(function proxyCallback(err, val, res) {
        self.pool.release(client);
        return cb(err, val, res);
      });

      try {
        return Client.prototype[op].apply(client, args);
      } catch (e) {
        self.pool.release(client);
        return cb(e);
      }
    });
  };
});


ClientPool.prototype.search = function search(base, opts, controls, callback) {
  if (typeof (controls) === 'function') {
    callback = controls;
    controls = [];
  }

  var self = this;

  return this.pool.acquire(function onAcquire(err, client) {
    if (err)
      return callback(err);

    // This is largely in existence for search requests
    client.timeout = self.timeout || client.timeout;


    return client.search(base, opts, controls, function (err, res) {
      function cleanup() {
        self.pool.release(client);
      }

      if (err) {
        cleanup();
        return callback(err, res);
      }
      res.on('error', cleanup);
      res.on('end', cleanup);

      return callback(null, res);
    });
  });
};


ClientPool.prototype.abandon = function abandon(msgid, controls, callback) {
  if (typeof (controls) === 'function') {
    callback = controls;
    controls = [];
  }

  this.log.error({
    messageID: msgid
  }, 'Abandon is not supported with connection pooling. Ignoring.');
  return callback(null);
};


ClientPool.prototype.bind = function bind(dn, creds, controls, callback) {
  if (typeof (controls) === 'function') {
    callback = controls;
    controls = [];
  }

  var self = this;

  self.options.bindDN = null;
  self.options.bindCredentials = null;
  self.options.bindControls = null;

  return this.pool.shutdown(function () {
    self.pool = createPool(self.options);

    return self.pool.acquire(function onAcquire(err, client) {
      if (err)
        return callback(err);

      return client.bind(dn, creds, controls, function (err, res) {
        self.pool.release(client);

        if (err)
          return callback(err, res);

        self.options.bindDN = dn;
        self.options.bindCredentials = creds;
        self.options.bindControls = controls;

        return callback(null, res);
      });
    });
  });
};


ClientPool.prototype.unbind = function unbind(callback) {
  return this.pool.shutdown(callback);
};