'use strict';

var util = require('util');
var fs = require('fs');
var path = require('path');
var JsonParser = require('jsonparse');
var qs = require('querystring');
var request = require('request');
var dns = require('dns');
var events = require('events');
var http = require('http');
var url = require('url');

var binding = require('./binding');
var connStr = require('./connstr');
var ViewQuery = require('./viewquery');
var SpatialQuery = require('./spatialquery');
var N1qlQuery = require('./n1qlquery');
var BucketManager = require('./bucketmgr');

var CONST = binding.Constants;
var CBpp = binding.CouchbaseImpl;

/**
 * The *CAS* value is a special object that indicates the current state
 * of the item on the server. Each time an object is mutated on the server, the
 * value is changed. <i>CAS</i> objects can be used in conjunction with
 * mutation operations to ensure that the value on the server matches the local
 * value retrieved by the client.  This is useful when doing document updates
 * on the server as you can ensure no changes were applied by other clients
 * while you were in the process of mutating the document locally.
 *
 * In the Node.js SDK, the CAS is represented as an opaque value. As such,y
 * ou cannot generate CAS objects, but should rather use the values returned
 * from a {@link Bucket.OpCallback}.
 *
 * @typedef {Object} Bucket.CAS
 */

/**
 * @class CouchbaseError
 * @classdesc
 * The virtual class thrown for all Couchnode errors.
 * @private
 * @extends Error
 */
/**
 * The error code for this error.
 * @var {errors} CouchbaseError#code
 */

/**
 * Single-Key callbacks.
 *
 * This callback is passed to all of the single key functions.
 *
 * It returns a result objcet containing a combination of a CAS and a value,
 * depending on which operation was invoked.
 *
 * @typedef {function} Bucket.OpCallback
 *
 * @param {undefined|Error} error
 *  The error for the operation. This can either be an Error object
 *  or a value which evaluates to false (null, undefined, 0 or false).
 * @param {Object} result
 *  The result of the operation that was executed.  This usually contains
 *  at least a <i>cas</i> property, and on some operations will contain a
 *  <i>value</i> property as well.
 */

/**
 * Multi-Get Callback.
 *
 * This callback is used to return results from a getMulti operation.
 *
 * @typedef {function} Bucket.MultiGetCallback
 *
 * @param {undefined|number} error
 *  The number of keys that failed to be retrieved.  The precise errors
 *  are available by checking the error property of the individual documents.
 * @param {Array.<Object>} results
 *  This is a map of keys to results.  The result for each key will optionally
 *  contain an error if one occured, or if no error occured will contain
 *  the CAS and value of the document.
 */

/**
 * This is used as a callback from executed queries.  It is a shortcut method
 * that automatically subscribes to the rows and error events of the
 * {@link Bucket.ViewQueryResponse}.
 *
 * @typedef {function} Bucket.QueryCallback
 *
 * @param {undefined|Error} error
 *  The error for the operation. This can either be an Error object
 *  or a falsy value.
 * @param {Array.<Object>} rows
 *  The rows returned from the query
 * @param {Bucket.ViewQueryResponse.Meta} meta
 *  The metadata returned by the query.
 */

/**
 * @class Bucket.TranscoderDoc
 * @classdesc
 * A class used in relation to transcoders
 *
 * @property {Buffer} value
 * @property {number} flags
 *
 * @since 2.0.0
 * @uncommitted
 */
/**
 * Transcoder Encoding Function.
 *
 * This function will receive a value when a storage operation is invoked
 * that needs to encode user-provided data for storage into Couchbase.  It
 * expects to be returned a `Buffer` object to store along with an integer
 * representing any flag metadata relating to how to decode the key later
 * using the matching {@link DecoderFunction}.
 *
 * @typedef {function} Bucket.EncoderFunction
 *
 * @param {*} value The value needing encoding.
 * @returns {!Bucket.TranscoderDoc} The data to store to Couchbase.
 */
/**
 * Transcoder Decoding Function.
 *
 * This function will receive an object containing a `Buffer` value and an
 * integer value representing any flags metadata whenever a retrieval
 * operation is executed.  It is expected that this function will return a
 * value representing the original value stored and encoded with its
 * matching {@link EncoderFunction}.
 *
 * @typedef {function} Bucket.DecoderFunction
 *
 * @param {!Bucket.TranscoderDoc} doc The data from Couchbase to decode.
 * @returns {*} The resulting value.
 */

/**
 * @class
 * The Bucket class represents a connection to a Couchbase bucket.  Never
 * instantiate this class directly.  Instead use the {@link Cluster#openBucket}
 * method instead.
 *
 * @private
 *
 * @since 2.0.0
 * @committed
 */
function Bucket(options) {
  // We normalize both for consistency as well as to
  //  create a duplicate object to use
  options.dsnObj = connStr.normalize(options.dsnObj);

  var bucketDsn = connStr.stringify(options.dsnObj);
  var bucketUser = options.username;
  var bucketPass = options.password;

  this._name = options.dsnObj.bucket;
  this._username = options.username;
  this._password = options.password;

  this._cb = new CBpp(bucketDsn, bucketUser, bucketPass);

  this.connected = null;
  this._cb.setConnectCallback(function(err) {
    if (err) {
      this.connected = false;
      var errObj = new Error('failed to connect to bucket');
      errObj.code = err;
      return this.emit('error', errObj);
    }
    this.connected = true;
    this.emit('connect');
  }.bind(this));

  this.waitQueue = [];
  this.on('connect', function() {
    for (var i = 0; i < this.waitQueue.length; ++i) {
      var itm = this.waitQueue[i];
      this._invoke(itm[0], itm[1]);
    }
    this.waitQueue = [];
  });
  this.on('error', function(err) {
    for (var i = 0; i < this.waitQueue.length; ++i) {
      var itm = this.waitQueue[i];
      itm[1][itm[1].length-1](err, null);
    }
    this.waitQueue = [];
  });

  this.httpAgent = new http.Agent();
  this.httpAgent.maxSockets = 250;

  /* istanbul ignore else  */
  if (options.dsnObj.hosts.length !== 1 ||
      options.dsnObj.hosts[0][1] ||
      (options.dsnObj.scheme !== 'couchbase' &&
        options.dsnObj.scheme !== 'couchbases')) {
    // We perform the connect on the next tick to ensure
    //   consistent behaviour between SRV and non-SRV.
    process.nextTick(function() {
      this._cb.connect();
    }.bind(this));
  } else {
    var srvHost = options.dsnObj.hosts[0][0];
    var srvPrefix = '_' + options.dsnObj.scheme;

    dns.resolveSrv(srvPrefix + '.' + srvHost, function(err, addrs) {
      if (!err) {
        options.dsnObj.hosts = [];
        for (var i = 0; i < addrs.length; ++i) {
          options.dsnObj.hosts.push([addrs[i].name, addrs[i].port]);
        }
        var srvDsn = connStr.stringify(options.dsnObj);
        this._cb.control(CONST.CNTL_REINIT_DSN, CONST.CNTL_SET, srvDsn);
      }

      this._cb.connect();
    }.bind(this));
  }
}
util.inherits(Bucket, events.EventEmitter);

