'use strict';

var events = require('events');
var util = require('util');
var ViewQuery = require('../viewquery');
var SpatialQuery = require('../spatialquery');
var N1qlQuery = require('../n1qlquery');
var BucketManager = require('./bucketmgr');
var CbError = require('./error');
var errs = require('../errors');

// Mock version should match the latest fully supported version of couchnode.
var MOCK_VERSION = '2.0.0';

function MockCouchbaseCas(idx) {
  this.x = idx;
}
MockCouchbaseCas.prototype.toString = function() {
  return this.x.toString(10);
};
MockCouchbaseCas.prototype.toJSON = function() {
  return this.x.toString(10);
};
MockCouchbaseCas.prototype.inspect = function() {
  return 'MockCouchbaseCas<' + this.x + '>';
};

var casCounter = 0;
/* istanbul ignore next */
function _createCas() {
  return new MockCouchbaseCas(casCounter++);
}
/* istanbul ignore next */
function _compareCas(a, b) {
  if (!b) {
    return true;
  } else if (!a && b) {
    return false;
  } else {
    if (typeof a === 'string') {
      a = {x: parseInt(a, 10)};
    }
    if (typeof b === 'string') {
      b = {x: parseInt(b, 10)};
    }
    return a.x === b.x;
  }
}
/* istanbul ignore next */
function _makeExpiryDate(expiry) {
  if (!expiry) {
    return null;
  }

  if (expiry <= 30*24*60*60) {
    var dt = new Date();
    dt.setTime(dt.getTime() + (expiry*1000));
    return dt;
  } else {
    return new Date(expiry * 1000);
  }
}
/* istanbul ignore next */
function _makeLockDate(lockTime) {
  var dt = new Date();
  dt.setTime(dt.getTime() + (lockTime*1000));
  return dt;
}

var FLAGS = {
  // Node Flags - Formats
  NF_JSON: 0x00,
  NF_RAW: 0x02,
  NF_UTF8: 0x04,
  NF_MASK: 0xFF
};
function _defaultEncode(doc) {
  if (typeof doc === 'string') {
    return {
      flags: FLAGS.NF_UTF8,
      value: new Buffer(doc, 'utf8')
    };
  } else if (Buffer.isBuffer(doc)) {
    return {
      flags: FLAGS.NF_RAW,
      value: new Buffer(doc)
    };
  } else {
    return {
      flags: FLAGS.NF_JSON,
      value: new Buffer(JSON.stringify(doc), 'utf8')
    };
  }
}
function _defaultDecode(info) {
  if (info.flags === FLAGS.NF_UTF8) {
    return info.value.toString('utf8');
  } else if (info.flags === FLAGS.NF_RAW) {
    return new Buffer(info.value);
  } else if (info.flags === FLAGS.NF_JSON) {
    return JSON.parse(info.value.toString('utf8'));
  } else {
    return new Buffer(info.value);
  }
}

/* istanbul ignore next */
function MockStorage() {
  this.items = {};
}
/* istanbul ignore next */
MockStorage.prototype._encodeKey = function(key) {
  return key;
};
/* istanbul ignore next */
MockStorage.prototype.get = function(key, hashkey) {
  if (!hashkey) {
    throw new Error('invalid hashkey');
  }

  var keyItem = this.items[this._encodeKey(key)];
  if (!keyItem) {
    return null;
  }
  var item = keyItem[this._encodeKey(hashkey)];
  if (!item) {
    return null;
  }
  if (item.expiry && item.expiry <= new Date()) {
    return null;
  }
  return item;
};
/* istanbul ignore next */
MockStorage.prototype.set = function(key, hashkey, item) {
  if (!hashkey) {
    throw new Error('invalid hashkey');
  }

  var keyE = this._encodeKey(key);
  var hashkeyE = this._encodeKey(hashkey);
  if (!this.items[keyE]) {
    this.items[keyE] = {};
  }
  this.items[keyE][hashkeyE] = item;
};
/* istanbul ignore next */
MockStorage.prototype.remove = function(key, hashkey) {
  if (!hashkey) {
    throw new Error('invalid hashkey');
  }

  var keyE = this._encodeKey(key);
  var hashkeyE = this._encodeKey(hashkey);
  if (!this.items[keyE]) {
    return null;
  }

  delete this.items[keyE][hashkeyE];
  return _createCas();
};

