'use strict';

/**
 * @fileOverview GridFS is a tool for MongoDB to store files to the database.
 * Because of the restrictions of the object size the database can hold, a
 * facility to split a file into several chunks is needed. The {@link GridStore}
 * class offers a simplified api to interact with files while managing the
 * chunks of split files behind the scenes. More information about GridFS can be
 * found <a href="http://www.mongodb.org/display/DOCS/GridFS">here</a>.
 *
 * @example
 * const MongoClient = require('mongodb').MongoClient;
 * const GridStore = require('mongodb').GridStore;
 * const ObjectID = require('mongodb').ObjectID;
 * const test = require('assert');
 * // Connection url
 * const url = 'mongodb://localhost:27017';
 * // Database Name
 * const dbName = 'test';
 * // Connect using MongoClient
 * MongoClient.connect(url, function(err, client) {
 *   const db = client.db(dbName);
 *   const gridStore = new GridStore(db, null, "w");
 *   gridStore.open(function(err, gridStore) {
 *     gridStore.write("hello world!", function(err, gridStore) {
 *       gridStore.close(function(err, result) {
 *         // Let's read the file using object Id
 *         GridStore.read(db, result._id, function(err, data) {
 *           test.equal('hello world!', data);
 *           client.close();
 *           test.done();
 *         });
 *       });
 *     });
 *   });
 * });
 */
const Chunk = require('./chunk');
const ObjectID = require('../core').BSON.ObjectID;
const ReadPreference = require('../core').ReadPreference;
const Buffer = require('safe-buffer').Buffer;
const fs = require('fs');
const f = require('util').format;
const util = require('util');
const MongoError = require('../core').MongoError;
const inherits = util.inherits;
const Duplex = require('stream').Duplex;
const shallowClone = require('../utils').shallowClone;
const executeLegacyOperation = require('../utils').executeLegacyOperation;
const deprecate = require('util').deprecate;

var REFERENCE_BY_FILENAME = 0,
  REFERENCE_BY_ID = 1;

const deprecationFn = deprecate(() => {},
'GridStore is deprecated, and will be removed in a future version. Please use GridFSBucket instead');

/**
 * Namespace provided by the core module
 * @external Duplex
 */

/**
 * Create a new GridStore instance
 *
 * Modes
 *  - **"r"** - read only. This is the default mode.
 *  - **"w"** - write in truncate mode. Existing data will be overwritten.
 *
 * @class
 * @param {Db} db A database instance to interact with.
 * @param {object} [id] optional unique id for this file
 * @param {string} [filename] optional filename for this file, no unique constrain on the field
 * @param {string} mode set the mode for this file.
 * @param {object} [options] Optional settings.
 * @param {(number|string)} [options.w] **Deprecated** The write concern. Use writeConcern instead.
 * @param {number} [options.wtimeout] **Deprecated** The write concern timeout. Use writeConcern instead.
 * @param {boolean} [options.j=false] **Deprecated** Specify a journal write concern. Use writeConcern instead.
 * @param {boolean} [options.fsync=false] **Deprecated** Specify a file sync write concern. Use writeConcern instead.
 * @param {object|WriteConcern} [options.writeConcern] Specify write concern settings.
 * @param {string} [options.root] Root collection to use. Defaults to **{GridStore.DEFAULT_ROOT_COLLECTION}**.
 * @param {string} [options.content_type] MIME type of the file. Defaults to **{GridStore.DEFAULT_CONTENT_TYPE}**.
 * @param {number} [options.chunk_size=261120] Size for the chunk. Defaults to **{Chunk.DEFAULT_CHUNK_SIZE}**.
 * @param {object} [options.metadata] Arbitrary data the user wants to store.
 * @param {object} [options.promiseLibrary] A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible
 * @param {(ReadPreference|string)} [options.readPreference] The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST).
 * @property {number} chunkSize Get the gridstore chunk size.
 * @property {number} md5 The md5 checksum for this file.
 * @property {number} chunkNumber The current chunk number the gridstore has materialized into memory
 * @return {GridStore} a GridStore instance.
 * @deprecated Use GridFSBucket API instead
 */
var GridStore = function GridStore(db, id, filename, mode, options) {
  deprecationFn();
  if (!(this instanceof GridStore)) return new GridStore(db, id, filename, mode, options);
  this.db = db;

  // Handle options
  if (typeof options === 'undefined') options = {};
  // Handle mode
  if (typeof mode === 'undefined') {
    mode = filename;
    filename = undefined;
  } else if (typeof mode === 'object') {
    options = mode;
    mode = filename;
    filename = undefined;
  }

  if (id && id._bsontype === 'ObjectID') {
    this.referenceBy = REFERENCE_BY_ID;
    this.fileId = id;
    this.filename = filename;
  } else if (typeof filename === 'undefined') {
    this.referenceBy = REFERENCE_BY_FILENAME;
    this.filename = id;
    if (mode.indexOf('w') != null) {
      this.fileId = new ObjectID();
    }
  } else {
    this.referenceBy = REFERENCE_BY_ID;
    this.fileId = id;
    this.filename = filename;
  }

  // Set up the rest
  this.mode = mode == null ? 'r' : mode;
  this.options = options || {};

  // Opened
  this.isOpen = false;

  // Set the root if overridden
  this.root =
    this.options['root'] == null ? GridStore.DEFAULT_ROOT_COLLECTION : this.options['root'];
  this.position = 0;
  this.readPreference =
    this.options.readPreference || db.options.readPreference || ReadPreference.primary;
  this.writeConcern = _getWriteConcern(db, this.options);
  // Set default chunk size
  this.internalChunkSize =
    this.options['chunkSize'] == null ? Chunk.DEFAULT_CHUNK_SIZE : this.options['chunkSize'];

  // Get the promiseLibrary
  var promiseLibrary = this.options.promiseLibrary || Promise;

  // Set the promiseLibrary
  this.promiseLibrary = promiseLibrary;

  Object.defineProperty(this, 'chunkSize', {
    enumerable: true,
    get: function() {
      return this.internalChunkSize;
    },
    set: function(value) {
      if (!(this.mode[0] === 'w' && this.position === 0 && this.uploadDate == null)) {
        // eslint-disable-next-line no-self-assign
        this.internalChunkSize = this.internalChunkSize;
      } else {
        this.internalChunkSize = value;
      }
    }
  });

  Object.defineProperty(this, 'md5', {
    enumerable: true,
    get: function() {
      return this.internalMd5;
    }
  });

  Object.defineProperty(this, 'chunkNumber', {
    enumerable: true,
    get: function() {
      return this.currentChunk && this.currentChunk.chunkNumber
        ? this.currentChunk.chunkNumber
        : null;
    }
  });
};

/**
 * The callback format for the Gridstore.open method
 * @callback GridStore~openCallback
 * @param {MongoError} error An error instance representing the error during the execution.
 * @param {GridStore} gridStore The GridStore instance if the open method was successful.
 */