/**
 * Connected Event.
 * Invoked once the connection has been established successfully.
 *
 * @event Bucket#connect
 *
 * @since 2.0.0
 * @committed
 */

/**
 * Error Event.
 * Invoked if the connection encounters any errors without having an
 * operation context available to handle the error.
 *
 * @event Bucket#error
 * @param {Error} err
 * The error that occured while attempting to connect to the cluster.
 *
 * @since 2.0.0
 * @committed
 */

/**
 * Enables N1QL support on the client.  A cbq-server URI must be passed.  This
 * method will be deprecated in the future in favor of automatic configuration
 * through the connected cluster.
 *
 * @param {string|string[]} hosts
 * An array of host/port combinations which are N1QL servers attached to
 * this cluster.
 *
 * @example
 * bucket.enableN1ql(['http://1.1.1.1:8093/','http://1.1.1.2:8093']);
 *
 * @since 2.0.0
 * @volatile
 */
/* istanbul ignore next */
Bucket.prototype.enableN1ql = function(hosts) {
  if (Array.isArray(hosts)) {
    this.queryhosts = hosts;
  } else {
    this.queryhosts = [hosts];
  }
};

/**
 * Returns an instance of a {@link BuckerManager} for performing management
 * operations against a bucket.
 *
 * @returns {BucketManager}
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.manager = function() {
  return new BucketManager(this);
};

/**
 * Shuts down this connection.
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.disconnect = function() {
  if (this.connected !== false) {
    this.connected = false;
    this._cb.shutdown();
  }
};

/**
 * Configures a custom set of transcoder functions for encoding and decoding
 * values that are being stored or retreived from the server.
 *
 * @param {EncoderFunction} encoder The function for encoding.
 * @param {DecoderFunction} decoder The function for decoding.
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.setTranscoder = function(encoder, decoder) {
  this._cb.setTranscoder(encoder, decoder);
};

Bucket.prototype._execAndUriParse = function(fn) {
  var uri = fn.call(this._cb);
  if (uri.substr(0, 7) === 'http://' ||
      uri.substr(0, 8) === 'https://') {
    return url.parse(uri);
  } else {
    return url.parse('http://' + uri);
  }
};

/**
 * Picks a random CAPI node and builds an http or https request against
 * it using the passed path.
 *
 * @param path
 * @param method
 * @returns {http.ClientRequest}
 *
 * @private
 * @ignore
 */
Bucket.prototype._capiRequest = function(path, method, callback) {
  var nodeUri = this._execAndUriParse(this._cb.getViewNode);
  var reqOpts = {
    agent: this.httpAgent,
    hostname: nodeUri.hostname,
    port: nodeUri.port,
    path: '/' + this._name + '/' + path,
    method: method,
    headers: { 'Content-Type': 'application/json' }
  };
  if (this._password) {
    reqOpts.auth = this._username + ':' + this._password;
  }
  callback(null, http.request(reqOpts));
};

/**
 * Picks a random management node and builds an http or https request against
 * it using the passsed path.
 *
 * @param path
 * @returns {http.ClientRequest}
 *
 * @private
 * @ignore
 */
Bucket.prototype._mgmtRequest = function(path, method, callback) {
  var nodeUri = this._execAndUriParse(this._cb.getMgmtNode);
  var reqOpts = {
    hostname: nodeUri.hostname,
    port: nodeUri.port,
    path: '/' + path,
    method: method,
    headers: { 'Content-Type': 'application/json' }
  };
  if (this._password) {
    reqOpts.auth = this._username + ':' + this._password;
  }
  callback(null, http.request(reqOpts));
};

/**
 * @class Meta
 * @classdesc
 * The meta-information available from a view query response.
 * @private
 * @memberof Bucket.ViewQueryResponse
 */
/**
 * The total number of rows available in the index of the view
 * that was queried.
 *
 * @var {number} Bucket.ViewQueryResponse.Meta#total_rows
 * @since 2.0.0
 * @committed
 */

/**
 * Emitted whenever a new row is available from a queries result set.
 *
 * @event Bucket.ViewQueryResponse#row
 * @param {Object} row
 * @param {Bucket.ViewQueryResponse.Meta} meta
 *
 * @since 2.0.0
 * @committed
 */
/**
 * Emitted whenever all rows are available from a queries result set.
 *
 * @event Bucket.ViewQueryResponse#rows
 * @param {Object[]} rows
 * @param {Bucket.ViewQueryResponse.Meta} meta
 *
 * @since 2.0.0
 * @committed
 */
/**
 * Emitted once a query has completed executing and emitting all rows.
 *
 * @event Bucket.ViewQueryResponse#end
 * @param {Bucket.ViewQueryResponse.Meta} meta
 *
 * @since 2.0.0
 * @committed
 */
/**
 * Emitted if an error occurs while executing a query.
 *
 * @event Bucket.ViewQueryResponse#error
 * @param {Error} error
 *
 * @since 2.0.0
 * @committed
 */

/**
 * An event emitter allowing you to bind to various query result set
 * events.
 *
 * @constructor
 *
 * @private
 * @memberof Bucket
 * @extends events.EventEmitter
 *
 * @since 2.0.0
 * @committed
 */
function ViewQueryResponse() {
}
util.inherits(ViewQueryResponse, events.EventEmitter);

