text: import Promise from 'pouchdb-promise';
  import { assign, uuid, rev, invalidIdError, normalizeDdocFunctionName, parseDdocFunctionName } from 'pouchdb-utils';
  import { DOC_VALIDATION, INVALID_REV, createError, BAD_ARG, REV_CONFLICT, MISSING_DOC } from 'pouchdb-errors';
  import { atob, btoa, binaryStringToBlobOrBuffer, blobOrBufferToBinaryString, blobOrBufferToBase64 } from 'pouchdb-binary-utils';
  import { binaryMd5 } from 'pouchdb-md5';
  import { isDeleted, merge, winningRev, revExists, isLocalId } from 'pouchdb-merge';
  import { Map } from 'pouchdb-collections';
  
  function allDocsKeysQuery(api, opts) {
    var keys = opts.keys;
    var finalResults = {
      offset: opts.skip
    };
    return Promise.all(keys.map(function (key) {
      var subOpts = assign({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);
          }
          /* istanbul ignore if */
          if (opts.update_seq && res.update_seq !== undefined) {
            finalResults.update_seq = res.update_seq;
          }
          finalResults.total_rows = res.total_rows;
          resolve(res.rows[0] || {key: key, error: 'not_found'});
        });
      });
    })).then(function (results) {
      finalResults.rows = results;
      return finalResults;
    });
  }
  
  function toObject(array) {
    return array.reduce(function (obj, item) {
      obj[item] = true;
      return obj;
    }, {});
  }
  // List of top level reserved words for doc
  var reservedWords = toObject([
    '_id',
    '_rev',
    '_attachments',
    '_deleted',
    '_revisions',
    '_revs_info',
    '_conflicts',
    '_deleted_conflicts',
    '_local_seq',
    '_rev_tree',
    //replication documents
    '_replication_id',
    '_replication_state',
    '_replication_state_time',
    '_replication_state_reason',
    '_replication_stats',
    // Specific to Couchbase Sync Gateway
    '_removed'
  ]);
  
  // List of reserved words that should end up the document
  var dataWords = toObject([
    '_attachments',
    //replication documents
    '_replication_id',
    '_replication_state',
    '_replication_state_time',
    '_replication_state_reason',
    '_replication_stats'
  ]);
  
  function parseRevisionInfo(rev$$1) {
    if (!/^\d+-./.test(rev$$1)) {
      return createError(INVALID_REV);
    }
    var idx = rev$$1.indexOf('-');
    var left = rev$$1.substring(0, idx);
    var right = rev$$1.substring(idx + 1);
    return {
      prefix: parseInt(left, 10),
      id: right
    };
  }
  
  function makeRevTreeFromRevisions(revisions, opts) {
    var pos = revisions.start - revisions.ids.length + 1;
  
    var revisionIds = revisions.ids;
    var ids = [revisionIds[0], opts, []];
  
    for (var i = 1, len = revisionIds.length; i < len; i++) {
      ids = [revisionIds[i], {status: 'missing'}, [ids]];
    }
  
    return [{
      pos: pos,
      ids: ids
    }];
  }
  
  // Preprocess documents, parse their revisions, assign an id and a
  // revision for new writes that are missing them, etc
  function parseDoc(doc, newEdits) {
  
    var nRevNum;
    var newRevId;
    var revInfo;
    var opts = {status: 'available'};
    if (doc._deleted) {
      opts.deleted = true;
    }
  
    if (newEdits) {
      if (!doc._id) {
        doc._id = uuid();
      }
      newRevId = rev();
      if (doc._rev) {
        revInfo = parseRevisionInfo(doc._rev);
        if (revInfo.error) {
          return revInfo;
        }
        doc._rev_tree = [{
          pos: revInfo.prefix,
          ids: [revInfo.id, {status: 'missing'}, [[newRevId, opts, []]]]
        }];
        nRevNum = revInfo.prefix + 1;
      } else {
        doc._rev_tree = [{
          pos: 1,
          ids : [newRevId, opts, []]
        }];
        nRevNum = 1;
      }
    } else {
      if (doc._revisions) {
        doc._rev_tree = makeRevTreeFromRevisions(doc._revisions, opts);
        nRevNum = doc._revisions.start;
        newRevId = doc._revisions.ids[0];
      }
      if (!doc._rev_tree) {
        revInfo = parseRevisionInfo(doc._rev);
        if (revInfo.error) {
          return revInfo;
        }
        nRevNum = revInfo.prefix;
        newRevId = revInfo.id;
        doc._rev_tree = [{
          pos: nRevNum,
          ids: [newRevId, opts, []]
        }];
      }
    }
  
    invalidIdError(doc._id);
  
    doc._rev = nRevNum + '-' + newRevId;
  
    var result = {metadata : {}, data : {}};
    for (var key in doc) {
      /* istanbul ignore else */
      if (Object.prototype.hasOwnProperty.call(doc, key)) {
        var specialKey = key[0] === '_';
        if (specialKey && !reservedWords[key]) {
          var error = createError(DOC_VALIDATION, key);
          error.message = DOC_VALIDATION.message + ': ' + key;
          throw error;
        } else if (specialKey && !dataWords[key]) {
          result.metadata[key.slice(1)] = doc[key];
        } else {
          result.data[key] = doc[key];
        }
      }
    }
    return result;
  }
  
  function parseBase64(data) {
    try {
      return atob(data);
    } catch (e) {
      var err = createError(BAD_ARG,
        'Attachment is not a valid base64 string');
      return {error: err};
    }
  }
  
  function preprocessString(att, blobType, callback) {
    var asBinary = parseBase64(att.data);
    if (asBinary.error) {
      return callback(asBinary.error);
    }
  
    att.length = asBinary.length;
    if (blobType === 'blob') {
      att.data = binaryStringToBlobOrBuffer(asBinary, att.content_type);
    } else if (blobType === 'base64') {
      att.data = btoa(asBinary);
    } else { // binary
      att.data = asBinary;
    }
    binaryMd5(asBinary, function (result) {
      att.digest = 'md5-' + result;
      callback();
    });
  }
  
  function preprocessBlob(att, blobType, callback) {
    binaryMd5(att.data, function (md5) {
      att.digest = 'md5-' + md5;
      // size is for blobs (browser), length is for buffers (node)
      att.length = att.data.size || att.data.length || 0;
      if (blobType === 'binary') {
        blobOrBufferToBinaryString(att.data, function (binString) {
          att.data = binString;
          callback();
        });
      } else if (blobType === 'base64') {
        blobOrBufferToBase64(att.data, function (b64) {
          att.data = b64;
          callback();
        });
      } else {
        callback();
      }
    });
  }
  
  function preprocessAttachment(att, blobType, callback) {
    if (att.stub) {
      return callback();
    }
    if (typeof att.data === 'string') { // input is a base64 string
      preprocessString(att, blobType, callback);
    } else { // input is a blob
      preprocessBlob(att, blobType, callback);
    }
  }
  
  function preprocessAttachments(docInfos, blobType, callback) {
  
    if (!docInfos.length) {
      return callback();
    }
  
    var docv = 0;
    var overallErr;
  
    docInfos.forEach(function (docInfo) {
      var attachments = docInfo.data && docInfo.data._attachments ?
        Object.keys(docInfo.data._attachments) : [];
      var recv = 0;
  
      if (!attachments.length) {
        return done();
      }
  
      function processedAttachment(err) {
        overallErr = err;
        recv++;
        if (recv === attachments.length) {
          done();
        }
      }
  
      for (var key in docInfo.data._attachments) {
        if (docInfo.data._attachments.hasOwnProperty(key)) {
          preprocessAttachment(docInfo.data._attachments[key],
            blobType, processedAttachment);
        }
      }
    });
  
    function done() {
      docv++;
      if (docInfos.length === docv) {
        if (overallErr) {
          callback(overallErr);
        } else {
          callback();
        }
      }
    }
  }
  
  function updateDoc(revLimit, prev, docInfo, results,
                     i, cb, writeDoc, newEdits) {
  
    if (revExists(prev.rev_tree, docInfo.metadata.rev)) {
      results[i] = docInfo;
      return cb();
    }
  
    // sometimes this is pre-calculated. historically not always
    var previousWinningRev = prev.winningRev || winningRev(prev);
    var previouslyDeleted = 'deleted' in prev ? prev.deleted :
      isDeleted(prev, previousWinningRev);
    var deleted = 'deleted' in docInfo.metadata ? docInfo.metadata.deleted :
      isDeleted(docInfo.metadata);
    var isRoot = /^1-/.test(docInfo.metadata.rev);
  
    if (previouslyDeleted && !deleted && newEdits && isRoot) {
      var newDoc = docInfo.data;
      newDoc._rev = previousWinningRev;
      newDoc._id = docInfo.metadata.id;
      docInfo = parseDoc(newDoc, newEdits);
    }
  
    var merged = merge(prev.rev_tree, docInfo.metadata.rev_tree[0], revLimit);
  
    var inConflict = newEdits && ((
      (previouslyDeleted && deleted && merged.conflicts !== 'new_leaf') ||
      (!previouslyDeleted && merged.conflicts !== 'new_leaf') ||
      (previouslyDeleted && !deleted && merged.conflicts === 'new_branch')));
  
    if (inConflict) {
      var err = createError(REV_CONFLICT);
      results[i] = err;
      return cb();
    }
  
    var newRev = docInfo.metadata.rev;
    docInfo.metadata.rev_tree = merged.tree;
    docInfo.stemmedRevs = merged.stemmedRevs || [];
    /* istanbul ignore else */
    if (prev.rev_map) {
      docInfo.metadata.rev_map = prev.rev_map; // used only by leveldb
    }
  
    // recalculate
    var winningRev$$1 = winningRev(docInfo.metadata);
    var winningRevIsDeleted = isDeleted(docInfo.metadata, winningRev$$1);
  
    // calculate the total number of documents that were added/removed,
    // from the perspective of total_rows/doc_count
    var delta = (previouslyDeleted === winningRevIsDeleted) ? 0 :
      previouslyDeleted < winningRevIsDeleted ? -1 : 1;
  
    var newRevIsDeleted;
    if (newRev === winningRev$$1) {
      // if the new rev is the same as the winning rev, we can reuse that value
      newRevIsDeleted = winningRevIsDeleted;
    } else {
      // if they're not the same, then we need to recalculate
      newRevIsDeleted = isDeleted(docInfo.metadata, newRev);
    }
  
    writeDoc(docInfo, winningRev$$1, winningRevIsDeleted, newRevIsDeleted,
      true, delta, i, cb);
  }
  
  function rootIsMissing(docInfo) {
    return docInfo.metadata.rev_tree[0].ids[1].status === 'missing';
  }
  
  function processDocs(revLimit, docInfos, api, fetchedDocs, tx, results,
                       writeDoc, opts, overallCallback) {
  
    // Default to 1000 locally
    revLimit = revLimit || 1000;
  
    function insertDoc(docInfo, resultsIdx, callback) {
      // Cant insert new deleted documents
      var winningRev$$1 = winningRev(docInfo.metadata);
      var deleted = isDeleted(docInfo.metadata, winningRev$$1);
      if ('was_delete' in opts && deleted) {
        results[resultsIdx] = createError(MISSING_DOC, 'deleted');
        return callback();
      }
  
      // 4712 - detect whether a new document was inserted with a _rev
      var inConflict = newEdits && rootIsMissing(docInfo);
  
      if (inConflict) {
        var err = createError(REV_CONFLICT);
        results[resultsIdx] = err;
        return callback();
      }
  
      var delta = deleted ? 0 : 1;
  
      writeDoc(docInfo, winningRev$$1, deleted, deleted, false,
        delta, resultsIdx, callback);
    }
  
    var newEdits = opts.new_edits;
    var idsToDocs = new Map();
  
    var docsDone = 0;
    var docsToDo = docInfos.length;
  
    function checkAllDocsDone() {
      if (++docsDone === docsToDo && overallCallback) {
        overallCallback();
      }
    }
  
    docInfos.forEach(function (currentDoc, resultsIdx) {
  
      if (currentDoc._id && isLocalId(currentDoc._id)) {
        var fun = currentDoc._deleted ? '_removeLocal' : '_putLocal';
        api[fun](currentDoc, {ctx: tx}, function (err, res) {
          results[resultsIdx] = err || res;
          checkAllDocsDone();
        });
        return;
      }
  
      var id = currentDoc.metadata.id;
      if (idsToDocs.has(id)) {
        docsToDo--; // duplicate
        idsToDocs.get(id).push([currentDoc, resultsIdx]);
      } else {
        idsToDocs.set(id, [[currentDoc, resultsIdx]]);
      }
    });
  
    // in the case of new_edits, the user can provide multiple docs
    // with the same id. these need to be processed sequentially
    idsToDocs.forEach(function (docs, id) {
      var numDone = 0;
  
      function docWritten() {
        if (++numDone < docs.length) {
          nextDoc();
        } else {
          checkAllDocsDone();
        }
      }
      function nextDoc() {
        var value = docs[numDone];
        var currentDoc = value[0];
        var resultsIdx = value[1];
  
        if (fetchedDocs.has(id)) {
          updateDoc(revLimit, fetchedDocs.get(id), currentDoc, results,
            resultsIdx, docWritten, writeDoc, newEdits);
        } else {
          // Ensure stemming applies to new writes as well
          var merged = merge([], currentDoc.metadata.rev_tree[0], revLimit);
          currentDoc.metadata.rev_tree = merged.tree;
          currentDoc.stemmedRevs = merged.stemmedRevs || [];
          insertDoc(currentDoc, resultsIdx, docWritten);
        }
      }
      nextDoc();
    });
  }
  
  export { allDocsKeysQuery, invalidIdError, isDeleted, isLocalId, normalizeDdocFunctionName, parseDdocFunctionName, parseDoc, preprocessAttachments, processDocs, updateDoc };
  