/**
 * Opens the file from the database and initialize this object. Also creates a
 * new one if file does not exist.
 *
 * @method
 * @param {object} [options] Optional settings
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~openCallback} [callback] this will be called after executing this method
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.open = function(options, callback) {
  if (typeof options === 'function') (callback = options), (options = {});
  options = options || {};

  if (this.mode !== 'w' && this.mode !== 'w+' && this.mode !== 'r') {
    throw MongoError.create({ message: 'Illegal mode ' + this.mode, driver: true });
  }

  return executeLegacyOperation(this.db.s.topology, open, [this, options, callback], {
    skipSessions: true
  });
};

var open = function(self, options, callback) {
  // Get the write concern
  var writeConcern = _getWriteConcern(self.db, self.options);

  // If we are writing we need to ensure we have the right indexes for md5's
  if (self.mode === 'w' || self.mode === 'w+') {
    // Get files collection
    var collection = self.collection();
    // Put index on filename
    collection.ensureIndex([['filename', 1]], writeConcern, function() {
      // Get chunk collection
      var chunkCollection = self.chunkCollection();
      // Make an unique index for compatibility with mongo-cxx-driver:legacy
      var chunkIndexOptions = shallowClone(writeConcern);
      chunkIndexOptions.unique = true;
      // Ensure index on chunk collection
      chunkCollection.ensureIndex(
        [
          ['files_id', 1],
          ['n', 1]
        ],
        chunkIndexOptions,
        function() {
          // Open the connection
          _open(self, writeConcern, function(err, r) {
            if (err) return callback(err);
            self.isOpen = true;
            callback(err, r);
          });
        }
      );
    });
  } else {
    // Open the gridstore
    _open(self, writeConcern, function(err, r) {
      if (err) return callback(err);
      self.isOpen = true;
      callback(err, r);
    });
  }
};

/**
 * Verify if the file is at EOF.
 *
 * @method
 * @return {boolean} true if the read/write head is at the end of this file.
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.eof = function() {
  return this.position === this.length ? true : false;
};

/**
 * The callback result format.
 * @callback GridStore~resultCallback
 * @param {object} [options] Optional settings
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {MongoError} error An error instance representing the error during the execution.
 * @param {object} result The result from the callback.
 */

/**
 * Retrieves a single character from this file.
 *
 * @method
 * @param {GridStore~resultCallback} [callback] this gets called after this method is executed. Passes null to the first parameter and the character read to the second or null to the second if the read/write head is at the end of the file.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.getc = function(options, callback) {
  if (typeof options === 'function') (callback = options), (options = {});
  options = options || {};

  return executeLegacyOperation(this.db.s.topology, getc, [this, options, callback], {
    skipSessions: true
  });
};

var getc = function(self, options, callback) {
  if (self.eof()) {
    callback(null, null);
  } else if (self.currentChunk.eof()) {
    nthChunk(self, self.currentChunk.chunkNumber + 1, function(err, chunk) {
      self.currentChunk = chunk;
      self.position = self.position + 1;
      callback(err, self.currentChunk.getc());
    });
  } else {
    self.position = self.position + 1;
    callback(null, self.currentChunk.getc());
  }
};

/**
 * Writes a string to the file with a newline character appended at the end if
 * the given string does not have one.
 *
 * @method
 * @param {string} string the string to write.
 * @param {object} [options] Optional settings
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~resultCallback} [callback] this will be called after executing this method. The first parameter will contain null and the second one will contain a reference to this object.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.puts = function(string, options, callback) {
  if (typeof options === 'function') (callback = options), (options = {});
  options = options || {};

  var finalString = string.match(/\n$/) == null ? string + '\n' : string;
  return executeLegacyOperation(
    this.db.s.topology,
    this.write.bind(this),
    [finalString, options, callback],
    { skipSessions: true }
  );
};

/**
 * Return a modified Readable stream including a possible transform method.
 *
 * @method
 * @return {GridStoreStream}
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.stream = function() {
  return new GridStoreStream(this);
};

/**
 * Writes some data. This method will work properly only if initialized with mode "w" or "w+".
 *
 * @method
 * @param {(string|Buffer)} data the data to write.
 * @param {boolean} [close] closes this file after writing if set to true.
 * @param {object} [options] Optional settings
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~resultCallback} [callback] this will be called after executing this method. The first parameter will contain null and the second one will contain a reference to this object.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.write = function write(data, close, options, callback) {
  if (typeof options === 'function') (callback = options), (options = {});
  options = options || {};

  return executeLegacyOperation(
    this.db.s.topology,
    _writeNormal,
    [this, data, close, options, callback],
    { skipSessions: true }
  );
};

/**
 * Handles the destroy part of a stream
 *
 * @method
 * @result {null}
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.destroy = function destroy() {
  // close and do not emit any more events. queued data is not sent.
  if (!this.writable) return;
  this.readable = false;
  if (this.writable) {
    this.writable = false;
    this._q.length = 0;
    this.emit('close');
  }
};

/**
 * Stores a file from the file system to the GridFS database.
 *
 * @method
 * @param {(string|Buffer|FileHandle)} file the file to store.
 * @param {object} [options] Optional settings
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~resultCallback} [callback] this will be called after executing this method. The first parameter will contain null and the second one will contain a reference to this object.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.writeFile = function(file, options, callback) {
  if (typeof options === 'function') (callback = options), (options = {});
  options = options || {};

  return executeLegacyOperation(this.db.s.topology, writeFile, [this, file, options, callback], {
    skipSessions: true
  });
};

var writeFile = function(self, file, options, callback) {
  if (typeof file === 'string') {
    fs.open(file, 'r', function(err, fd) {
      if (err) return callback(err);
      self.writeFile(fd, callback);
    });
    return;
  }

  self.open(function(err, self) {
    if (err) return callback(err, self);

    fs.fstat(file, function(err, stats) {
      if (err) return callback(err, self);

      var offset = 0;
      var index = 0;

      // Write a chunk
      var writeChunk = function() {
        // Allocate the buffer
        var _buffer = Buffer.alloc(self.chunkSize);
        // Read the file
        fs.read(file, _buffer, 0, _buffer.length, offset, function(err, bytesRead, data) {
          if (err) return callback(err, self);

          offset = offset + bytesRead;

          // Create a new chunk for the data
          var chunk = new Chunk(self, { n: index++ }, self.writeConcern);
          chunk.write(data.slice(0, bytesRead), function(err, chunk) {
            if (err) return callback(err, self);

            chunk.save({}, function(err) {
              if (err) return callback(err, self);

              self.position = self.position + bytesRead;

              // Point to current chunk
              self.currentChunk = chunk;

              if (offset >= stats.size) {
                fs.close(file, function(err) {
                  if (err) return callback(err);

                  self.close(function(err) {
                    if (err) return callback(err, self);
                    return callback(null, self);
                  });
                });
              } else {
                return process.nextTick(writeChunk);
              }
            });
          });
        });
      };

      // Process the first write
      process.nextTick(writeChunk);
    });
  });
};

/**
 * Saves this file to the database. This will overwrite the old entry if it
 * already exists. This will work properly only if mode was initialized to
 * "w" or "w+".
 *
 * @method
 * @param {object} [options] Optional settings
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~resultCallback} [callback] this will be called after executing this method. The first parameter will contain null and the second one will contain a reference to this object.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.close = function(options, callback) {
  if (typeof options === 'function') (callback = options), (options = {});
  options = options || {};

  return executeLegacyOperation(this.db.s.topology, close, [this, options, callback], {
    skipSessions: true
  });
};

var close = function(self, options, callback) {
  if (self.mode[0] === 'w') {
    // Set up options
    options = Object.assign({}, self.writeConcern, options);

    if (self.currentChunk != null && self.currentChunk.position > 0) {
      self.currentChunk.save({}, function(err) {
        if (err && typeof callback === 'function') return callback(err);

        self.collection(function(err, files) {
          if (err && typeof callback === 'function') return callback(err);

          // Build the mongo object
          if (self.uploadDate != null) {
            buildMongoObject(self, function(err, mongoObject) {
              if (err) {
                if (typeof callback === 'function') return callback(err);
                else throw err;
              }

              files.save(mongoObject, options, function(err) {
                if (typeof callback === 'function') callback(err, mongoObject);
              });
            });
          } else {
            self.uploadDate = new Date();
            buildMongoObject(self, function(err, mongoObject) {
              if (err) {
                if (typeof callback === 'function') return callback(err);
                else throw err;
              }

              files.save(mongoObject, options, function(err) {
                if (typeof callback === 'function') callback(err, mongoObject);
              });
            });
          }
        });
      });
    } else {
      self.collection(function(err, files) {
        if (err && typeof callback === 'function') return callback(err);

        self.uploadDate = new Date();
        buildMongoObject(self, function(err, mongoObject) {
          if (err) {
            if (typeof callback === 'function') return callback(err);
            else throw err;
          }

          files.save(mongoObject, options, function(err) {
            if (typeof callback === 'function') callback(err, mongoObject);
          });
        });
      });
    }
  } else if (self.mode[0] === 'r') {
    if (typeof callback === 'function') callback(null, null);
  } else {
    if (typeof callback === 'function')
      callback(MongoError.create({ message: f('Illegal mode %s', self.mode), driver: true }));
  }
};

/**
 * The collection callback format.
 * @callback GridStore~collectionCallback
 * @param {MongoError} error An error instance representing the error during the execution.
 * @param {Collection} collection The collection from the command execution.
 */