/**
 * Executes a view http request.
 *
 * @param {string} viewtype
 * @param {string} ddoc
 * @param {string} name
 * @param {Object} q
 * @param {ViewQueryResponse} emitter
 *
 * @private
 * @ignore
 */
Bucket.prototype._viewReq = function(viewtype, ddoc, name, q, emitter) {
  var isSpatial = false;
  if (viewtype === '_spatial') {
    isSpatial = true;
  }

  var includeDocs = false;
  var opts = {};
  for (var i in q) {
    if (q.hasOwnProperty(i)) {
      if (i === 'include_docs') {
        if (q[i] && q[i] === 'true') {
          includeDocs = true;
        } else {
          includeDocs = false;
        }
      } else {
        opts[i] = q[i];
      }
    }
  }

  var rows = [];
  this._cb.viewQuery(
      isSpatial,
      ddoc, name,
      qs.stringify(opts),
      includeDocs,
  function(errCode, val) {
    if (errCode === -1) {
      var row = val;
      if (rows) {
        if (events.EventEmitter.listenerCount(emitter, 'rows') > 0) {
          rows.push(row);
        } else {
          rows = null;
        }
      }
      emitter.emit('row', row);
    } else if (errCode === 0) {
      var meta = val;
      if (rows) {
        emitter.emit('rows', rows, meta);
      }
      emitter.emit('end', meta);
    } else {
      var errStr = val;
      var jsonError = JSON.parse(errStr);
      var errorMessage = 'unknown error : error parsing failed';
      if (jsonError) {
        errorMessage = jsonError.message;
        if (jsonError.error || jsonError.reason) {
          var subError = jsonError.error + ': ' + jsonError.reason;
          if (!errorMessage) {
            errorMessage = subError;
          } else {
            errorMessage += ' (' + subError + ')';
          }
        }
      }
      emitter.emit('error', new Error(errorMessage));
    }
  });
};

/**
 * Performs a view request.
 *
 * @param {string} viewtype
 * @param {string} ddoc
 * @param {string} name
 * @param {Object} q
 * @param {function(err,res,meta)} callback
 *
 * @private
 * @ignore
 */
Bucket.prototype._view = function(viewtype, ddoc, name, q, callback) {
  var path = '_design/' + ddoc + '/' + viewtype + '/' +
      name + '?' + qs.stringify(q);

  var req = new ViewQueryResponse();
  this._maybeInvoke(this._viewReq.bind(this),
      [viewtype, ddoc, name, q, req, callback]);

  if (callback) {
    req.on('rows', function(rows, meta) {
      callback(null, rows, meta);
    });
    req.on('error', function(err) {
      callback(err, null, null);
    });
  }

  return req;
};

/**
 * @class Meta
 * @classdesc
 * The meta-information available from a view query response.
 * @private
 * @memberof Bucket.N1qlQueryResponse
 */
/**
 * The identifier for this query request.
 *
 * @var {number} Bucket.N1qlQueryResponse.Meta#requestID
 * @since 2.0.8
 * @committed
 */

/**
 * Emitted whenever a new row is available from a queries result set.
 *
 * @event Bucket.N1qlQueryResponse#row
 * @param {Object} row
 * @param {Bucket.N1qlQueryResponse.Meta} meta
 *
 * @since 2.0.8
 * @committed
 */
/**
 * Emitted whenever all rows are available from a queries result set.
 *
 * @event Bucket.N1qlQueryResponse#rows
 * @param {Object[]} rows
 * @param {Bucket.N1qlQueryResponse.Meta} meta
 *
 * @since 2.0.8
 * @committed
 */
/**
 * Emitted once a query has completed executing and emitting all rows.
 *
 * @event Bucket.N1qlQueryResponse#end
 * @param {Bucket.N1qlQueryResponse.Meta} meta
 *
 * @since 2.0.8
 * @committed
 */
/**
 * Emitted if an error occurs while executing a query.
 *
 * @event Bucket.N1qlQueryResponse#error
 * @param {Error} error
 *
 * @since 2.0.8
 * @committed
 */

/**
 * An event emitter allowing you to bind to various query result set
 * events.
 *
 * @constructor
 *
 * @private
 * @memberof Bucket
 * @extends events.EventEmitter
 *
 * @since 2.0.8
 * @committed
 */
function N1qlQueryResponse() {
}
util.inherits(N1qlQueryResponse, events.EventEmitter);

/**
 * Executes a N1QL http request.
 *
 * @param {string} queryStr
 * @param {N1qlQueryResponse} emitter
 *
 * @private
 * @ignore
 */
Bucket.prototype._n1qlReq = function(host, q, adhoc, emitter) {
  var rows = [];
  this._cb.n1qlQuery(
      host, q, adhoc,
  function(errCode, val) {
    if (errCode === -1) { // Row
      var row = val;
      rows.push(row);
      emitter.emit('row', row);
    } else if (errCode === 0) { // Success
      var meta = val;
      emitter.emit('rows', rows, meta);
      emitter.emit('end', meta);
    } else { // Error
      var errStr = val;
      var jsonError = JSON.parse(errStr);
      var err;
      if (jsonError.errors.length > 0) {
        var firstErr = jsonError.errors[0];
        err = new Error(firstErr.msg);
        err.code = firstErr.code;
        err.otherErrors = [];

        for (var i = 1; i < jsonError.errors.length; ++i) {
          var nextErr = jsonError.errors[i];
          var otherErr = new Error(nextErr.msg);
          otherErr.code = nextErr.code;
          err.otherErrors.push(otherErr);
        }
      } else {
        err = new Error('An unknown error occured');
      }
      err.requestID = jsonError.requestID;

      emitter.emit('error', err);
    }
  });
};

/**
 * Executes a N1QL query from a N1QL query string.
 *
 * @param {string} query
 * @param {function} callback

 * @private
 * @ignore
 */
Bucket.prototype._n1ql = function(query, params, callback) {
  var host;
  if (this.queryhosts) {
    var qhosts = this.queryhosts;
    host = qhosts[Math.floor(Math.random() * qhosts.length)];
    if (host.indexOf(':') === -1) {
      host = host + ':8093';
    }
  }

  var req = new N1qlQueryResponse();
  this._maybeInvoke(this._n1qlReq.bind(this),
      [host, query.toObject(params), query.isAdhoc, req, callback]);

  if (callback) {
    req.on('rows', function(rows, meta) {
      callback(null, rows, meta);
    });
    req.on('error', function(err) {
      callback(err, null, null);
    });
  }

  return req;
};