function MockBucket(options) {
  this.storage = new MockStorage();
  this.ddocs = {};
  this.operationTimeout = 2500;
  this.viewTimeout = 2500;
  this.durabilityTimeout = 2500;
  this.durabilityInterval = 2500;
  this.managementTimeout = 2500;
  this.configThrottle = 2500;
  this.connectionTimeout = 2500;
  this.nodeConnectionTimeout = 2500;
  this._encodeDoc = _defaultEncode;
  this._decodeDoc = _defaultDecode;

  var self = this;

  var connOpts = options.dsnObj;

  this.connected = null;
  process.nextTick(function() {
    if (connOpts.bucket !== 'invalid_bucket') {
      self.connected = true;
      self.emit('connect');
    } else {
      self.connected = false;
      self.emit('error', new Error('invalid bucket name'));
    }
  });

  this.waitQueue = [];
  this.on('connect', function() {
    for (var i = 0; i < this.waitQueue.length; ++i) {
      this.waitQueue[i][0].call(this);
    }
    this.waitQueue = [];
  });
  this.on('error', function(err) {
    for (var i = 0; i < this.waitQueue.length; ++i) {
      this.waitQueue[i][1].call(this, err,  null);
    }
    this.waitQueue = [];
  });
}
util.inherits(MockBucket, events.EventEmitter);

MockBucket.prototype.dump = function() {
  var out = {
    data: {},
    ddocs: this.ddocs
  };

  var keyspace = this.storage.items;
  for (var i in keyspace) {
    if (keyspace.hasOwnProperty(i)) {
      var hashspace = keyspace[i];
      if (hashspace[i]) {
        var data = hashspace[i];
        out.data[i] = {
          value: this._decodeDoc({value:data.value,flags:data.flags}),
          flags: data.flags,
          expiry: data.expiry,
          cas: data.cas
        };
      }
    }
  }

  console.log(util.inspect(out, {depth: 16}));
};

/* istanbul ignore next */
MockBucket.prototype.enableN1ql = function(uri) {
  throw new Error('not supported on mock');
};

MockBucket.prototype.manager = function() {
  return new BucketManager(this);
};

MockBucket.prototype.disconnect = function() {
  this.connected = false;
};

MockBucket.prototype.setTranscoder = function(encoder, decoder) {
  if (encoder) {
    this._encodeDoc = encoder;
  } else {
    this._encodeDoc = _defaultEncode;
  }
  if (decoder) {
    this._decodeDoc = decoder;
  } else {
    this._decodeDoc = _defaultDecode;
  }
};

MockBucket.prototype._maybeInvoke = function(fn, callback) {
  if (this.connected === true) {
    setImmediate(fn.bind(this));
  } else if (this.connected === false) {
    throw new Error('cannot perform operations on a shutdown bucket');
  } else {
    this.waitQueue.push([fn, callback]);
  }
};

MockBucket.prototype._isValidKey = function(key) {
  return typeof key === 'string' || key instanceof Buffer;
};

MockBucket.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.');
    }
  }
};

MockBucket.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.');
    }
  }
};

MockBucket.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.');
    }
  }
};

MockBucket.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.');
    }
  }
};

MockBucket.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(function() {
    if (!options.hashkey) {
      options.hashkey = key;
    }

    var origItem = this.storage.get(key, options.hashkey);
    if (!origItem) {
      return callback(new CbError('key not found', errs.keyNotFound), null);
    }
    var decValue = this._decodeDoc({value:origItem.value,flags:origItem.flags});
    if (origItem.lockExpiry && origItem.lockExpiry > new Date()) {
      // If the key is locked, the server actually responds with a -1 cas value
      //   which is considered special, here we just make a fake cas.
      callback(null, {
        value: decValue,
        cas: _createCas()
      });
    } else {
      callback(null, {
        value: decValue,
        cas: origItem.cas
      });
    }
  }, callback);
};

MockBucket.prototype.getMulti = function(keys, callback) {
  if (!Array.isArray(keys) || keys.length === 0) {
    throw new TypeError('First argument needs to be an array of non-zero length.');
  }
  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]);
  }
};

MockBucket.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(function() {
    if (!options.hashkey) {
      options.hashkey = key;
    }

    var origItem = this.storage.get(key, options.hashkey);
    if (!origItem) {
      return callback(new CbError('key not found', errs.keyNotFound), null);
    }
    if (origItem.lockExpiry && origItem.lockExpiry > new Date()) {
      return callback(new CbError(
          'temporary error - key locked', errs.temporaryError), null);
    }
    origItem.expiry = _makeExpiryDate(expiry);
    var decValue = this._decodeDoc({value:origItem.value,flags:origItem.flags});
    callback(null, {
      value: decValue,
      cas: origItem.cas
    });
  }, callback);
};