/**
 * Retrieve this file's chunks collection.
 *
 * @method
 * @param {GridStore~collectionCallback} callback the command callback.
 * @return {Collection}
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.chunkCollection = function(callback) {
  if (typeof callback === 'function') return this.db.collection(this.root + '.chunks', callback);
  return this.db.collection(this.root + '.chunks');
};

/**
 * Deletes all the chunks of this file in the database.
 *
 * @method
 * @param {object} [options] Optional settings
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~resultCallback} [callback] the command callback.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.unlink = function(options, callback) {
  if (typeof options === 'function') (callback = options), (options = {});
  options = options || {};

  return executeLegacyOperation(this.db.s.topology, unlink, [this, options, callback], {
    skipSessions: true
  });
};

var unlink = function(self, options, callback) {
  deleteChunks(self, function(err) {
    if (err !== null) {
      err.message = 'at deleteChunks: ' + err.message;
      return callback(err);
    }

    self.collection(function(err, collection) {
      if (err !== null) {
        err.message = 'at collection: ' + err.message;
        return callback(err);
      }

      collection.remove({ _id: self.fileId }, self.writeConcern, function(err) {
        callback(err, self);
      });
    });
  });
};

/**
 * Retrieves the file collection associated with this object.
 *
 * @method
 * @param {GridStore~collectionCallback} callback the command callback.
 * @return {Collection}
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.collection = function(callback) {
  if (typeof callback === 'function') this.db.collection(this.root + '.files', callback);
  return this.db.collection(this.root + '.files');
};

/**
 * The readlines callback format.
 * @callback GridStore~readlinesCallback
 * @param {MongoError} error An error instance representing the error during the execution.
 * @param {string[]} strings The array of strings returned.
 */

/**
 * Read the entire file as a list of strings splitting by the provided separator.
 *
 * @method
 * @param {string} [separator] The character to be recognized as the newline separator.
 * @param {object} [options] Optional settings
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~readlinesCallback} [callback] the command callback.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.readlines = function(separator, options, callback) {
  var args = Array.prototype.slice.call(arguments, 0);
  callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
  separator = args.length ? args.shift() : '\n';
  separator = separator || '\n';
  options = args.length ? args.shift() : {};

  return executeLegacyOperation(
    this.db.s.topology,
    readlines,
    [this, separator, options, callback],
    { skipSessions: true }
  );
};

var readlines = function(self, separator, options, callback) {
  self.read(function(err, data) {
    if (err) return callback(err);

    var items = data.toString().split(separator);
    items = items.length > 0 ? items.splice(0, items.length - 1) : [];
    for (var i = 0; i < items.length; i++) {
      items[i] = items[i] + separator;
    }

    callback(null, items);
  });
};

/**
 * Deletes all the chunks of this file in the database if mode was set to "w" or
 * "w+" and resets the read/write head to the initial position.
 *
 * @method
 * @param {object} [options] Optional settings
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~resultCallback} [callback] this will be called after executing this method. The first parameter will contain null and the second one will contain a reference to this object.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.rewind = function(options, callback) {
  if (typeof options === 'function') (callback = options), (options = {});
  options = options || {};

  return executeLegacyOperation(this.db.s.topology, rewind, [this, options, callback], {
    skipSessions: true
  });
};

var rewind = function(self, options, callback) {
  if (self.currentChunk.chunkNumber !== 0) {
    if (self.mode[0] === 'w') {
      deleteChunks(self, function(err) {
        if (err) return callback(err);
        self.currentChunk = new Chunk(self, { n: 0 }, self.writeConcern);
        self.position = 0;
        callback(null, self);
      });
    } else {
      self.currentChunk(0, function(err, chunk) {
        if (err) return callback(err);
        self.currentChunk = chunk;
        self.currentChunk.rewind();
        self.position = 0;
        callback(null, self);
      });
    }
  } else {
    self.currentChunk.rewind();
    self.position = 0;
    callback(null, self);
  }
};

/**
 * The read callback format.
 * @callback GridStore~readCallback
 * @param {MongoError} error An error instance representing the error during the execution.
 * @param {Buffer} data The data read from the GridStore object
 */