/**
 * Executes a previously prepared query object.  This could be a
 * {@link ViewQuery} or a {@link N1qlQuery}.
 *
 * Note: N1qlQuery queries are currently an uncommitted interface and may be
 * subject to change in 2.0.0's final release.
 *
 * @param {ViewQuery|N1qlQuery} query
 * The query to execute.
 * @param {Object|Array} [params]
 * A list or map to do replacements on a N1QL query.
 * @param {Bucket.QueryCallback} callback
 * @returns {Bucket.ViewQueryResponse|Bucket.N1qlQueryResponse}
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.query = function(query, params, callback) {
  if (params instanceof Function) {
    callback = arguments[1];
    params = undefined;
  }

  if (query instanceof ViewQuery) {
    return this._view(
        '_view', query.ddoc, query.name, query.options, callback);
  } else if (query instanceof SpatialQuery) {
    return this._view(
        '_spatial', query.ddoc, query.name, query.options, callback);
  } else if (query instanceof N1qlQuery) {
    return this._n1ql(
        query, params, callback
    );
  } else {
    throw new TypeError(
        'First argument needs to be a ViewQuery, SpatialQuery or N1qlQuery.');
  }
};

/**
 * Gets or sets a libcouchbase instance setting.
 *
 * @private
 * @ignore
 */
Bucket.prototype._ctl = function(cc, value) {
  if (value !== undefined) {
    return this._cb.control.call(this._cb, cc, CONST.CNTL_SET, value);
  } else {
    return this._cb.control.call(this._cb, cc, CONST.CNTL_GET);
  }
};


/**
 * Creates an durability failure Error object.
 *
 * @param {Error} innerError
 * The internal error that occured that caused the durability requirements to
 * fail to succeed.
 * @returns {CouchbaseError}
 *
 * @private
 * @ignore
 */
function _endureError(innerError)
{
  var out_error = new binding.Error('Durability requirements failed');
  out_error.code = CONST['ErrorCode::DURABILITY_FAILED'];
  out_error.innerError = innerError;
  return out_error;
}

/**
 * Common callback interceptor for durability requirements.  This function
 * will wrap user-provided callbacks in a handler which will ensure all
 * durability requirements are met prior to invoking the user-provided callback.
 *
 * @private
 * @ignore
 */
Bucket.prototype._interceptEndure =
    function(key, options, is_delete, callback) {
  if (!options.persist_to && !options.replicate_to) {
    // leave early if we can
    return callback;
  }

  // Return our interceptor
  var _this = this;
  return function(err, res) {
    if (err) {
      callback(err, res);
      return;
    }

    _this._cb.durability.call(_this._cb, key, options.hashkey, res.cas,
        options.persist_to, options.replicate_to, is_delete,
        function(endure_err) {
      if(endure_err) {
        callback(_endureError(endure_err), res);
        return;
      }

      callback(err, res);
    });
  };
};

/**
 * Invokes an operation and dispatches a callback error if one occurs.
 *
 * @param {Function} fn The operation callback to invoke.
 * @param {Array.<*>} args An array of arguments to pass to the function.
 *
 * @private
 * @ignore
 */
Bucket.prototype._invoke = function(fn, args) {
  try {
    fn.apply(this._cb, args);
  } catch(e) {
    args[args.length-1](e, null);
  }
};

/**
 * Will either invoke the binding method specified by fn, or alternatively
 * push the operation to a queue which is flushed once a connection
 * has been established or failed.
 *
 * @param {Function} fn The binding method to invoke.
 * @param {Array} args A list of arguments for the method.
 *
 * @private
 * @ignore
 */
Bucket.prototype._maybeInvoke = function(fn, args) {
  if (this.connected === true) {
    this._invoke(fn, args);
  } else if (this.connected === false) {
    throw new Error('cannot perform operations on a shutdown bucket');
  } else {
    this.waitQueue.push([fn, args]);
  }
};

/**
 * Deduces if the C++ binding layer will accept the passed value
 * as an appropriately typed key.
 *
 * @param {string|Buffer} key The key
 * @returns {boolean}
 *
 * @private
 * @ignore
 */
Bucket.prototype._isValidKey = function(key) {
  return typeof key === 'string' || key instanceof Buffer;
};

/**
 * Checks that the passed options have a valid hashkey specified.
 * Note that hashkey/groupid is not a supported feature of Couchbase Server
 * and this client. It should be considered volatile and experimental. Using
 * this could lead to an unbalanced cluster, inability to interoperate with the
 * data from other languages, not being able to use the Couchbase Server UI to
 * look up documents and other possible future upgrade/migration concerns.
 *
 * @param {Object} options The options objects to check.
 *
 * @private
 * @ignore
 */
Bucket.prototype._checkHashkeyOption = function(options) {
  if (options.hashkey !== undefined) {
    if (!this._isValidKey(options.hashkey)) {
      throw new TypeError('hashkey option needs to be a string or buffer.');
    }
  }
};

/**
 * Checks that the passed options have a valid expiry specified.
 *
 * @param {Object} options The options objects to check.
 *
 * @private
 * @ignore
 */
Bucket.prototype._checkExpiryOption = function(options) {
  if (options.expiry !== undefined) {
    if (typeof options.expiry !== 'number' || options.expiry < 0) {
      throw new TypeError('expiry option needs to be 0 or a positive integer.');
    }
  }
};

/**
 * Checks that the passed options have a valid cas specified.
 *
 * @param {Object} options The options objects to check.
 *
 * @private
 * @ignore
 */
Bucket.prototype._checkCasOption = function(options) {
  if (options.cas !== undefined) {
    if (typeof options.cas !== 'object' && typeof options.cas !== 'string') {
      throw new TypeError('cas option needs to be a CAS object or string.');
    }
  }
};

/**
 * Checks that the passed options have a valid persist_to
 * and replicate_to specified.
 *
 * @param {Object} options The options objects to check.
 *
 * @private
 * @ignore
 */