MockBucket.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(function() {
    if (!options.hashkey) {
      options.hashkey = key;
    }

    var origItem = this.storage.get(key, options.hashkey);
    if (!origItem) {
      return callback(new CbError('key not found', errs.keyNotFound), null);
    }
    if (origItem.lockExpiry && origItem.lockExpiry > new Date()) {
      return callback(new CbError(
          'temporary error - key locked', errs.temporaryError), null);
    }
    if (options.lockTime) {
      origItem.lockExpiry = _makeLockDate(options.lockTime);
    } else {
      origItem.lockExpiry = _makeLockDate(15);
    }
    origItem.cas = _createCas();
    var decValue = this._decodeDoc({value:origItem.value,flags:origItem.flags});
    callback(null, {
      value: decValue,
      cas: origItem.cas
    });
  }, callback);
};

MockBucket.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);

  /* istanbul ignore next */
  this._maybeInvoke(function() {
    if (!options.hashkey) {
      options.hashkey = key;
    }

    var origItem = this.storage.get(key, options.hashkey);
    if (!origItem) {
      return callback(new CbError('key not found', errs.keyNotFound), null);
    }
    var decValue = this._decodeDoc({value:origItem.value,flags:origItem.flags});
    callback(null, {
      value: decValue,
      cas: origItem.cas
    });
  }, callback);
};

MockBucket.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(function() {
    if (!options.hashkey) {
      options.hashkey = key;
    }

    var origItem = this.storage.get(key, options.hashkey);
    if (!origItem) {
      return callback(new CbError('key not found', errs.keyNotFound), null);
    }
    if (origItem.lockExpiry && origItem.lockExpiry > new Date()) {
      return callback(new CbError(
          'temporary error - key locked', errs.temporaryError), null);
    }
    origItem.expiry = _makeExpiryDate(expiry);
    callback(null, {
      cas: origItem.cas
    });
  }, callback);
};

MockBucket.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(function() {
    if (!options.hashkey) {
      options.hashkey = key;
    }

    var origItem = this.storage.get(key, options.hashkey);
    if (!origItem) {
      return callback(new CbError('key not found', errs.keyNotFound), null);
    }
    if (!_compareCas(origItem.cas, cas)) {
      return callback(new CbError(
          'cas does not match', errs.keyAlreadyExists), null);
    }
    if (!origItem.lockExpiry || origItem.lockExpiry <= new Date()) {
      return callback(new CbError(
          'key not locked', errs.temporaryError), null);
    }
    origItem.lockExpiry = null;
    callback(null, {
      cas: origItem.cas
    });
  }, callback);
};

MockBucket.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(function() {
    if (!options.hashkey) {
      options.hashkey = key;
    }

    var origItem = this.storage.get(key, options.hashkey);
    if (!origItem) {
      return callback(new CbError('key not found', errs.keyNotFound), null);
    }
    if (origItem.lockExpiry && origItem.lockExpiry > new Date()) {
      return callback(new CbError(
          'temporary error - key locked', errs.temporaryError), null);
    }
    var delCas = this.storage.remove(key, options.hashkey);
    callback(null, {
      cas: delCas
    });
  }, callback);
};