/**
 * Retrieves the contents of this file and advances the read/write head. Works with Buffers only.
 *
 * There are 3 signatures for this method:
 *
 * (callback)
 * (length, callback)
 * (length, buffer, callback)
 *
 * @method
 * @param {number} [length] the number of characters to read. Reads all the characters from the read/write head to the EOF if not specified.
 * @param {(string|Buffer)} [buffer] a string to hold temporary data. This is used for storing the string data read so far when recursively calling this method.
 * @param {object} [options] Optional settings
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~readCallback} [callback] the command callback.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.read = function(length, buffer, options, callback) {
  var args = Array.prototype.slice.call(arguments, 0);
  callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
  length = args.length ? args.shift() : null;
  buffer = args.length ? args.shift() : null;
  options = args.length ? args.shift() : {};

  return executeLegacyOperation(
    this.db.s.topology,
    read,
    [this, length, buffer, options, callback],
    { skipSessions: true }
  );
};

var read = function(self, length, buffer, options, callback) {
  // The data is a c-terminated string and thus the length - 1
  var finalLength = length == null ? self.length - self.position : length;
  var finalBuffer = buffer == null ? Buffer.alloc(finalLength) : buffer;
  // Add a index to buffer to keep track of writing position or apply current index
  finalBuffer._index = buffer != null && buffer._index != null ? buffer._index : 0;

  if (self.currentChunk.length() - self.currentChunk.position + finalBuffer._index >= finalLength) {
    var slice = self.currentChunk.readSlice(finalLength - finalBuffer._index);
    // Copy content to final buffer
    slice.copy(finalBuffer, finalBuffer._index);
    // Update internal position
    self.position = self.position + finalBuffer.length;
    // Check if we don't have a file at all
    if (finalLength === 0 && finalBuffer.length === 0)
      return callback(MongoError.create({ message: 'File does not exist', driver: true }), null);
    // Else return data
    return callback(null, finalBuffer);
  }

  // Read the next chunk
  slice = self.currentChunk.readSlice(self.currentChunk.length() - self.currentChunk.position);
  // Copy content to final buffer
  slice.copy(finalBuffer, finalBuffer._index);
  // Update index position
  finalBuffer._index += slice.length;

  // Load next chunk and read more
  nthChunk(self, self.currentChunk.chunkNumber + 1, function(err, chunk) {
    if (err) return callback(err);

    if (chunk.length() > 0) {
      self.currentChunk = chunk;
      self.read(length, finalBuffer, callback);
    } else {
      if (finalBuffer._index > 0) {
        callback(null, finalBuffer);
      } else {
        callback(
          MongoError.create({
            message: 'no chunks found for file, possibly corrupt',
            driver: true
          }),
          null
        );
      }
    }
  });
};

/**
 * The tell callback format.
 * @callback GridStore~tellCallback
 * @param {MongoError} error An error instance representing the error during the execution.
 * @param {number} position The current read position in the GridStore.
 */

/**
 * Retrieves the position of the read/write head of this file.
 *
 * @method
 * @param {number} [length] the number of characters to read. Reads all the characters from the read/write head to the EOF if not specified.
 * @param {(string|Buffer)} [buffer] a string to hold temporary data. This is used for storing the string data read so far when recursively calling this method.
 * @param {object} [options] Optional settings
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~tellCallback} [callback] the command callback.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.tell = function(callback) {
  var self = this;
  // We provided a callback leg
  if (typeof callback === 'function') return callback(null, this.position);
  // Return promise
  return new self.promiseLibrary(function(resolve) {
    resolve(self.position);
  });
};

/**
 * The tell callback format.
 * @callback GridStore~gridStoreCallback
 * @param {MongoError} error An error instance representing the error during the execution.
 * @param {GridStore} gridStore The gridStore.
 */

/**
 * Moves the read/write head to a new location.
 *
 * There are 3 signatures for this method
 *
 * Seek Location Modes
 *  - **GridStore.IO_SEEK_SET**, **(default)** set the position from the start of the file.
 *  - **GridStore.IO_SEEK_CUR**, set the position from the current position in the file.
 *  - **GridStore.IO_SEEK_END**, set the position from the end of the file.
 *
 * @method
 * @param {number} [position] the position to seek to
 * @param {number} [seekLocation] seek mode. Use one of the Seek Location modes.
 * @param {object} [options] Optional settings
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~gridStoreCallback} [callback] the command callback.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.prototype.seek = function(position, seekLocation, options, callback) {
  var args = Array.prototype.slice.call(arguments, 1);
  callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
  seekLocation = args.length ? args.shift() : null;
  options = args.length ? args.shift() : {};

  return executeLegacyOperation(
    this.db.s.topology,
    seek,
    [this, position, seekLocation, options, callback],
    { skipSessions: true }
  );
};

var seek = function(self, position, seekLocation, options, callback) {
  // Seek only supports read mode
  if (self.mode !== 'r') {
    return callback(
      MongoError.create({ message: 'seek is only supported for mode r', driver: true })
    );
  }

  var seekLocationFinal = seekLocation == null ? GridStore.IO_SEEK_SET : seekLocation;
  var finalPosition = position;
  var targetPosition = 0;

  // Calculate the position
  if (seekLocationFinal === GridStore.IO_SEEK_CUR) {
    targetPosition = self.position + finalPosition;
  } else if (seekLocationFinal === GridStore.IO_SEEK_END) {
    targetPosition = self.length + finalPosition;
  } else {
    targetPosition = finalPosition;
  }

  // Get the chunk
  var newChunkNumber = Math.floor(targetPosition / self.chunkSize);
  var seekChunk = function() {
    nthChunk(self, newChunkNumber, function(err, chunk) {
      if (err) return callback(err, null);
      if (chunk == null) return callback(new Error('no chunk found'));

      // Set the current chunk
      self.currentChunk = chunk;
      self.position = targetPosition;
      self.currentChunk.position = self.position % self.chunkSize;
      callback(err, self);
    });
  };

  seekChunk();
};

/**
 * @ignore
 */