Bucket.prototype._checkDuraOptions = function(options) {
  if (options.persist_to !== undefined) {
    if (typeof options.persist_to !== 'number' ||
      options.persist_to < 0 || options.persist_to > 8) {
      throw new TypeError(
        'persist_to option needs to be an integer between 0 and 8.');
    }
  }
  if (options.replicate_to !== undefined) {
    if (typeof options.replicate_to !== 'number' ||
      options.replicate_to < 0 || options.replicate_to > 8) {
      throw new TypeError(
        'replicate_to option needs to be an integer between 0 and 8.');
    }
  }
};

/**
 * Retrieves a document.
 *
 * @param {string|Buffer} key
 * The target document key.
 * @param {Object} [options]
 * @param {Bucket.OpCallback} callback
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.get = function(key, options, callback) {
  if (options instanceof Function) {
    callback = arguments[1];
    options = {};
  }

  if (!this._isValidKey(key)) {
    throw new TypeError('First argument needs to be a string or buffer.');
  }
  if (typeof options !== 'object') {
    throw new TypeError('Second argument needs to be an object or callback.');
  }
  if (typeof callback !== 'function') {
    throw new TypeError('Third argument needs to be a callback.');
  }
  this._checkHashkeyOption(options);

  this._maybeInvoke(this._cb.get, [key, options.hashkey, 0, 0, callback]);
};

/**
 * Retrieves a list of keys
 *
 * @param {Array.<Buffer|string>} keys
 * The target document keys.
 * @param {Bucket.MultiGetCallback} callback
 *
 * @see Bucket#get
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.getMulti = function(keys, callback) {
  if (!Array.isArray(keys) || keys.length === 0) {
    throw new TypeError('First argument needs to be an array of length > 0.');
  }
  if (typeof callback !== 'function') {
    throw new TypeError('Second argument needs to be a callback.');
  }

  var self = this;
  var outMap = {};
  var resCount = 0;
  var errCount = 0;
  function getSingle(key) {
    self.get(key, function(err, res) {
      resCount++;
      if (err) {
        errCount++;
        outMap[key] = { error: err };
      } else {
        outMap[key] = res;
      }
      if (resCount === keys.length) {
        return callback(errCount, outMap);
      }
    });
  }
  for (var i = 0; i < keys.length; ++i) {
    getSingle(keys[i]);
  }
};

/**
 * Retrieves a document and updates the expiry of the item at the same time.
 *
 * @param {string|Buffer} key
 * The target document key.
 * @param {number} expiry
 * The expiration time to use. If a value of 0 is provided, then the
 * current expiration time is cleared and the key is set to
 * never expire. Otherwise, the key is updated to expire in the
 * time provided (in seconds).
 * @param {Object} [options]
 * @param {Bucket.OpCallback} callback
 *
 * @see Bucket#get
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.getAndTouch = function(key, expiry, options, callback) {
  if (options instanceof Function) {
    callback = arguments[2];
    options = {};
  }

  if (typeof key !== 'string' && !(key instanceof Buffer)) {
    throw new TypeError('First argument needs to be a string or buffer.');
  }
  if (typeof expiry !== 'number' || expiry < 0) {
    throw new TypeError('Second argument needs to be 0 or a positive integer.');
  }
  if (typeof options !== 'object') {
    throw new TypeError('Third argument needs to be an object or callback.');
  }
  if (typeof callback !== 'function') {
    throw new TypeError('Fourth argument needs to be a callback.');
  }
  this._checkHashkeyOption(options);
  this._checkDuraOptions(options);

  this._maybeInvoke(this._cb.get, [key, options.hashkey, expiry, 0, callback]);
};

/**
 * Lock the document on the server and retrieve it. When an document is locked,
 * its CAS changes and subsequent operations on the document (without providing
 * the current CAS) will fail until the lock is no longer held.
 *
 * This function behaves identically to {@link Bucket#get} in that it will
 * return the value. It differs in that the document is also locked. This
 * ensures that attempts by other client instances to access this document
 * while the lock is held will fail.
 *
 * Once locked, a document can be unlocked either by explicitly calling
 * {@link Bucket#unlock} or by performing a storage operation
 * (e.g. {@link Bucket#upsert}, {@link Bucket#replace}, {@link Bucket::append})
 * with the current CAS value.  Note that any other lock operations on this
 * key will fail while a document is locked.
 *
 * @param {string|Buffer} key
 * The target document key.
 * @param {Object} [options]
 *  @param {number} [options.lockTime=15]
 *  The duration of time the lock should be held for. Note that the maximum
 *  duration for a lock is 30 seconds, and if a higher value is specified,
 *  it will be rounded to this number.
 * @param {Bucket.OpCallback} callback
 *
 * @see Bucket#get
 * @see Bucekt#unlock
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.getAndLock = function(key, options, callback) {
  if (options instanceof Function) {
    callback = arguments[1];
    options = {};
  }

  if (!this._isValidKey(key)) {
    throw new TypeError('First argument needs to be a string or buffer.');
  }
  if (typeof options !== 'object') {
    throw new TypeError('Second argument needs to be an object or callback.');
  }
  if (typeof callback !== 'function') {
    throw new TypeError('Third argument needs to be a callback.');
  }
  if (options.lockTime !== undefined) {
    if (typeof options.lockTime !== 'number' || options.lockTime < 1) {
      throw new TypeError('lockTime option needs to be a positive integer.');
    }
  }
  this._checkHashkeyOption(options);

  this._maybeInvoke(this._cb.get,
    [key, options.hashkey, options.lockTime, 1, callback]);
};

/**
 * Get a document from a replica server in your cluster.
 *
 * @param {string|Buffer} key
 * The target document key.
 * @param {Object} [options]
 *  @param {number} [options.index=undefined]
 *  The index for which replica you wish to retrieve this value from, or
 *  if undefined, use the value from the first server that replies.
 * @param {Bucket.OpCallback} callback
 *
 * @see Bucket#get
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.getReplica = function(key, options, callback) {
  if (options instanceof Function) {
    callback = arguments[1];
    options = {};
  }
  if (typeof key !== 'string' && !(key instanceof Buffer)) {
    throw new TypeError('First argument needs to be a string or buffer.');
  }
  if (typeof options !== 'object') {
    throw new TypeError('Second argument needs to be an object or callback.');
  }
  if (typeof callback !== 'function') {
    throw new TypeError('Third argument needs to be a callback.');
  }
  if (options.hashkey !== undefined) {
    if (!this._isValidKey(options.hashkey)) {
      throw new TypeError('hashkey option needs to be a string or buffer.');
    }
  }
  this._checkHashkeyOption(options);

  this._maybeInvoke(this._cb.getReplica,
    [key, options.hashkey, options.index, callback]);
};

/**
 * Update the document expiration time.
 *
 * @param {string|Buffer} key
 * The target document key.
 * @param {number} expiry
 * The expiration time to use. If a value of 0 is provided, then the
 * current expiration time is cleared and the key is set to
 * never expire. Otherwise, the key is updated to expire in the
 * time provided (in seconds).  Values larger than 30*24*60*60 seconds
 * (30 days) are interpreted as absolute times (from the epoch).
 * @param {Object} [options]
 *  @param {number} [options.persist_to=0]
 *  Ensures this operation is persisted to this many nodes
 *  @param {number} [options.replicate_to=0]
 *  Ensures this operation is replicated to this many nodes
 * @param {Bucket.OpCallback} callback.
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.touch = function(key, expiry, options, callback) {
  if (options instanceof Function) {
    callback = arguments[2];
    options = {};
  }

  if (!this._isValidKey(key)) {
    throw new TypeError('First argument needs to be a string or buffer.');
  }
  if (typeof expiry !== 'number' || expiry < 0) {
    throw new TypeError('Second argument needs to be 0 or a positive integer..');
  }
  if (typeof options !== 'object') {
    throw new TypeError('Third argument needs to be an object or callback.');
  }
  if (typeof callback !== 'function') {
    throw new TypeError('Fourth argument needs to be a callback.');
  }
  this._checkHashkeyOption(options);
  this._checkCasOption(options);
  this._checkDuraOptions(options);

  this._maybeInvoke(this._cb.get, [key, options.hashkey, expiry, 0, callback]);
};

/**
 * Unlock a previously locked document on the server.  See the
 * {@link Bucket#lock} method for more details on locking.
 *
 * @param {string|Buffer} key
 * The target document key.
 * @param {Bucket.CAS} cas
 * The CAS value returned when the key was locked.  This operation will fail
 * if the CAS value provided does not match that which was the result of the
 * original lock operation.
 * @param {Object} [options]
 * @param {Bucket.OpCallback} callback
 *
 * @see Bucket#getAndLock
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.unlock = function(key, cas, options, callback) {
  if (options instanceof Function) {
    callback = arguments[2];
    options = {};
  }
  if (typeof key !== 'string' && !(key instanceof Buffer)) {
    throw new TypeError('First argument needs to be a string or buffer.');
  }
  if (typeof cas !== 'object') {
    throw new TypeError('Second argument needs to be a CAS object.');
  }
  if (typeof options !== 'object') {
    throw new TypeError('Third argument needs to be an object or callback.');
  }
  if (typeof callback !== 'function') {
    throw new TypeError('Fourth argument needs to be a callback.');
  }
  this._checkHashkeyOption(options);

  this._maybeInvoke(this._cb.unlock, [key, options.hashkey, cas, callback]);
};

/**
 * Deletes a document on the server.
 *
 * @param {string|Buffer} key
 * The target document key.
 * @param {Object} [options]
 *  @param {Bucket.CAS} [options.cas=undefined]
 *  The CAS value to check. If the item on the server contains a different
 *  CAS value, the operation will fail.  Note that if this option is undefined,
 *  no comparison will be performed.
 *  @param {number} [options.persist_to=0]
 *  Ensures this operation is persisted to this many nodes
 *  @param {number} [options.replicate_to=0]
 *  Ensures this operation is replicated to this many nodes
 * @param {Bucket.OpCallback} callback
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.remove = function(key, options, callback) {
  if (options instanceof Function) {
    callback = arguments[1];
    options = {};
  }

  if (!this._isValidKey(key)) {
    throw new TypeError('First argument needs to be a string or buffer.');
  }
  if (typeof options !== 'object') {
    throw new TypeError('Second argument needs to be an object or callback.');
  }
  if (typeof callback !== 'function') {
    throw new TypeError('Third argument needs to be a callback.');
  }
  this._checkHashkeyOption(options);
  this._checkCasOption(options);
  this._checkDuraOptions(options);

  this._maybeInvoke(this._cb.remove,
      [key, options.hashkey, options.cas,
        this._interceptEndure(key, options, 1, callback)]);
};

/**
 * Performs a storage operation.  This is a single handler function for all
 * possible storage operations.  This is thanks to libcouchbase handling them
 * all as a single entity as well.
 *
 * @param {string|Buffer} key
 * @param {*} value
 * @param {Object} [options]
 * @param {Bucket.OpCallback} callback
 * @param {number} opType
 *
 * @private
 * @ignore
 */