MockBucket.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(function() {
    /*
    The following is intentionally verbose and repeated to make it
    easier to see test-coverage of different paths that would occur
    on the server.
     */
    if (opType === 'set') {
      if (!options.hashkey) {
        options.hashkey = key;
      }
      var origSetItem = this.storage.get(key, options.hashkey);
      if (origSetItem && origSetItem.lockExpiry &&
          origSetItem.lockExpiry > new Date() && !options.cas) {
        return callback(new CbError(
            'temporary error - key locked', errs.temporaryError), null);
      }
      if (origSetItem && !_compareCas(origSetItem.cas, options.cas)) {
        return callback(new CbError(
            'cas mismatch', errs.keyAlreadyExists), null);
      }

      var encItemSet = this._encodeDoc(value);
      var newSetItem = {
        value: encItemSet.value,
        flags: encItemSet.flags,
        expiry: _makeExpiryDate(options.expiry),
        cas: _createCas()
      };
      this.storage.set(key, options.hashkey, newSetItem);
      callback(null, {cas: newSetItem.cas});
    } else if (opType === 'add') {
      if (!options.hashkey) {
        options.hashkey = key;
      }
      var origAddItem = this.storage.get(key, options.hashkey);
      if (origAddItem) {
        return callback(new CbError(
            'key already exists', errs.keyAlreadyExists), null);
      }

      var encItemAdd = this._encodeDoc(value);
      var newAddItem = {
        value: encItemAdd.value,
        flags: encItemAdd.flags,
        expiry: _makeExpiryDate(options.expiry),
        cas: _createCas()
      };
      this.storage.set(key, options.hashkey, newAddItem);
      callback(null, {cas: newAddItem.cas});
    } else if (opType === 'replace') {
      if (!options.hashkey) {
        options.hashkey = key;
      }
      var origReplaceItem = this.storage.get(key, options.hashkey);
      if (!origReplaceItem) {
        return callback(new CbError(
            'key does not exist', errs.keyNotFound), null);
      }
      if (origReplaceItem.lockExpiry && origReplaceItem.lockExpiry > new Date() && !options.cas) {
        return callback(new CbError(
            'temporary error - key locked', errs.temporaryError), null);
      }

      if (origReplaceItem && !_compareCas(origReplaceItem.cas, options.cas)) {
        return callback(new CbError(
            'cas mismatch', errs.keyAlreadyExists), null);
      }

      var encItemReplace = this._encodeDoc(value);
      var newReplaceItem = {
        value: encItemReplace.value,
        flags: encItemReplace.flags,
        expiry: _makeExpiryDate(options.expiry),
        cas: _createCas()
      };
      this.storage.set(key, options.hashkey, newReplaceItem);
      callback(null, {cas: newReplaceItem.cas});
    } else if (opType === 'append') {
      if (!options.hashkey) {
        options.hashkey = key;
      }
      var origAppendItem = this.storage.get(key, options.hashkey);
      if (!origAppendItem) {
        return callback(new CbError(
            'key does not exist', errs.keyNotFound), null);
      }
      if (origAppendItem.lockExpiry && origAppendItem.lockExpiry > new Date()) {
        return callback(new CbError(
            'temporary error - key locked', errs.temporaryError), null);
      }

      var encValAppend = this._encodeDoc(value);
      origAppendItem.value = Buffer.concat([
          origAppendItem.value, encValAppend.value]);
      origAppendItem.cas = _createCas();

      callback(null, {
        cas: origAppendItem.cas
      });
    } else if (opType === 'prepend') {
      if (!options.hashkey) {
        options.hashkey = key;
      }
      var origPrependItem = this.storage.get(key, options.hashkey);
      if (!origPrependItem) {
        return callback(new CbError(
            'key does not exist', errs.keyNotFound), null);
      }
      if (origPrependItem.lockExpiry && origPrependItem.lockExpiry > new Date()) {
        return callback(new CbError(
            'temporary error - key locked', errs.temporaryError), null);
      }

      var encValPrepend = this._encodeDoc(value);
      origPrependItem.value = Buffer.concat([
        encValPrepend.value, origPrependItem.value]);
      origPrependItem.cas = _createCas();

      callback(null, {
        cas: origPrependItem.cas
      });
    }
  }, callback);
};

MockBucket.prototype.upsert = function(key, value, options, callback) {
  this._store(key, value, options, callback, 'set');
};

MockBucket.prototype.insert = function(key, value, options, callback) {
  this._store(key, value, options, callback, 'add');
};

MockBucket.prototype.replace = function(key, value, options, callback) {
  this._store(key, value, options, callback, 'replace');
};

MockBucket.prototype.append = function(key, fragment, options, callback) {
  this._store(key, fragment, options, callback, 'append');
};

MockBucket.prototype.prepend = function(key, fragment, options, callback) {
  this._store(key, fragment, options, callback, 'prepend');
};