var _open = function(self, options, callback) {
  var collection = self.collection();
  // Create the query
  var query =
    self.referenceBy === REFERENCE_BY_ID ? { _id: self.fileId } : { filename: self.filename };
  query = null == self.fileId && self.filename == null ? null : query;
  options.readPreference = self.readPreference;

  // Fetch the chunks
  if (query != null) {
    collection.findOne(query, options, function(err, doc) {
      if (err) {
        return error(err);
      }

      // Check if the collection for the files exists otherwise prepare the new one
      if (doc != null) {
        self.fileId = doc._id;
        // Prefer a new filename over the existing one if this is a write
        self.filename =
          self.mode === 'r' || self.filename === undefined ? doc.filename : self.filename;
        self.contentType = doc.contentType;
        self.internalChunkSize = doc.chunkSize;
        self.uploadDate = doc.uploadDate;
        self.aliases = doc.aliases;
        self.length = doc.length;
        self.metadata = doc.metadata;
        self.internalMd5 = doc.md5;
      } else if (self.mode !== 'r') {
        self.fileId = self.fileId == null ? new ObjectID() : self.fileId;
        self.contentType = GridStore.DEFAULT_CONTENT_TYPE;
        self.internalChunkSize =
          self.internalChunkSize == null ? Chunk.DEFAULT_CHUNK_SIZE : self.internalChunkSize;
        self.length = 0;
      } else {
        self.length = 0;
        var txtId = self.fileId._bsontype === 'ObjectID' ? self.fileId.toHexString() : self.fileId;
        return error(
          MongoError.create({
            message: f(
              'file with id %s not opened for writing',
              self.referenceBy === REFERENCE_BY_ID ? txtId : self.filename
            ),
            driver: true
          }),
          self
        );
      }

      // Process the mode of the object
      if (self.mode === 'r') {
        nthChunk(self, 0, options, function(err, chunk) {
          if (err) return error(err);
          self.currentChunk = chunk;
          self.position = 0;
          callback(null, self);
        });
      } else if (self.mode === 'w' && doc) {
        // Delete any existing chunks
        deleteChunks(self, options, function(err) {
          if (err) return error(err);
          self.currentChunk = new Chunk(self, { n: 0 }, self.writeConcern);
          self.contentType =
            self.options['content_type'] == null ? self.contentType : self.options['content_type'];
          self.internalChunkSize =
            self.options['chunk_size'] == null
              ? self.internalChunkSize
              : self.options['chunk_size'];
          self.metadata =
            self.options['metadata'] == null ? self.metadata : self.options['metadata'];
          self.aliases = self.options['aliases'] == null ? self.aliases : self.options['aliases'];
          self.position = 0;
          callback(null, self);
        });
      } else if (self.mode === 'w') {
        self.currentChunk = new Chunk(self, { n: 0 }, self.writeConcern);
        self.contentType =
          self.options['content_type'] == null ? self.contentType : self.options['content_type'];
        self.internalChunkSize =
          self.options['chunk_size'] == null ? self.internalChunkSize : self.options['chunk_size'];
        self.metadata = self.options['metadata'] == null ? self.metadata : self.options['metadata'];
        self.aliases = self.options['aliases'] == null ? self.aliases : self.options['aliases'];
        self.position = 0;
        callback(null, self);
      } else if (self.mode === 'w+') {
        nthChunk(self, lastChunkNumber(self), options, function(err, chunk) {
          if (err) return error(err);
          // Set the current chunk
          self.currentChunk = chunk == null ? new Chunk(self, { n: 0 }, self.writeConcern) : chunk;
          self.currentChunk.position = self.currentChunk.data.length();
          self.metadata =
            self.options['metadata'] == null ? self.metadata : self.options['metadata'];
          self.aliases = self.options['aliases'] == null ? self.aliases : self.options['aliases'];
          self.position = self.length;
          callback(null, self);
        });
      }
    });
  } else {
    // Write only mode
    self.fileId = null == self.fileId ? new ObjectID() : self.fileId;
    self.contentType = GridStore.DEFAULT_CONTENT_TYPE;
    self.internalChunkSize =
      self.internalChunkSize == null ? Chunk.DEFAULT_CHUNK_SIZE : self.internalChunkSize;
    self.length = 0;

    // No file exists set up write mode
    if (self.mode === 'w') {
      // Delete any existing chunks
      deleteChunks(self, options, function(err) {
        if (err) return error(err);
        self.currentChunk = new Chunk(self, { n: 0 }, self.writeConcern);
        self.contentType =
          self.options['content_type'] == null ? self.contentType : self.options['content_type'];
        self.internalChunkSize =
          self.options['chunk_size'] == null ? self.internalChunkSize : self.options['chunk_size'];
        self.metadata = self.options['metadata'] == null ? self.metadata : self.options['metadata'];
        self.aliases = self.options['aliases'] == null ? self.aliases : self.options['aliases'];
        self.position = 0;
        callback(null, self);
      });
    } else if (self.mode === 'w+') {
      nthChunk(self, lastChunkNumber(self), options, function(err, chunk) {
        if (err) return error(err);
        // Set the current chunk
        self.currentChunk = chunk == null ? new Chunk(self, { n: 0 }, self.writeConcern) : chunk;
        self.currentChunk.position = self.currentChunk.data.length();
        self.metadata = self.options['metadata'] == null ? self.metadata : self.options['metadata'];
        self.aliases = self.options['aliases'] == null ? self.aliases : self.options['aliases'];
        self.position = self.length;
        callback(null, self);
      });
    }
  }

  // only pass error to callback once
  function error(err) {
    if (error.err) return;
    callback((error.err = err));
  }
};

/**
 * @ignore
 */
var writeBuffer = function(self, buffer, close, callback) {
  if (typeof close === 'function') {
    callback = close;
    close = null;
  }
  var finalClose = typeof close === 'boolean' ? close : false;

  if (self.mode !== 'w') {
    callback(
      MongoError.create({
        message: f(
          'file with id %s not opened for writing',
          self.referenceBy === REFERENCE_BY_ID ? self.referenceBy : self.filename
        ),
        driver: true
      }),
      null
    );
  } else {
    if (self.currentChunk.position + buffer.length >= self.chunkSize) {
      // Write out the current Chunk and then keep writing until we have less data left than a chunkSize left
      // to a new chunk (recursively)
      var previousChunkNumber = self.currentChunk.chunkNumber;
      var leftOverDataSize = self.chunkSize - self.currentChunk.position;
      var firstChunkData = buffer.slice(0, leftOverDataSize);
      var leftOverData = buffer.slice(leftOverDataSize);
      // A list of chunks to write out
      var chunksToWrite = [self.currentChunk.write(firstChunkData)];
      // If we have more data left than the chunk size let's keep writing new chunks
      while (leftOverData.length >= self.chunkSize) {
        // Create a new chunk and write to it
        var newChunk = new Chunk(self, { n: previousChunkNumber + 1 }, self.writeConcern);
        firstChunkData = leftOverData.slice(0, self.chunkSize);
        leftOverData = leftOverData.slice(self.chunkSize);
        // Update chunk number
        previousChunkNumber = previousChunkNumber + 1;
        // Write data
        newChunk.write(firstChunkData);
        // Push chunk to save list
        chunksToWrite.push(newChunk);
      }

      // Set current chunk with remaining data
      self.currentChunk = new Chunk(self, { n: previousChunkNumber + 1 }, self.writeConcern);
      // If we have left over data write it
      if (leftOverData.length > 0) self.currentChunk.write(leftOverData);

      // Update the position for the gridstore
      self.position = self.position + buffer.length;
      // Total number of chunks to write
      var numberOfChunksToWrite = chunksToWrite.length;

      for (var i = 0; i < chunksToWrite.length; i++) {
        chunksToWrite[i].save({}, function(err) {
          if (err) return callback(err);

          numberOfChunksToWrite = numberOfChunksToWrite - 1;

          if (numberOfChunksToWrite <= 0) {
            // We care closing the file before returning
            if (finalClose) {
              return self.close(function(err) {
                callback(err, self);
              });
            }

            // Return normally
            return callback(null, self);
          }
        });
      }
    } else {
      // Update the position for the gridstore
      self.position = self.position + buffer.length;
      // We have less data than the chunk size just write it and callback
      self.currentChunk.write(buffer);
      // We care closing the file before returning
      if (finalClose) {
        return self.close(function(err) {
          callback(err, self);
        });
      }
      // Return normally
      return callback(null, self);
    }
  }
};

/**
 * Creates a mongoDB object representation of this object.
 *
 *        <pre><code>
 *        {
 *          '_id' : , // {number} id for this file
 *          'filename' : , // {string} name for this file
 *          'contentType' : , // {string} mime type for this file
 *          'length' : , // {number} size of this file?
 *          'chunksize' : , // {number} chunk size used by this file
 *          'uploadDate' : , // {Date}
 *          'aliases' : , // {array of string}
 *          'metadata' : , // {string}
 *        }
 *        </code></pre>
 *
 * @ignore
 */