Bucket.prototype._store = function(key, value, options, callback, opType) {
  if (options instanceof Function) {
    callback = arguments[2];
    options = {};
  }

  if (!this._isValidKey(key)) {
    throw new TypeError('First argument needs to be a string or buffer.');
  }
  if (value === undefined) {
    throw new TypeError('Second argument must not be undefined.');
  }
  if (typeof options !== 'object') {
    throw new TypeError('Third argument needs to be an object or callback.');
  }
  if (typeof callback !== 'function') {
    throw new TypeError('Fourth argument needs to be a callback.');
  }
  this._checkHashkeyOption(options);
  this._checkExpiryOption(options);
  this._checkCasOption(options);
  this._checkDuraOptions(options);

  this._maybeInvoke(this._cb.store,
    [key, options.hashkey, value, options.expiry, options.cas, opType,
      this._interceptEndure(key, options, 0, callback)]);
};

/**
 * Stores a document to the bucket.
 *
 * @param {string|Buffer} key
 * The target document key.
 * @param {!*} value
 * The document's contents.
 * @param {Object} [options]
 *  @param {Bucket.CAS} [options.cas=undefined]
 *  The CAS value to check. If the item on the server contains a different
 *  CAS value, the operation will fail.  Note that if this option is undefined,
 *  no comparison will be performed.
 *  @param {number} [options.expiry=0]
 *  Set the initial expiration time for the document.  A value of 0 represents
 *  never expiring.
 *  @param {number} [options.persist_to=0]
 *  Ensures this operation is persisted to this many nodes
 *  @param {number} [options.replicate_to=0]
 *  Ensures this operation is replicated to this many nodes
 * @param {Bucket.OpCallback} callback
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.upsert = function(key, value, options, callback) {
  this._store(key, value, options, callback, CONST.SET);
};

/**
 * Identical to {@link Bucket#upsert} but will fail if the document already
 * exists.
 *
 * @param {string|Buffer} key
 * The target document key.
 * @param {!*} value
 * The document's contents.
 * @param {Object} [options]
 *  @param {number} [options.expiry=0]
 *  Set the initial expiration time for the document.  A value of 0 represents
 *  never expiring.
 *  @param {number} [options.persist_to=0]
 *  Ensures this operation is persisted to this many nodes
 *  @param {number} [options.replicate_to=0]
 *  Ensures this operation is replicated to this many nodes
 * @param {Bucket.OpCallback} callback
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.insert = function(key, value, options, callback) {
  this._store(key, value, options, callback, CONST.ADD);
};

/**
 * Identical to {@link Bucket#upsert}, but will only succeed if the document
 * exists already (i.e. the inverse of {@link Bucket#insert}).
 *
 * @param {string|Buffer} key
 * The target document key.
 * @param {!*} value
 * The document's contents.
 * @param {Object} [options]
 *  @param {Bucket.CAS} [options.cas=undefined]
 *  The CAS value to check. If the item on the server contains a different
 *  CAS value, the operation will fail.  Note that if this option is undefined,
 *  no comparison will be performed.
 *  @param {number} [options.expiry=0]
 *  Set the initial expiration time for the document.  A value of 0 represents
 *  never expiring.
 *  @param {number} [options.persist_to=0]
 *  Ensures this operation is persisted to this many nodes
 *  @param {number} [options.replicate_to=0]
 *  Ensures this operation is replicated to this many nodes
 * @param {Bucket.OpCallback} callback
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.replace = function(key, value, options, callback) {
  this._store(key, value, options, callback, CONST.REPLACE);
};

/**
 * Similar to {@link Bucket#upsert}, but instead of setting a new key,
 * it appends data to the existing key. Note that this function only makes
 * sense when the stored data is a string; 'appending' to a JSON document may
 * result in parse errors when the document is later retrieved.
 *
 * @param {string|Buffer} key
 * The target document key.
 * @param {!*} fragment
 * The document's contents to append.
 * @param {Object} [options]
 *  @param {Bucket.CAS} [options.cas=undefined]
 *  The CAS value to check. If the item on the server contains a different
 *  CAS value, the operation will fail.  Note that if this option is undefined,
 *  no comparison will be performed.
 *  @param {number} [options.persist_to=0]
 *  Ensures this operation is persisted to this many nodes
 *  @param {number} [options.replicate_to=0]
 *  Ensures this operation is replicated to this many nodes
 * @param {Bucket.OpCallback} callback
 *
 * @see Bucket#prepend
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.append = function(key, fragment, options, callback) {
  this._store(key, fragment, options, callback, CONST.APPEND);
};

/**
 * Like {@linkcode Bucket#append}, but prepends data to the existing value.
 *
 * @param {string|Buffer} key
 * The target document key.
 * @param {!*} fragment
 * The document's contents to prepend.
 * @param {Object} [options]
 *  @param {Bucket.CAS} [options.cas=undefined]
 *  The CAS value to check. If the item on the server contains a different
 *  CAS value, the operation will fail.  Note that if this option is undefined,
 *  no comparison will be performed.
 *  @param {number} [options.persist_to=0]
 *  Ensures this operation is persisted to this many nodes
 *  @param {number} [options.replicate_to=0]
 *  Ensures this operation is replicated to this many nodes
 * @param {Bucket.OpCallback} callback
 *
 * @see Bucket#append
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.prepend = function(key, fragment, options, callback) {
  this._store(key, fragment, options, callback, CONST.PREPEND);
};

/**
 * Increments or decrements a key's numeric value.
 *
 * Note that JavaScript does not support 64-bit integers (while libcouchbase
 * and the server do). You might receive an inaccurate value if the
 * number is greater than 53-bits (JavaScript's maximum integer precision).
 *
 * @param {string|Buffer} key
 * The target document key.
 * @param {number} delta
 * The amount to add or subtract from the counter value.  This value may be
 * any non-zero integer.
 * @param {Object} [options]
 *  @param {number} [options.initial=undefined]
 *  Sets the initial value for the document if it does not exist.  Specifying
 *  a value of undefined will cause the operation to fail if the document
 *  does not exist, otherwise this value must be equal to or greater than 0.
 *  @param {number} [options.expiry=0]
 *  Set the initial expiration time for the document.  A value of 0 represents
 *  never expiring.
 *  @param {number} [options.persist_to=0]
 *  Ensures this operation is persisted to this many nodes
 *  @param {number} [options.replicate_to=0]
 *  Ensures this operation is replicated to this many nodes
 * @param {Bucket.OpCallback} callback
 *
 * @since 2.0.0
 * @committed
 */