MockBucket.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(function() {
    if (!options.hashkey) {
      options.hashkey = key;
    }
    var origCountItem = this.storage.get(key, options.hashkey);
    if (options.initial !== undefined && !origCountItem) {
      var newCountItem = {
        value: new Buffer(options.initial.toString(), 'utf8'),
        flags: 0,
        cas: _createCas()
      };
      this.storage.set(key, options.hashkey, newCountItem);
      return callback(null, {
        value: options.initial,
        cas: newCountItem.cas
      });
    }
    if (!origCountItem) {
      return callback(new CbError(
          'key does not exist', errs.keyNotFound), null);
    }
    if (origCountItem.lockExpiry && origCountItem.lockExpiry > new Date()) {
      return callback(new CbError(
          'temporary error - key locked', errs.temporaryError), null);
    }

    var strValue = origCountItem.value.toString('utf8');
    var numValue = parseInt(strValue, 10);
    numValue += delta;
    origCountItem.value = new Buffer(numValue.toString(), 'utf8');
    origCountItem.cas = _createCas();

    callback(null, {
      value: numValue,
      cas: origCountItem.cas
    });
  }, callback);
};

MockBucket.prototype.query = function(query, params, callback) {
  if (params instanceof Function) {
    callback = arguments[1];
    params = undefined;
  }

  if (query instanceof ViewQuery) {
    return this._view(query.ddoc, query.name, query.options, callback);
  } else if (query instanceof SpatialQuery) {
    throw new Error('Spatial queries are not supported in the mock.');
  } else if (query instanceof N1qlQuery) {
    throw new Error('N1QL queries are not supported in the mock.');
  } else {
    throw new TypeError(
        'First argument needs to be a ViewQuery, SpatialQuery or N1qlQuery.');
  }
};

Object.defineProperty(MockBucket.prototype, 'lcbVersion', {
  get: function() {
    return '0.0.0';
  },
  writeable: false
});

Object.defineProperty(MockBucket.prototype, 'clientVersion', {
  get: function() {
    return MOCK_VERSION;
  },
  writeable: false
});

function ViewQueryResponse(req) {
}
util.inherits(ViewQueryResponse, events.EventEmitter);