var buildMongoObject = function(self, callback) {
  // Calcuate the length
  var mongoObject = {
    _id: self.fileId,
    filename: self.filename,
    contentType: self.contentType,
    length: self.position ? self.position : 0,
    chunkSize: self.chunkSize,
    uploadDate: self.uploadDate,
    aliases: self.aliases,
    metadata: self.metadata
  };

  var md5Command = { filemd5: self.fileId, root: self.root };
  self.db.command(md5Command, function(err, results) {
    if (err) return callback(err);

    mongoObject.md5 = results.md5;
    callback(null, mongoObject);
  });
};

/**
 * Gets the nth chunk of this file.
 * @ignore
 */
var nthChunk = function(self, chunkNumber, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = {};
  }

  options = options || self.writeConcern;
  options.readPreference = self.readPreference;
  // Get the nth chunk
  self
    .chunkCollection()
    .findOne({ files_id: self.fileId, n: chunkNumber }, options, function(err, chunk) {
      if (err) return callback(err);

      var finalChunk = chunk == null ? {} : chunk;
      callback(null, new Chunk(self, finalChunk, self.writeConcern));
    });
};

/**
 * @ignore
 */
var lastChunkNumber = function(self) {
  return Math.floor((self.length ? self.length - 1 : 0) / self.chunkSize);
};

/**
 * Deletes all the chunks of this file in the database.
 *
 * @ignore
 */
var deleteChunks = function(self, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = {};
  }

  options = options || self.writeConcern;

  if (self.fileId != null) {
    self.chunkCollection().remove({ files_id: self.fileId }, options, function(err) {
      if (err) return callback(err, false);
      callback(null, true);
    });
  } else {
    callback(null, true);
  }
};

/**
 * The collection to be used for holding the files and chunks collection.
 *
 * @classconstant DEFAULT_ROOT_COLLECTION
 */
GridStore.DEFAULT_ROOT_COLLECTION = 'fs';

/**
 * Default file mime type
 *
 * @classconstant DEFAULT_CONTENT_TYPE
 */
GridStore.DEFAULT_CONTENT_TYPE = 'binary/octet-stream';

/**
 * Seek mode where the given length is absolute.
 *
 * @classconstant IO_SEEK_SET
 */
GridStore.IO_SEEK_SET = 0;

/**
 * Seek mode where the given length is an offset to the current read/write head.
 *
 * @classconstant IO_SEEK_CUR
 */
GridStore.IO_SEEK_CUR = 1;

/**
 * Seek mode where the given length is an offset to the end of the file.
 *
 * @classconstant IO_SEEK_END
 */
GridStore.IO_SEEK_END = 2;

/**
 * Checks if a file exists in the database.
 *
 * @method
 * @static
 * @param {Db} db the database to query.
 * @param {string} name The name of the file to look for.
 * @param {string} [rootCollection] The root collection that holds the files and chunks collection. Defaults to **{GridStore.DEFAULT_ROOT_COLLECTION}**.
 * @param {object} [options] Optional settings.
 * @param {(ReadPreference|string)} [options.readPreference] The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST).
 * @param {object} [options.promiseLibrary] A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~resultCallback} [callback] result from exists.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.exist = function(db, fileIdObject, rootCollection, options, callback) {
  var args = Array.prototype.slice.call(arguments, 2);
  callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
  rootCollection = args.length ? args.shift() : null;
  options = args.length ? args.shift() : {};
  options = options || {};

  return executeLegacyOperation(
    db.s.topology,
    exists,
    [db, fileIdObject, rootCollection, options, callback],
    { skipSessions: true }
  );
};

var exists = function(db, fileIdObject, rootCollection, options, callback) {
  // Establish read preference
  var readPreference = options.readPreference || ReadPreference.PRIMARY;
  // Fetch collection
  var rootCollectionFinal =
    rootCollection != null ? rootCollection : GridStore.DEFAULT_ROOT_COLLECTION;
  db.collection(rootCollectionFinal + '.files', function(err, collection) {
    if (err) return callback(err);

    // Build query
    var query =
      typeof fileIdObject === 'string' ||
      Object.prototype.toString.call(fileIdObject) === '[object RegExp]'
        ? { filename: fileIdObject }
        : { _id: fileIdObject }; // Attempt to locate file

    // We have a specific query
    if (
      fileIdObject != null &&
      typeof fileIdObject === 'object' &&
      Object.prototype.toString.call(fileIdObject) !== '[object RegExp]'
    ) {
      query = fileIdObject;
    }

    // Check if the entry exists
    collection.findOne(query, { readPreference: readPreference }, function(err, item) {
      if (err) return callback(err);
      callback(null, item == null ? false : true);
    });
  });
};

/**
 * Gets the list of files stored in the GridFS.
 *
 * @method
 * @static
 * @param {Db} db the database to query.
 * @param {string} [rootCollection] The root collection that holds the files and chunks collection. Defaults to **{GridStore.DEFAULT_ROOT_COLLECTION}**.
 * @param {object} [options] Optional settings.
 * @param {(ReadPreference|string)} [options.readPreference] The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST).
 * @param {object} [options.promiseLibrary] A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~resultCallback} [callback] result from exists.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.list = function(db, rootCollection, options, callback) {
  var args = Array.prototype.slice.call(arguments, 1);
  callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
  rootCollection = args.length ? args.shift() : null;
  options = args.length ? args.shift() : {};
  options = options || {};

  return executeLegacyOperation(db.s.topology, list, [db, rootCollection, options, callback], {
    skipSessions: true
  });
};

var list = function(db, rootCollection, options, callback) {
  // Ensure we have correct values
  if (rootCollection != null && typeof rootCollection === 'object') {
    options = rootCollection;
    rootCollection = null;
  }

  // Establish read preference
  var readPreference = options.readPreference || ReadPreference.primary;
  // Check if we are returning by id not filename
  var byId = options['id'] != null ? options['id'] : false;
  // Fetch item
  var rootCollectionFinal =
    rootCollection != null ? rootCollection : GridStore.DEFAULT_ROOT_COLLECTION;
  var items = [];
  db.collection(rootCollectionFinal + '.files', function(err, collection) {
    if (err) return callback(err);

    collection.find({}, { readPreference: readPreference }, function(err, cursor) {
      if (err) return callback(err);

      cursor.each(function(err, item) {
        if (item != null) {
          items.push(byId ? item._id : item.filename);
        } else {
          callback(err, items);
        }
      });
    });
  });
};

/**
 * Reads the contents of a file.
 *
 * This method has the following signatures
 *
 * (db, name, callback)
 * (db, name, length, callback)
 * (db, name, length, offset, callback)
 * (db, name, length, offset, options, callback)
 *
 * @method
 * @static
 * @param {Db} db the database to query.
 * @param {string} name The name of the file.
 * @param {number} [length] The size of data to read.
 * @param {number} [offset] The offset from the head of the file of which to start reading from.
 * @param {object} [options] Optional settings.
 * @param {(ReadPreference|string)} [options.readPreference] The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST).
 * @param {object} [options.promiseLibrary] A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~readCallback} [callback] the command callback.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.read = function(db, name, length, offset, options, callback) {
  var args = Array.prototype.slice.call(arguments, 2);
  callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
  length = args.length ? args.shift() : null;
  offset = args.length ? args.shift() : null;
  options = args.length ? args.shift() : null;
  options = options || {};

  return executeLegacyOperation(
    db.s.topology,
    readStatic,
    [db, name, length, offset, options, callback],
    { skipSessions: true }
  );
};

var readStatic = function(db, name, length, offset, options, callback) {
  new GridStore(db, name, 'r', options).open(function(err, gridStore) {
    if (err) return callback(err);
    // Make sure we are not reading out of bounds
    if (offset && offset >= gridStore.length)
      return callback('offset larger than size of file', null);
    if (length && length > gridStore.length)
      return callback('length is larger than the size of the file', null);
    if (offset && length && offset + length > gridStore.length)
      return callback('offset and length is larger than the size of the file', null);

    if (offset != null) {
      gridStore.seek(offset, function(err, gridStore) {
        if (err) return callback(err);
        gridStore.read(length, callback);
      });
    } else {
      gridStore.read(length, callback);
    }
  });
};

/**
 * Read the entire file as a list of strings splitting by the provided separator.
 *
 * @method
 * @static
 * @param {Db} db the database to query.
 * @param {(String|object)} name the name of the file.
 * @param {string} [separator] The character to be recognized as the newline separator.
 * @param {object} [options] Optional settings.
 * @param {(ReadPreference|string)} [options.readPreference] The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST).
 * @param {object} [options.promiseLibrary] A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~readlinesCallback} [callback] the command callback.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.readlines = function(db, name, separator, options, callback) {
  var args = Array.prototype.slice.call(arguments, 2);
  callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
  separator = args.length ? args.shift() : null;
  options = args.length ? args.shift() : null;
  options = options || {};

  return executeLegacyOperation(
    db.s.topology,
    readlinesStatic,
    [db, name, separator, options, callback],
    { skipSessions: true }
  );
};

var readlinesStatic = function(db, name, separator, options, callback) {
  var finalSeperator = separator == null ? '\n' : separator;
  new GridStore(db, name, 'r', options).open(function(err, gridStore) {
    if (err) return callback(err);
    gridStore.readlines(finalSeperator, callback);
  });
};

/**
 * Deletes the chunks and metadata information of a file from GridFS.
 *
 * @method
 * @static
 * @param {Db} db The database to query.
 * @param {(string|array)} names The name/names of the files to delete.
 * @param {object} [options] Optional settings.
 * @param {object} [options.promiseLibrary] A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible
 * @param {ClientSession} [options.session] optional session to use for this operation
 * @param {GridStore~resultCallback} [callback] the command callback.
 * @return {Promise} returns Promise if no callback passed
 * @deprecated Use GridFSBucket API instead
 */