Bucket.prototype.counter = function(key, delta, options, callback) {
  if (options instanceof Function) {
    callback = arguments[2];
    options = {};
  }

  if (!this._isValidKey(key)) {
    throw new TypeError('First argument needs to be a string or buffer.');
  }
  if (typeof delta !== 'number' || delta === 0) {
    throw new TypeError('Second argument must be a non-zero integer.');
  }
  if (typeof options !== 'object') {
    throw new TypeError('Third argument needs to be an object or callback.');
  }
  if (typeof callback !== 'function') {
    throw new TypeError('Fourth argument needs to be a callback.');
  }
  if (options.initial) {
    if (typeof options.initial !== 'number' || options.initial < 0) {
      throw new TypeError('initial option must be 0 or a positive integer.');
    }
  }
  this._checkHashkeyOption(options);
  this._checkExpiryOption(options);
  this._checkDuraOptions(options);

  this._maybeInvoke(this._cb.arithmetic,
    [key, options.hashkey, options.expiry, delta, options.initial,
      this._interceptEndure(key, options, 0, callback)]);
};

/**
 * Gets or sets the operation timeout in milliseconds. The operation timeout
 * is the time that Bucket will wait for a response from the server for a CRUD
 * operation. If the response is not received within this time frame, the
 * operation is failed with an error.
 *
 * @member {number} Bucket#operationTimeout
 * @default 2500
 *
 * @since 2.0.0
 * @committed
 */
