text: import { extend } from 'js-extend';
  import Promise from 'pouchdb-promise';
  import { Map } from 'pouchdb-collections';
  import { EventEmitter } from 'events';
  import inherits from 'inherits';
  import Changes from './changes';
  import {
    pick,
    adapterFun,
    upsert,
    bulkGetShim,
    invalidIdError
  } from 'pouchdb-utils';
  import {
    traverseRevTree,
    collectLeaves,
    rootToLeaf,
    collectConflicts,
    isDeleted,
    isLocalId
  } from 'pouchdb-merge';
  import {
    MISSING_BULK_DOCS,
    MISSING_DOC,
    REV_CONFLICT,
    INVALID_ID,
    UNKNOWN_ERROR,
    QUERY_PARSE_ERROR,
    BAD_REQUEST,
    NOT_AN_OBJECT,
    INVALID_REV,
    createError
  } from 'pouchdb-errors';
  
  /*
   * A generic pouch adapter
   */
  
  function compare(left, right) {
    return left < right ? -1 : left > right ? 1 : 0;
  }
  
  // returns first element of arr satisfying callback predicate
  function arrayFirst(arr, callback) {
    for (var i = 0; i < arr.length; i++) {
      if (callback(arr[i], i) === true) {
        return arr[i];
      }
    }
  }
  
  // Wrapper for functions that call the bulkdocs api with a single doc,
  // if the first result is an error, return an error
  function yankError(callback) {
    return function (err, results) {
      if (err || (results[0] && results[0].error)) {
        callback(err || results[0]);
      } else {
        callback(null, results.length ? results[0]  : results);
      }
    };
  }
  
  // clean docs given to us by the user
  function cleanDocs(docs) {
    for (var i = 0; i < docs.length; i++) {
      var doc = docs[i];
      if (doc._deleted) {
        delete doc._attachments; // ignore atts for deleted docs
      } else if (doc._attachments) {
        // filter out extraneous keys from _attachments
        var atts = Object.keys(doc._attachments);
        for (var j = 0; j < atts.length; j++) {
          var att = atts[j];
          doc._attachments[att] = pick(doc._attachments[att],
            ['data', 'digest', 'content_type', 'length', 'revpos', 'stub']);
        }
      }
    }
  }
  
  // compare two docs, first by _id then by _rev
  function compareByIdThenRev(a, b) {
    var idCompare = compare(a._id, b._id);
    if (idCompare !== 0) {
      return idCompare;
    }
    var aStart = a._revisions ? a._revisions.start : 0;
    var bStart = b._revisions ? b._revisions.start : 0;
    return compare(aStart, bStart);
  }
  
  // for every node in a revision tree computes its distance from the closest
  // leaf
  function computeHeight(revs) {
    var height = {};
    var edges = [];
    traverseRevTree(revs, function (isLeaf, pos, id, prnt) {
      var rev = pos + "-" + id;
      if (isLeaf) {
        height[rev] = 0;
      }
      if (prnt !== undefined) {
        edges.push({from: prnt, to: rev});
      }
      return rev;
    });
  
    edges.reverse();
    edges.forEach(function (edge) {
      if (height[edge.from] === undefined) {
        height[edge.from] = 1 + height[edge.to];
      } else {
        height[edge.from] = Math.min(height[edge.from], 1 + height[edge.to]);
      }
    });
    return height;
  }
  
  function allDocsKeysQuery(api, opts, callback) {
    var keys =  ('limit' in opts) ?
        opts.keys.slice(opts.skip, opts.limit + opts.skip) :
        (opts.skip > 0) ? opts.keys.slice(opts.skip) : opts.keys;
    if (opts.descending) {
      keys.reverse();
    }
    if (!keys.length) {
      return api._allDocs({limit: 0}, callback);
    }
    var finalResults = {
      offset: opts.skip
    };
    return Promise.all(keys.map(function (key) {
      var subOpts = extend({key: key, deleted: 'ok'}, opts);
      ['limit', 'skip', 'keys'].forEach(function (optKey) {
        delete subOpts[optKey];
      });
      return new Promise(function (resolve, reject) {
        api._allDocs(subOpts, function (err, res) {
          /* istanbul ignore if */
          if (err) {
            return reject(err);
          }
          finalResults.total_rows = res.total_rows;
          resolve(res.rows[0] || {key: key, error: 'not_found'});
        });
      });
    })).then(function (results) {
      finalResults.rows = results;
      return finalResults;
    });
  }
  
  // all compaction is done in a queue, to avoid attaching
  // too many listeners at once
  function doNextCompaction(self) {
    var task = self._compactionQueue[0];
    var opts = task.opts;
    var callback = task.callback;
    self.get('_local/compaction').catch(function () {
      return false;
    }).then(function (doc) {
      if (doc && doc.last_seq) {
        opts.last_seq = doc.last_seq;
      }
      self._compact(opts, function (err, res) {
        /* istanbul ignore if */
        if (err) {
          callback(err);
        } else {
          callback(null, res);
        }
        process.nextTick(function () {
          self._compactionQueue.shift();
          if (self._compactionQueue.length) {
            doNextCompaction(self);
          }
        });
      });
    });
  }
  
  function attachmentNameError(name) {
    if (name.charAt(0) === '_') {
      return name + 'is not a valid attachment name, attachment ' +
        'names cannot start with \'_\'';
    }
    return false;
  }
  
  inherits(AbstractPouchDB, EventEmitter);
  
  function AbstractPouchDB() {
    EventEmitter.call(this);
  }
  
  AbstractPouchDB.prototype.post =
    adapterFun('post', function (doc, opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    if (typeof doc !== 'object' || Array.isArray(doc)) {
      return callback(createError(NOT_AN_OBJECT));
    }
    this.bulkDocs({docs: [doc]}, opts, yankError(callback));
  });
  
  AbstractPouchDB.prototype.put = adapterFun('put', function (doc, opts, cb) {
    if (typeof opts === 'function') {
      cb = opts;
      opts = {};
    }
    if (typeof doc !== 'object' || Array.isArray(doc)) {
      return cb(createError(NOT_AN_OBJECT));
    }
    invalidIdError(doc._id);
    if (isLocalId(doc._id) && typeof this._putLocal === 'function') {
      if (doc._deleted) {
        return this._removeLocal(doc, cb);
      } else {
        return this._putLocal(doc, cb);
      }
    }
    this.bulkDocs({docs: [doc]}, opts, yankError(cb));
  });
  
  AbstractPouchDB.prototype.putAttachment =
    adapterFun('putAttachment', function (docId, attachmentId, rev,
                                                blob, type) {
    var api = this;
    if (typeof type === 'function') {
      type = blob;
      blob = rev;
      rev = null;
    }
    // Lets fix in https://github.com/pouchdb/pouchdb/issues/3267
    /* istanbul ignore if */
    if (typeof type === 'undefined') {
      type = blob;
      blob = rev;
      rev = null;
    }
  
    function createAttachment(doc) {
      var prevrevpos = '_rev' in doc ? parseInt(doc._rev, 10) : 0;
      doc._attachments = doc._attachments || {};
      doc._attachments[attachmentId] = {
        content_type: type,
        data: blob,
        revpos: ++prevrevpos
      };
      return api.put(doc);
    }
  
    return api.get(docId).then(function (doc) {
      if (doc._rev !== rev) {
        throw createError(REV_CONFLICT);
      }
  
      return createAttachment(doc);
    }, function (err) {
       // create new doc
      /* istanbul ignore else */
      if (err.reason === MISSING_DOC.message) {
        return createAttachment({_id: docId});
      } else {
        throw err;
      }
    });
  });
  
  AbstractPouchDB.prototype.removeAttachment =
    adapterFun('removeAttachment', function (docId, attachmentId, rev,
                                                   callback) {
    var self = this;
    self.get(docId, function (err, obj) {
      /* istanbul ignore if */
      if (err) {
        callback(err);
        return;
      }
      if (obj._rev !== rev) {
        callback(createError(REV_CONFLICT));
        return;
      }
      /* istanbul ignore if */
      if (!obj._attachments) {
        return callback();
      }
      delete obj._attachments[attachmentId];
      if (Object.keys(obj._attachments).length === 0) {
        delete obj._attachments;
      }
      self.put(obj, callback);
    });
  });
  
  AbstractPouchDB.prototype.remove =
    adapterFun('remove', function (docOrId, optsOrRev, opts, callback) {
    var doc;
    if (typeof optsOrRev === 'string') {
      // id, rev, opts, callback style
      doc = {
        _id: docOrId,
        _rev: optsOrRev
      };
      if (typeof opts === 'function') {
        callback = opts;
        opts = {};
      }
    } else {
      // doc, opts, callback style
      doc = docOrId;
      if (typeof optsOrRev === 'function') {
        callback = optsOrRev;
        opts = {};
      } else {
        callback = opts;
        opts = optsOrRev;
      }
    }
    opts = opts || {};
    opts.was_delete = true;
    var newDoc = {_id: doc._id, _rev: (doc._rev || opts.rev)};
    newDoc._deleted = true;
    if (isLocalId(newDoc._id) && typeof this._removeLocal === 'function') {
      return this._removeLocal(doc, callback);
    }
    this.bulkDocs({docs: [newDoc]}, opts, yankError(callback));
  });
  
  AbstractPouchDB.prototype.revsDiff =
    adapterFun('revsDiff', function (req, opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    var ids = Object.keys(req);
  
    if (!ids.length) {
      return callback(null, {});
    }
  
    var count = 0;
    var missing = new Map();
  
    function addToMissing(id, revId) {
      if (!missing.has(id)) {
        missing.set(id, {missing: []});
      }
      missing.get(id).missing.push(revId);
    }
  
    function processDoc(id, rev_tree) {
      // Is this fast enough? Maybe we should switch to a set simulated by a map
      var missingForId = req[id].slice(0);
      traverseRevTree(rev_tree, function (isLeaf, pos, revHash, ctx,
        opts) {
          var rev = pos + '-' + revHash;
          var idx = missingForId.indexOf(rev);
          if (idx === -1) {
            return;
          }
  
          missingForId.splice(idx, 1);
          /* istanbul ignore if */
          if (opts.status !== 'available') {
            addToMissing(id, rev);
          }
        });
  
      // Traversing the tree is synchronous, so now `missingForId` contains
      // revisions that were not found in the tree
      missingForId.forEach(function (rev) {
        addToMissing(id, rev);
      });
    }
  
    ids.map(function (id) {
      this._getRevisionTree(id, function (err, rev_tree) {
        if (err && err.status === 404 && err.message === 'missing') {
          missing.set(id, {missing: req[id]});
        } else if (err) {
          /* istanbul ignore next */
          return callback(err);
        } else {
          processDoc(id, rev_tree);
        }
  
        if (++count === ids.length) {
          // convert LazyMap to object
          var missingObj = {};
          missing.forEach(function (value, key) {
            missingObj[key] = value;
          });
          return callback(null, missingObj);
        }
      });
    }, this);
  });
  
  // _bulk_get API for faster replication, as described in
  // https://github.com/apache/couchdb-chttpd/pull/33
  // At the "abstract" level, it will just run multiple get()s in
  // parallel, because this isn't much of a performance cost
  // for local databases (except the cost of multiple transactions, which is
  // small). The http adapter overrides this in order
  // to do a more efficient single HTTP request.
  AbstractPouchDB.prototype.bulkGet =
    adapterFun('bulkGet', function (opts, callback) {
    bulkGetShim(this, opts, callback);
  });
  
  // compact one document and fire callback
  // by compacting we mean removing all revisions which
  // are further from the leaf in revision tree than max_height
  AbstractPouchDB.prototype.compactDocument =
    adapterFun('compactDocument', function (docId, maxHeight, callback) {
    var self = this;
    this._getRevisionTree(docId, function (err, revTree) {
      /* istanbul ignore if */
      if (err) {
        return callback(err);
      }
      var height = computeHeight(revTree);
      var candidates = [];
      var revs = [];
      Object.keys(height).forEach(function (rev) {
        if (height[rev] > maxHeight) {
          candidates.push(rev);
        }
      });
  
      traverseRevTree(revTree, function (isLeaf, pos, revHash, ctx, opts) {
        var rev = pos + '-' + revHash;
        if (opts.status === 'available' && candidates.indexOf(rev) !== -1) {
          revs.push(rev);
        }
      });
      self._doCompaction(docId, revs, callback);
    });
  });
  
  // compact the whole database using single document
  // compaction
  AbstractPouchDB.prototype.compact =
    adapterFun('compact', function (opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
  
    var self = this;
    opts = opts || {};
  
    self._compactionQueue = self._compactionQueue || [];
    self._compactionQueue.push({opts: opts, callback: callback});
    if (self._compactionQueue.length === 1) {
      doNextCompaction(self);
    }
  });
  AbstractPouchDB.prototype._compact = function (opts, callback) {
    var self = this;
    var changesOpts = {
      return_docs: false,
      last_seq: opts.last_seq || 0
    };
    var promises = [];
  
    function onChange(row) {
      promises.push(self.compactDocument(row.id, 0));
    }
    function onComplete(resp) {
      var lastSeq = resp.last_seq;
      Promise.all(promises).then(function () {
        return upsert(self, '_local/compaction', function deltaFunc(doc) {
          if (!doc.last_seq || doc.last_seq < lastSeq) {
            doc.last_seq = lastSeq;
            return doc;
          }
          return false; // somebody else got here first, don't update
        });
      }).then(function () {
        callback(null, {ok: true});
      }).catch(callback);
    }
    self.changes(changesOpts)
      .on('change', onChange)
      .on('complete', onComplete)
      .on('error', callback);
  };
  /* Begin api wrappers. Specific functionality to storage belongs in the
     _[method] */
  AbstractPouchDB.prototype.get =
    adapterFun('get', function (id, opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    if (typeof id !== 'string') {
      return callback(createError(INVALID_ID));
    }
    if (isLocalId(id) && typeof this._getLocal === 'function') {
      return this._getLocal(id, callback);
    }
    var leaves = [], self = this;
  
    function finishOpenRevs() {
      var result = [];
      var count = leaves.length;
      /* istanbul ignore if */
      if (!count) {
        return callback(null, result);
      }
      // order with open_revs is unspecified
      leaves.forEach(function (leaf) {
        self.get(id, {
          rev: leaf,
          revs: opts.revs,
          attachments: opts.attachments
        }, function (err, doc) {
          if (!err) {
            result.push({ok: doc});
          } else {
            result.push({missing: leaf});
          }
          count--;
          if (!count) {
            callback(null, result);
          }
        });
      });
    }
  
    if (opts.open_revs) {
      if (opts.open_revs === "all") {
        this._getRevisionTree(id, function (err, rev_tree) {
          if (err) {
            return callback(err);
          }
          leaves = collectLeaves(rev_tree).map(function (leaf) {
            return leaf.rev;
          });
          finishOpenRevs();
        });
      } else {
        if (Array.isArray(opts.open_revs)) {
          leaves = opts.open_revs;
          for (var i = 0; i < leaves.length; i++) {
            var l = leaves[i];
            // looks like it's the only thing couchdb checks
            if (!(typeof (l) === "string" && /^\d+-/.test(l))) {
              return callback(createError(INVALID_REV));
            }
          }
          finishOpenRevs();
        } else {
          return callback(createError(UNKNOWN_ERROR,
            'function_clause'));
        }
      }
      return; // open_revs does not like other options
    }
  
    return this._get(id, opts, function (err, result) {
      if (err) {
        return callback(err);
      }
  
      var doc = result.doc;
      var metadata = result.metadata;
      var ctx = result.ctx;
  
      if (opts.conflicts) {
        var conflicts = collectConflicts(metadata);
        if (conflicts.length) {
          doc._conflicts = conflicts;
        }
      }
  
      if (isDeleted(metadata, doc._rev)) {
        doc._deleted = true;
      }
  
      if (opts.revs || opts.revs_info) {
        var paths = rootToLeaf(metadata.rev_tree);
        var path = arrayFirst(paths, function (arr) {
          return arr.ids.map(function (x) { return x.id; })
            .indexOf(doc._rev.split('-')[1]) !== -1;
        });
  
        var indexOfRev = path.ids.map(function (x) {return x.id; })
          .indexOf(doc._rev.split('-')[1]) + 1;
        var howMany = path.ids.length - indexOfRev;
        path.ids.splice(indexOfRev, howMany);
        path.ids.reverse();
  
        if (opts.revs) {
          doc._revisions = {
            start: (path.pos + path.ids.length) - 1,
            ids: path.ids.map(function (rev) {
              return rev.id;
            })
          };
        }
        if (opts.revs_info) {
          var pos =  path.pos + path.ids.length;
          doc._revs_info = path.ids.map(function (rev) {
            pos--;
            return {
              rev: pos + '-' + rev.id,
              status: rev.opts.status
            };
          });
        }
      }
  
      if (opts.attachments && doc._attachments) {
        var attachments = doc._attachments;
        var count = Object.keys(attachments).length;
        if (count === 0) {
          return callback(null, doc);
        }
        Object.keys(attachments).forEach(function (key) {
          this._getAttachment(doc._id, key, attachments[key], {
            // Previously the revision handling was done in adapter.js
            // getAttachment, however since idb-next doesnt we need to
            // pass the rev through
            rev: doc._rev,
            binary: opts.binary,
            ctx: ctx
          }, function (err, data) {
            var att = doc._attachments[key];
            att.data = data;
            delete att.stub;
            delete att.length;
            if (!--count) {
              callback(null, doc);
            }
          });
        }, self);
      } else {
        if (doc._attachments) {
          for (var key in doc._attachments) {
            /* istanbul ignore else */
            if (doc._attachments.hasOwnProperty(key)) {
              doc._attachments[key].stub = true;
            }
          }
        }
        callback(null, doc);
      }
    });
  });
  
  // TODO: I dont like this, it forces an extra read for every
  // attachment read and enforces a confusing api between
  // adapter.js and the adapter implementation
  AbstractPouchDB.prototype.getAttachment =
    adapterFun('getAttachment', function (docId, attachmentId, opts,
                                                callback) {
    var self = this;
    if (opts instanceof Function) {
      callback = opts;
      opts = {};
    }
    this._get(docId, opts, function (err, res) {
      if (err) {
        return callback(err);
      }
      if (res.doc._attachments && res.doc._attachments[attachmentId]) {
        opts.ctx = res.ctx;
        opts.binary = true;
        self._getAttachment(docId, attachmentId,
                            res.doc._attachments[attachmentId], opts, callback);
      } else {
        return callback(createError(MISSING_DOC));
      }
    });
  });
  
  AbstractPouchDB.prototype.allDocs =
    adapterFun('allDocs', function (opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    opts.skip = typeof opts.skip !== 'undefined' ? opts.skip : 0;
    if (opts.start_key) {
      opts.startkey = opts.start_key;
    }
    if (opts.end_key) {
      opts.endkey = opts.end_key;
    }
    if ('keys' in opts) {
      if (!Array.isArray(opts.keys)) {
        return callback(new TypeError('options.keys must be an array'));
      }
      var incompatibleOpt =
        ['startkey', 'endkey', 'key'].filter(function (incompatibleOpt) {
        return incompatibleOpt in opts;
      })[0];
      if (incompatibleOpt) {
        callback(createError(QUERY_PARSE_ERROR,
          'Query parameter `' + incompatibleOpt +
          '` is not compatible with multi-get'
        ));
        return;
      }
      if (this.type() !== 'http') {
        return allDocsKeysQuery(this, opts, callback);
      }
    }
  
    return this._allDocs(opts, callback);
  });
  
  AbstractPouchDB.prototype.changes = function (opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    return new Changes(this, opts, callback);
  };
  
  AbstractPouchDB.prototype.close = adapterFun('close', function (callback) {
    this._closed = true;
    return this._close(callback);
  });
  
  AbstractPouchDB.prototype.info = adapterFun('info', function (callback) {
    var self = this;
    this._info(function (err, info) {
      if (err) {
        return callback(err);
      }
      // assume we know better than the adapter, unless it informs us
      info.db_name = info.db_name || self._db_name;
      info.auto_compaction = !!(self.auto_compaction && self.type() !== 'http');
      info.adapter = self.type();
      callback(null, info);
    });
  });
  
  AbstractPouchDB.prototype.id = adapterFun('id', function (callback) {
    return this._id(callback);
  });
  
  /* istanbul ignore next */
  AbstractPouchDB.prototype.type = function () {
    return (typeof this._type === 'function') ? this._type() : this.adapter;
  };
  
  AbstractPouchDB.prototype.bulkDocs =
    adapterFun('bulkDocs', function (req, opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
  
    opts = opts || {};
  
    if (Array.isArray(req)) {
      req = {
        docs: req
      };
    }
  
    if (!req || !req.docs || !Array.isArray(req.docs)) {
      return callback(createError(MISSING_BULK_DOCS));
    }
  
    for (var i = 0; i < req.docs.length; ++i) {
      if (typeof req.docs[i] !== 'object' || Array.isArray(req.docs[i])) {
        return callback(createError(NOT_AN_OBJECT));
      }
    }
  
    var attachmentError;
    req.docs.forEach(function (doc) {
      if (doc._attachments) {
        Object.keys(doc._attachments).forEach(function (name) {
          attachmentError = attachmentError || attachmentNameError(name);
        });
      }
    });
  
    if (attachmentError) {
      return callback(createError(BAD_REQUEST, attachmentError));
    }
  
    if (!('new_edits' in opts)) {
      if ('new_edits' in req) {
        opts.new_edits = req.new_edits;
      } else {
        opts.new_edits = true;
      }
    }
  
    if (!opts.new_edits && this.type() !== 'http') {
      // ensure revisions of the same doc are sorted, so that
      // the local adapter processes them correctly (#2935)
      req.docs.sort(compareByIdThenRev);
    }
  
    cleanDocs(req.docs);
  
    return this._bulkDocs(req, opts, function (err, res) {
      if (err) {
        return callback(err);
      }
      if (!opts.new_edits) {
        // this is what couch does when new_edits is false
        res = res.filter(function (x) {
          return x.error;
        });
      }
      callback(null, res);
    });
  });
  
  AbstractPouchDB.prototype.registerDependentDatabase =
    adapterFun('registerDependentDatabase', function (dependentDb,
                                                            callback) {
    var depDB = new this.constructor(dependentDb, this.__opts);
  
    function diffFun(doc) {
      doc.dependentDbs = doc.dependentDbs || {};
      if (doc.dependentDbs[dependentDb]) {
        return false; // no update required
      }
      doc.dependentDbs[dependentDb] = true;
      return doc;
    }
    upsert(this, '_local/_pouch_dependentDbs', diffFun)
      .then(function () {
        callback(null, {db: depDB});
      }).catch(callback);
  });
  
  AbstractPouchDB.prototype.destroy =
    adapterFun('destroy', function (opts, callback) {
  
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
  
    var self = this;
    var usePrefix = 'use_prefix' in self ? self.use_prefix : true;
  
    function destroyDb() {
      // call destroy method of the particular adaptor
      self._destroy(opts, function (err, resp) {
        if (err) {
          return callback(err);
        }
        self._destroyed = true;
        self.emit('destroyed');
        callback(null, resp || { 'ok': true });
      });
    }
  
    if (self.type() === 'http') {
      // no need to check for dependent DBs if it's a remote DB
      return destroyDb();
    }
  
    self.get('_local/_pouch_dependentDbs', function (err, localDoc) {
      if (err) {
        /* istanbul ignore if */
        if (err.status !== 404) {
          return callback(err);
        } else { // no dependencies
          return destroyDb();
        }
      }
      var dependentDbs = localDoc.dependentDbs;
      var PouchDB = self.constructor;
      var deletedMap = Object.keys(dependentDbs).map(function (name) {
        // use_prefix is only false in the browser
        /* istanbul ignore next */
        var trueName = usePrefix ?
          name.replace(new RegExp('^' + PouchDB.prefix), '') : name;
        return new PouchDB(trueName, self.__opts).destroy();
      });
      Promise.all(deletedMap).then(destroyDb, callback);
    });
  });
  
  export default AbstractPouchDB;
  