GridStore.unlink = function(db, names, options, callback) {
  var args = Array.prototype.slice.call(arguments, 2);
  callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
  options = args.length ? args.shift() : {};
  options = options || {};

  return executeLegacyOperation(db.s.topology, unlinkStatic, [this, db, names, options, callback], {
    skipSessions: true
  });
};

var unlinkStatic = function(self, db, names, options, callback) {
  // Get the write concern
  var writeConcern = _getWriteConcern(db, options);

  // List of names
  if (names.constructor === Array) {
    var tc = 0;
    for (var i = 0; i < names.length; i++) {
      ++tc;
      GridStore.unlink(db, names[i], options, function() {
        if (--tc === 0) {
          callback(null, self);
        }
      });
    }
  } else {
    new GridStore(db, names, 'w', options).open(function(err, gridStore) {
      if (err) return callback(err);
      deleteChunks(gridStore, function(err) {
        if (err) return callback(err);
        gridStore.collection(function(err, collection) {
          if (err) return callback(err);
          collection.remove({ _id: gridStore.fileId }, writeConcern, function(err) {
            callback(err, self);
          });
        });
      });
    });
  }
};

/**
 *  @ignore
 */
var _writeNormal = function(self, data, close, options, callback) {
  // If we have a buffer write it using the writeBuffer method
  if (Buffer.isBuffer(data)) {
    return writeBuffer(self, data, close, callback);
  } else {
    return writeBuffer(self, Buffer.from(data, 'binary'), close, callback);
  }
};

/**
 * @ignore
 */
var _setWriteConcernHash = function(options) {
  const baseOptions = Object.assign(options, options.writeConcern);
  var finalOptions = {};
  if (baseOptions.w != null) finalOptions.w = baseOptions.w;
  if (baseOptions.journal === true) finalOptions.j = baseOptions.journal;
  if (baseOptions.j === true) finalOptions.j = baseOptions.j;
  if (baseOptions.fsync === true) finalOptions.fsync = baseOptions.fsync;
  if (baseOptions.wtimeout != null) finalOptions.wtimeout = baseOptions.wtimeout;
  return finalOptions;
};

/**
 * @ignore
 */
var _getWriteConcern = function(self, options) {
  // Final options
  var finalOptions = { w: 1 };
  options = options || {};

  // Local options verification
  if (
    options.writeConcern != null ||
    options.w != null ||
    typeof options.j === 'boolean' ||
    typeof options.journal === 'boolean' ||
    typeof options.fsync === 'boolean'
  ) {
    finalOptions = _setWriteConcernHash(options);
  } else if (options.safe != null && typeof options.safe === 'object') {
    finalOptions = _setWriteConcernHash(options.safe);
  } else if (typeof options.safe === 'boolean') {
    finalOptions = { w: options.safe ? 1 : 0 };
  } else if (
    self.options.writeConcern != null ||
    self.options.w != null ||
    typeof self.options.j === 'boolean' ||
    typeof self.options.journal === 'boolean' ||
    typeof self.options.fsync === 'boolean'
  ) {
    finalOptions = _setWriteConcernHash(self.options);
  } else if (
    self.safe &&
    (self.safe.w != null ||
      typeof self.safe.j === 'boolean' ||
      typeof self.safe.journal === 'boolean' ||
      typeof self.safe.fsync === 'boolean')
  ) {
    finalOptions = _setWriteConcernHash(self.safe);
  } else if (typeof self.safe === 'boolean') {
    finalOptions = { w: self.safe ? 1 : 0 };
  }

  // Ensure we don't have an invalid combination of write concerns
  if (
    finalOptions.w < 1 &&
    (finalOptions.journal === true || finalOptions.j === true || finalOptions.fsync === true)
  )
    throw MongoError.create({
      message: 'No acknowledgement using w < 1 cannot be combined with journal:true or fsync:true',
      driver: true
    });

  // Return the options
  return finalOptions;
};

/**
 * Create a new GridStoreStream instance (INTERNAL TYPE, do not instantiate directly)
 *
 * @class
 * @extends external:Duplex
 * @return {GridStoreStream} a GridStoreStream instance.
 * @deprecated Use GridFSBucket API instead
 */