MockBucket.prototype._view = function(ddoc, name, q, callback) {
  var req = new ViewQueryResponse();

  var self = this;
  process.nextTick(function() {
    self._execView(ddoc, name, q, function(err, rows, meta) {
      if (err) {
        return req.emit('error', err);
      }
      for (var i = 0; i < rows.length; ++i) {
        req.emit('row', rows[i]);
      }
      req.emit('rows', rows, meta);
      req.emit('end', meta);
    });
  });

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

MockBucket.prototype._indexView = function(ddoc, name, options, callback) {
  var ddocObj = this.ddocs[ddoc];
  if (!ddocObj) {
    return callback(new Error('not_found'));
  }
  if (ddocObj.views) {
    ddocObj = ddocObj.views;
  }
  var viewObj = ddocObj[name];
  if (!viewObj) {
    return callback(new Error('not_found'));
  }
  var viewMapFunc = viewObj.map;

  var retvals = [];

  var curdocval = null;
  var curdockey = null;
  var curdocmeta = null;

  function dateToArray (date) {
    date = date.getUTCDate ? date : new Date(date);
    return isFinite(date.valueOf()) ?
      [date.getUTCFullYear(),
      (date.getUTCMonth() + 1),
      date.getUTCDate(),
      date.getUTCHours(),
      date.getUTCMinutes(),
      date.getUTCSeconds()] : null;
  }

  function emit(key, val) {
    var row = {
      key: key,
      id: curdockey,
      value: val,
      doc: {
        meta: curdocmeta
      }
    };
    if (curdocmeta.type === 'json') {
      row.doc.json = curdocval;
    } else {
      row.doc.base64 = curdocval;
    }
    retvals.push(row);
  }

  var procOne = function(doc,meta){};
  eval('procOne = ' + viewMapFunc);

  for (var keyName in this.storage.items) {
    if (this.storage.items.hasOwnProperty(keyName)) {
      var thisKey = this.storage.items[keyName];
      for (var hashKey in thisKey) {
        if (thisKey.hasOwnProperty(hashKey)) {
          var thisVal = thisKey[hashKey];

          curdockey = keyName;

          curdocval = null;
          try {
            curdocval = JSON.parse(thisVal.value.toString());
          } catch (e) {
          }

          var curdoctype = curdocval ? 'json' : 'base64';
          if (!curdocval) {
            curdocval = thisVal.value.toString('base64');
          }

          curdocmeta = {
            id: curdockey,
            rev: '?NOTVALIDFORMOCK?',
            expiration: thisVal.expiry ? thisVal.expiry : 0,
            flags: thisVal.flags,
            type: curdoctype
          };

          procOne(curdocval, curdocmeta);
        }
      }
    }
  }

  var reducer = viewObj.reduce;
  if (reducer) {
    if (reducer === '_count') {
      reducer = function(key, values, rereduce) {
        if (rereduce) {
          var result = 0;
          for (var i = 0; i < values.length; i++) {
            result += values[i];
          }
          return result;
        } else {
          return values.length;
        }
      };
    } else if (reducer === '_sum') {
      reducer = function(key, values, rereduce) {
        var sum = 0;
        for(var i = 0; i < values.length; i++) {
          sum = sum + values[i];
        }
        return(sum);
      };
    } else if (reducer === '_stats') {
      reducer = function(key, values, rereduce) {
        return null;
      };
    } else {
      eval('reducer = ' + reducer);
    }
  }

  callback(null, retvals, reducer);
};

// http://docs.couchdb.org/en/latest/couchapp/views/collation.html
var SORT_ORDER = function() {
  var ordered_array = [
    'null',
    'false',
    'true',
    'number',
    'string',
    'array',
    'object',
    'unknown'
  ];

  var obj = {};
  for (var i = 0; i < ordered_array.length; i++) {
    obj[ordered_array[i]] = i;
  }
  return obj;
}();

/**
 * Returns the sorting priority for a given type
 * @param v The value whose type should be evaluated
 * @return The numeric sorting index
 */
function getSortIndex(v) {
  if (typeof v === 'string') {
    return SORT_ORDER['string'];
  } else if (typeof v === 'number') {
    return SORT_ORDER['number'];
  } else if (Array.isArray(v)) {
    return SORT_ORDER['array'];
  } else if (v === true) {
    return SORT_ORDER['true'];
  } else if (v === false) {
    return SORT_ORDER['false'];
  } else if (v === null) {
    return SORT_ORDER['null'];
  } else if (typeof v === 'object') {
    return SORT_ORDER['object'];
  } else {
    return SORT_ORDER['unknown'];
  }
}

/**
 * Compares one value with another
 * @param a The first value
 * @param b The second value
 * @param [exact] If both @c b and @c b are arrays, setting this parameter to true
 * ensures that they will only be equal if their length matches and their
 * contents match. If this value is false (the default), then only the common
 * subset of elements are evaluated
 *
 * @return {number} greater than 0 if @c a is bigger than @b; a number less
 * than 0 if @a is less than @b, or 0 if they are equal
 */
function cbCompare(a, b, exact) {
  if (Array.isArray(a) && Array.isArray(b)) {
    if (exact === true) {
      if (a.length !== b.length) {
        return a.length > b.length ? +1 : -1;
      }
    }
    var maxLength = a.length > b.length ? b.length : a.length;
    for (var i = 0; i < maxLength; ++i) {
      var subCmp = cbCompare(a[i], b[i], true);
      if (subCmp !== 0) {
        return subCmp;
      }
    }
    return 0;
  }

  if (typeof a === 'string' && typeof b === 'string') {
    return a.localeCompare(b);
  }

  if (typeof a === 'number' && typeof b === 'number') {
    return a - b;
  }

  // Now we need to do special things
  var aPriority = getSortIndex(a);
  var bPriority = getSortIndex(b);
  if (aPriority !== bPriority) {
    return aPriority - bPriority;
  } else {
    if (a < b) {
      return -1;
    } else if (a > b) {
      return 1;
    } else {
      return 0;
    }
  }
}

/**
 * Find the index of @c val in the array @arr
 * @param arr The array to search in
 * @param val The value to search for
 * @return {number} the index in the array, or -1 if the item does not exist
 */
function cbIndexOf(arr, val) {
  for (var i = 0; i < arr.length; ++i) {
    if (cbCompare(arr[i], val, true) === 0) {
      return i;
    }
  }
  return -1;
}

/**
 * Normalize a key for reduce
 * @param key The key to normalize
 * @param groupLevel The group level
 * @return {*}
 */
function cbNormKey(key, groupLevel) {
  if (groupLevel === 0) {
    return null;
  }

  if (Array.isArray(key)) {
    if (groupLevel === -1) {
      return key;
    } else {
      return key.slice(0, groupLevel);
    }
  } else {
    return key;
  }
}

MockBucket.prototype._execView = function(ddoc, name, options, callback) {
  this._indexView(ddoc, name, options, function(err, results, reducer) {
    if (err) {
      return callback(err);
    }

    // Store total emitted rows
    var rowcount = results.length;

    // Parse if needed
    var startkey = options.startkey ? JSON.parse(options.startkey) : undefined;
    var startkey_docid = options.startkey_docid;
    var endkey = options.endkey ? JSON.parse(options.endkey) : undefined;
    var endkey_docid = options.endkey_docid;
    var group_level = options.group_level || 0;

    var inclusive_start = true;
    var inclusive_end = true;
    if (options.inclusive_end !== undefined) {
      inclusive_end = options.inclusive_end;
    }

    // Invert if descending
    if (options.descending) {
      var _startkey = startkey;
      startkey = endkey;
      endkey = _startkey;
      var _startkey_docid = startkey_docid;
      startkey_docid = endkey_docid;
      endkey_docid = _startkey_docid;
      var _inclusive_start = inclusive_start;
      inclusive_start = inclusive_end;
      inclusive_end = _inclusive_start;
    }

    var key = options.key ? JSON.parse(options.key) : undefined;
    var keys = options.keys ? JSON.parse(options.keys) : undefined;

    var newResults = [];
    for (var i = 0; i < results.length; ++i) {
      var dockey = results[i].key;
      var docid = results[i].id;

      if (key !== undefined) {
        if (cbCompare(dockey, key) !== 0) {
          continue;
        }
      }
      if (keys !== undefined) {
        if (cbIndexOf(keys, dockey) < 0) {
          continue;
        }
      }

      if (inclusive_start) {
        if (startkey && cbCompare(dockey, startkey) < 0) {
          continue;
        }
        if (startkey_docid && cbCompare(docid, startkey_docid) < 0) {
          continue;
        }
      } else {
        if (startkey && cbCompare(dockey, startkey) <= 0) {
          continue;
        }
        if (startkey_docid && cbCompare(docid, startkey_docid) <= 0) {
          continue;
        }
      }

      if (inclusive_end) {
        if (endkey && cbCompare(dockey, endkey) > 0) {
          continue;
        }
        if (endkey_docid && cbCompare(docid, endkey_docid) > 0) {
          continue;
        }
      } else {
        if (endkey && cbCompare(dockey, endkey) >= 0) {
          continue;
        }
        if (endkey_docid && cbCompare(docid, endkey_docid) >= 0) {
          continue;
        }
      }

      if (!options.include_docs) {
        delete results[i].doc;
      }

      newResults.push(results[i]);
    }
    results = newResults;

    if (options.descending) {
      results.sort(function(a,b){
        var compare = cbCompare(b.key, a.key);
        if (compare != 0) { return compare; }
        if (a.id > b.id) { return -1; }
        if (a.id < b.id) { return +1; }
        return 0;
      });
    } else {
      results.sort(function(a,b){
        var compare = cbCompare(a.key, b.key);
        if (compare != 0) { return compare; }
        if (b.id > a.id) { return -1; }
        if (b.id < a.id) { return +1; }
        return 0;
      });
    }

    if (options.skip && typeof options.skip !== 'number')
      return callback(new Error('query_parse_error: Invalid value for integer parameter: "' + options.skip.toString() + '"'));
    if (options.limit && typeof options.limit !== 'number')
      return callback(new Error('query_parse_error: Invalid value for integer parameter: "' + options.limit.toString() + '"'));

    if (options.skip && options.limit) {
      results = results.slice(options.skip, options.skip + options.limit);
    } else if (options.skip) {
      results = results.slice(options.skip);
    } else if (options.limit) {
      results = results.slice(0, options.limit);
    }

    // Reduce Time!!
    if (reducer && options.reduce !== false) {
      var keys = [];
      for (var i = 0; i < results.length; ++i) {
        var keyN = cbNormKey(results[i].key, group_level);
        if (cbIndexOf(keys, keyN) < 0) {
          keys.push(keyN);
        }
      }

      var newResults = [];
      for (var j = 0; j < keys.length; ++j) {
        var values = [];
        for (var k = 0; k < results.length; ++k) {
          var keyN = cbNormKey(results[k].key, group_level);
          if (cbCompare(keyN, keys[j]) === 0) {
            values.push(results[k].value);
          }
        }
        var result = reducer(keys[j], values, false);
        newResults.push({
          key: keys[j],
          value: result
        });
      }
      results = newResults;
    }

    var meta = {
      total_rows: rowcount
    };

    callback(null, results, meta);
  });
};

module.exports = MockBucket;