Object.defineProperty(Bucket.prototype, 'operationTimeout', {
  get: function() {
    return this._ctl(CONST.CNTL_OP_TIMEOUT);
  },
  set: function(val) {
    this._ctl(CONST.CNTL_OP_TIMEOUT, val);
  }
});

/**
 * Gets or sets the view timeout in milliseconds. The view timeout is the
 * time that Bucket will wait for a response from the server for a view request.
 * If the response is not received within this time frame, the request fails
 * with an error.
 *
 * @member {number} Bucket#viewTimeout
 *
 * @since 2.0.0
 * @committed
 */
Object.defineProperty(Bucket.prototype, 'viewTimeout', {
  get: function() {
    return this._ctl(CONST.CNTL_VIEW_TIMEOUT);
  },
  set: function(val) {
    this._ctl(CONST.CNTL_VIEW_TIMEOUT, val);
  }
});

/**
 * Gets or sets the durability timeout in milliseconds. The durability timeout
 * is the time that Bucket will wait for a response from the server in regards
 * to a durability request.  If there are no responses received within this time
 * frame, the request fails with an error.
 *
 * @member {number} Bucket#durabilityTimeout
 *
 * @since 2.0.0
 * @committed
 */
Object.defineProperty(Bucket.prototype, 'durabilityTimeout', {
  get: function() {
    return this._ctl(CONST.CNTL_DURABILITY_TIMEOUT);
  },
  set: function(val) {
    this._ctl(CONST.CNTL_DURABILITY_TIMEOUT, val);
  }
});

/**
 * Gets or sets the durability interval in milliseconds. The durability
 * interval is the time that Bucket will wait between requesting new durability
 * information during a durability poll.
 *
 * @member {number} Bucket#durabilityInterval
 *
 * @since 2.0.0
 * @committed
 */
Object.defineProperty(Bucket.prototype, 'durabilityInterval', {
  get: function() {
    return this._ctl(CONST.CNTL_DURABILITY_INTERVAL);
  },
  set: function(val) {
    this._ctl(CONST.CNTL_DURABILITY_INTERVAL, val);
  }
});

/**
 * Gets or sets the management timeout in milliseconds. The management timeout
 * is the time that Bucket will wait for a response from the server for a
 * management request.  If the response is not received within this time frame,
 * the request is failed out with an error.
 *
 * @member {number} Bucket#managementTimeout
 *
 * @since 2.0.0
 * @committed
 */
Object.defineProperty(Bucket.prototype, 'managementTimeout', {
  get: function() {
    return this._ctl(CONST.CNTL_HTTP_TIMEOUT);
  },
  set: function(val) {
    this._ctl(CONST.CNTL_HTTP_TIMEOUT, val);
  }
});

/**
 * Gets or sets the config throttling in milliseconds. The config throttling is
 * the time that Bucket will wait before forcing a configuration refresh.  If no
 * refresh occurs before this period while a configuration is marked invalid,
 * an update will be triggered.
 *
 * @member {number} Bucket#configThrottle
 *
 * @since 2.0.0
 * @committed
 */
Object.defineProperty(Bucket.prototype, 'configThrottle', {
  get: function() {
    return this._ctl(CONST.CNTL_CONFDELAY_THRESH);
  },
  set: function(val) {
    this._ctl(CONST.CNTL_CONFDELAY_THRESH, val);
  }
});

/**
 * Sets or gets the connection timeout in milliseconds. This is the timeout
 * value used when connecting to the configuration port during the initial
 * connection (in this case, use this as a key in the 'options' parameter in
 * the constructor) and/or when Bucket attempts to reconnect in-situ (if the
 * current connection has failed).
 *
 * @member {number} Bucket#connectionTimeout
 * @default 5000
 *
 * @since 2.0.0
 * @committed
 */
Object.defineProperty(Bucket.prototype, 'connectionTimeout', {
  get: function() {
    return this._ctl(CONST.CNTL_CONFIGURATION_TIMEOUT );
  },
  set: function(val) {
    this._ctl(CONST.CNTL_CONFIGURATION_TIMEOUT, val);
  }
});

/**
 * Sets or gets the node connection timeout in msecs. This value is similar to
 * {@link Bucket#connectionTimeout}, but defines the time to wait for a
 * particular node to respond before trying the next one.
 *
 * @member {number} Bucket#nodeConnectionTimeout
 *
 * @since 2.0.0
 * @committed
 */
Object.defineProperty(Bucket.prototype, 'nodeConnectionTimeout', {
  get: function() {
    return this._ctl(CONST.CNTL_CONFIG_NODE_TIMEOUT );
  },
  set: function(val) {
    this._ctl(CONST.CNTL_CONFIG_NODE_TIMEOUT, val);
  }
});

/**
 * Returns the libcouchbase version as a string.  This information will usually
 * be in the format of 2.4.0-fffffff representing the major, minor, patch and
 * git-commit that the built libcouchbase is based upon.
 *
 * @member {string} Bucket#lcbVersion
 *
 * @example
 * "2.4.0-beta.adbf222"
 *
 * @since 2.0.0
 * @committed
 */
Object.defineProperty(Bucket.prototype, 'lcbVersion', {
  get: function() {
    return this._cb.lcbVersion();
  },
  writeable: false
});

/**
 * Returns the version of the Node.js library as a string.
 *
 * @member {string} Bucket#clientVersion
 *
 * @example
 * "2.0.0-beta.fa123bd"
 *
 * @since 2.0.0
 * @committed
 */
Object.defineProperty(Bucket.prototype, 'clientVersion', {
  get: function() {
    var pkgJson = fs.readFileSync(
      path.resolve(__dirname, '../package.json'));
    return JSON.parse(pkgJson).version;
  },
  writeable: false
});

module.exports = Bucket;