var GridStoreStream = function(gs) {
  // Initialize the duplex stream
  Duplex.call(this);

  // Get the gridstore
  this.gs = gs;

  // End called
  this.endCalled = false;

  // If we have a seek
  this.totalBytesToRead = this.gs.length - this.gs.position;
  this.seekPosition = this.gs.position;
};

//
// Inherit duplex
inherits(GridStoreStream, Duplex);

GridStoreStream.prototype._pipe = GridStoreStream.prototype.pipe;

// Set up override
GridStoreStream.prototype.pipe = function(destination) {
  var self = this;

  // Only open gridstore if not already open
  if (!self.gs.isOpen) {
    self.gs.open(function(err) {
      if (err) return self.emit('error', err);
      self.totalBytesToRead = self.gs.length - self.gs.position;
      self._pipe.apply(self, [destination]);
    });
  } else {
    self.totalBytesToRead = self.gs.length - self.gs.position;
    self._pipe.apply(self, [destination]);
  }

  return destination;
};

// Called by stream
GridStoreStream.prototype._read = function() {
  var self = this;

  var read = function() {
    // Read data
    self.gs.read(length, function(err, buffer) {
      if (err && !self.endCalled) return self.emit('error', err);

      // Stream is closed
      if (self.endCalled || buffer == null) return self.push(null);
      // Remove bytes read
      if (buffer.length <= self.totalBytesToRead) {
        self.totalBytesToRead = self.totalBytesToRead - buffer.length;
        self.push(buffer);
      } else if (buffer.length > self.totalBytesToRead) {
        self.totalBytesToRead = self.totalBytesToRead - buffer._index;
        self.push(buffer.slice(0, buffer._index));
      }

      // Finished reading
      if (self.totalBytesToRead <= 0) {
        self.endCalled = true;
      }
    });
  };

  // Set read length
  var length =
    self.gs.length < self.gs.chunkSize ? self.gs.length - self.seekPosition : self.gs.chunkSize;
  if (!self.gs.isOpen) {
    self.gs.open(function(err) {
      self.totalBytesToRead = self.gs.length - self.gs.position;
      if (err) return self.emit('error', err);
      read();
    });
  } else {
    read();
  }
};

GridStoreStream.prototype.destroy = function() {
  this.pause();
  this.endCalled = true;
  this.gs.close();
  this.emit('end');
};

GridStoreStream.prototype.write = function(chunk) {
  var self = this;
  if (self.endCalled)
    return self.emit(
      'error',
      MongoError.create({ message: 'attempting to write to stream after end called', driver: true })
    );
  // Do we have to open the gridstore
  if (!self.gs.isOpen) {
    self.gs.open(function() {
      self.gs.isOpen = true;
      self.gs.write(chunk, function() {
        process.nextTick(function() {
          self.emit('drain');
        });
      });
    });
    return false;
  } else {
    self.gs.write(chunk, function() {
      self.emit('drain');
    });
    return true;
  }
};

GridStoreStream.prototype.end = function(chunk, encoding, callback) {
  var self = this;
  var args = Array.prototype.slice.call(arguments, 0);
  callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
  chunk = args.length ? args.shift() : null;
  encoding = args.length ? args.shift() : null;
  self.endCalled = true;

  if (chunk) {
    self.gs.write(chunk, function() {
      self.gs.close(function() {
        if (typeof callback === 'function') callback();
        self.emit('end');
      });
    });
  }

  self.gs.close(function() {
    if (typeof callback === 'function') callback();
    self.emit('end');
  });
};

/**
 * The read() method pulls some data out of the internal buffer and returns it. If there is no data available, then it will return null.
 * @function external:Duplex#read
 * @param {number} size Optional argument to specify how much data to read.
 * @return {(String | Buffer | null)}
 */

/**
 * Call this function to cause the stream to return strings of the specified encoding instead of Buffer objects.
 * @function external:Duplex#setEncoding
 * @param {string} encoding The encoding to use.
 * @return {null}
 */

/**
 * This method will cause the readable stream to resume emitting data events.
 * @function external:Duplex#resume
 * @return {null}
 */

/**
 * This method will cause a stream in flowing-mode to stop emitting data events. Any data that becomes available will remain in the internal buffer.
 * @function external:Duplex#pause
 * @return {null}
 */

/**
 * This method pulls all the data out of a readable stream, and writes it to the supplied destination, automatically managing the flow so that the destination is not overwhelmed by a fast readable stream.
 * @function external:Duplex#pipe
 * @param {Writable} destination The destination for writing data
 * @param {object} [options] Pipe options
 * @return {null}
 */

/**
 * This method will remove the hooks set up for a previous pipe() call.
 * @function external:Duplex#unpipe
 * @param {Writable} [destination] The destination for writing data
 * @return {null}
 */

/**
 * This is useful in certain cases where a stream is being consumed by a parser, which needs to "un-consume" some data that it has optimistically pulled out of the source, so that the stream can be passed on to some other party.
 * @function external:Duplex#unshift
 * @param {(Buffer|string)} chunk Chunk of data to unshift onto the read queue.
 * @return {null}
 */

/**
 * Versions of Node prior to v0.10 had streams that did not implement the entire Streams API as it is today. (See "Compatibility" below for more information.)
 * @function external:Duplex#wrap
 * @param {Stream} stream An "old style" readable stream.
 * @return {null}
 */

/**
 * This method writes some data to the underlying system, and calls the supplied callback once the data has been fully handled.
 * @function external:Duplex#write
 * @param {(string|Buffer)} chunk The data to write
 * @param {string} encoding The encoding, if chunk is a String
 * @param {function} callback Callback for when this chunk of data is flushed
 * @return {boolean}
 */

/**
 * Call this method when no more data will be written to the stream. If supplied, the callback is attached as a listener on the finish event.
 * @function external:Duplex#end
 * @param {(string|Buffer)} chunk The data to write
 * @param {string} encoding The encoding, if chunk is a String
 * @param {function} callback Callback for when this chunk of data is flushed
 * @return {null}
 */

/**
 * GridStoreStream stream data event, fired for each document in the cursor.
 *
 * @event GridStoreStream#data
 * @type {object}
 */

/**
 * GridStoreStream stream end event
 *
 * @event GridStoreStream#end
 * @type {null}
 */

/**
 * GridStoreStream stream close event
 *
 * @event GridStoreStream#close
 * @type {null}
 */

/**
 * GridStoreStream stream readable event
 *
 * @event GridStoreStream#readable
 * @type {null}
 */

/**
 * GridStoreStream stream drain event
 *
 * @event GridStoreStream#drain
 * @type {null}
 */

/**
 * GridStoreStream stream finish event
 *
 * @event GridStoreStream#finish
 * @type {null}
 */

/**
 * GridStoreStream stream pipe event
 *
 * @event GridStoreStream#pipe
 * @type {null}
 */

/**
 * GridStoreStream stream unpipe event
 *
 * @event GridStoreStream#unpipe
 * @type {null}
 */

/**
 * GridStoreStream stream error event
 *
 * @event GridStoreStream#error
 * @type {null}
 */

/**
 * @ignore
 */
module.exports = GridStore;
