﻿import MongoDB = require('mongodb');
import Bluebird = require('bluebird');
import util = require('util');
import _ = require('lodash');
import Skmatc = require('skmatc');

import {Core} from './Core';
import {Instance} from './Instance';
import {Schema} from './Schema';
import {Hooks} from './Hooks';
import {Plugin} from './Plugins';
import {Cache} from './Cache';
import {CacheDirector} from './CacheDirector';
import * as General from './General';
import {Cursor} from './Cursor';
import * as Index from './Index';
import * as ModelOptions from './ModelOptions';

import {Omnom} from './utils/Omnom';
import {ModelCache} from './ModelCache';
import {ModelHelpers} from './ModelHelpers';
import {ModelHandlers} from './ModelHandlers';
import * as ModelInterfaces from './ModelInterfaces';
import {ModelSpecificInstance} from './ModelSpecificInstance';
import {InstanceImplementation} from './InstanceInterface';
import {Transforms} from './Transforms';
import * as AggregationPipeline from './Aggregate';

/**
 * An Iridium Model which represents a structured MongoDB collection.
 * Models expose the methods you will generally use to query those collections, and ensure that
 * the results of those queries are returned as {TInstance} instances.
 *
 * @param TDocument The interface used to determine the schema of documents in the collection.
 * @param TInstance The interface or class used to represent collection documents in the JS world.
 *
 * @class
 */
export class Model<TDocument extends { _id?: any }, TInstance> {
    /**
     * Creates a new Iridium model representing a given ISchema and backed by a collection whose name is specified
     * @param core The Iridium core that this model should use for database access
     * @param instanceType The class which will be instantiated for each document retrieved from the database
     * @constructor
     */
    constructor(core: Core, instanceType: InstanceImplementation<TDocument, TInstance>) {
        if (!(core instanceof Core)) throw new Error("You failed to provide a valid Iridium core for this model");
        if (typeof instanceType != 'function') throw new Error("You failed to provide a valid instance constructor for this model");
        if (typeof instanceType.collection != 'string' || !instanceType.collection) throw new Error("You failed to provide a valid collection name for this model");
        if (!_.isPlainObject(instanceType.schema) || instanceType.schema._id === undefined) throw new Error("You failed to provide a valid schema for this model");

        this._core = core;

        this.loadExternal(instanceType);
        this.onNewModel();
        this.loadInternal();
    }

    /**
     * Loads any externally available properties (generally accessed using public getters/setters).
     */
    private loadExternal(instanceType: InstanceImplementation<TDocument, TInstance>) {
        this._collection = instanceType.collection;
        this._schema = instanceType.schema;
        this._hooks = instanceType;
        this._cacheDirector = instanceType.cache;
        this._transforms = instanceType.transforms || {};
        this._validators = instanceType.validators || [];
        this._indexes = instanceType.indexes || [];

        if(!this._schema._id) this._schema._id = MongoDB.ObjectID;

        if(this._schema._id === MongoDB.ObjectID && !this._transforms['_id'])
            this._transforms['_id'] = {
                fromDB: value => value._bsontype == 'ObjectID' ? new MongoDB.ObjectID(value.id).toHexString() : value,
                toDB: value => value && typeof value === 'string' ? new MongoDB.ObjectID(value) : value
            };

        if ((<Function>instanceType).prototype instanceof Instance)
            this._Instance = ModelSpecificInstance(this, instanceType);
        else
            this._Instance = instanceType.bind(undefined, this);
    }

    /**
     * Loads any internally (protected/private) properties and helpers only used within Iridium itself.
     */
    private loadInternal() {
        this._cache = new ModelCache(this);
        this._helpers = new ModelHelpers(this);
        this._handlers = new ModelHandlers(this);
    }

    /**
     * Process any callbacks and plugin delegation for the creation of this model.
     * It will generally be called whenever a new Iridium Core is created, however is
     * more specifically tied to the lifespan of the models themselves.
     */
    private onNewModel() {
        this._core.plugins.forEach(plugin => plugin.newModel && plugin.newModel(this));
    }

    private _helpers: ModelHelpers<TDocument, TInstance>;
    /**
     * Provides helper methods used by Iridium for common tasks
     * @returns A set of helper methods which are used within Iridium for common tasks
     */
    get helpers(): ModelHelpers<TDocument, TInstance> {
        return this._helpers;
    }

    private _handlers: ModelHandlers<TDocument, TInstance>;
    /**
     * Provides helper methods used by Iridium for hook delegation and common processes
     * @returns A set of helper methods which perform common event and response handling tasks within Iridium.
     */
    get handlers(): ModelHandlers<TDocument, TInstance> {
        return this._handlers;
    }

    private _hooks: Hooks<TDocument, TInstance> = {};

    /**
     * Gets the even hooks subscribed on this model for a number of different state changes.
     * These hooks are primarily intended to allow lifecycle manipulation logic to be added
     * in the user's model definition, allowing tasks such as the setting of default values
     * or automatic client-side joins to take place.
     */
    get hooks(): Hooks<TDocument, TInstance> {
        return this._hooks;
    }

    private _schema: Schema;
    /**
     * Gets the schema dictating the data structure represented by this model.
     * The schema is used by skmatc to validate documents before saving to the database, however
     * until MongoDB 3.1 becomes widely available (with server side validation support) we are
     * limited in our ability to validate certain types of updates. As such, these validations
     * act more as a data-integrity check than anything else, unless you purely make use of Omnom
     * updates within instances.
     * @public
     * @returns The defined validation schema for this model
     */
    get schema(): Schema {
        return this._schema;
    }

    private _core: Core;
    /**
     * Gets the Iridium core that this model is associated with.
     * @public
     * @returns The Iridium core that this model is bound to
     */
    get core(): Core {
        return this._core;
    }

    private _collection: string;
    /**
     * Gets the underlying MongoDB collection from which this model's documents are retrieved.
     * You can make use of this object if you require any low level access to the MongoDB collection,
     * however we recommend you make use of the Iridium methods whereever possible, as we cannot
     * guarantee the accuracy of the type definitions for the underlying MongoDB driver.
     * @public
     * @returns {Collection}
     */
    get collection(): MongoDB.Collection {
        if (!this.core.connection) throw new Error("Iridium Core not connected to a database.");
        return this.core.connection.collection(this._collection);
    }

    /**
     * Gets the name of the underlying MongoDB collection from which this model's documents are retrieved
     * @public
     */
    get collectionName(): string {
        return this._collection;
    }

    /**
     * Sets the name of the underlying MongoDB collection from which this model's documents are retrieved
     * @public
     */
    set collectionName(value: string) {
        this._collection = value;
    }

    private _cacheDirector: CacheDirector;
    /**
     * Gets the cache controller which dictates which queries will be cached, and under which key
     * @public
     * @returns {CacheDirector}
     */
    get cacheDirector(): CacheDirector {
        return this._cacheDirector;
    }

    private _cache: ModelCache;
    /**
     * Gets the cache responsible for storing objects for quick retrieval under certain conditions
     * @public
     * @returns {ModelCache}
     */
    get cache(): ModelCache {
        return this._cache;
    }

    private _Instance: ModelInterfaces.ModelSpecificInstanceConstructor<TDocument, TInstance>;

    /**
     * Gets the constructor responsible for creating instances for this model
     */
    get Instance(): ModelInterfaces.ModelSpecificInstanceConstructor<TDocument, TInstance> {
        return this._Instance;
    }

    private _transforms: Transforms;

    /**
     * Gets the transforms which are applied whenever a document is received from the database, or
     * prior to storing a document in the database. Tasks such as converting an ObjectID to a string
     * and vice versa are all listed in this object.
     */
    get transforms() {
        return this._transforms;
    }

    private _validators: Skmatc.Validator[];

    /**
     * Gets the custom validation types available for this model. These validators are added to the
     * default skmatc validators, as well as those available through plugins, for use when checking
     * your instances.
     */
    get validators() {
        return this._validators;
    }

    private _indexes: (Index.Index | Index.IndexSpecification)[];

    /**
     * Gets the indexes which Iridium will manage on this model's database collection.
     */
    get indexes() {
        return this._indexes;
    }

    /**
     * Retrieves all documents in the collection and wraps them as instances
     * @param {function(Error, TInstance[])} callback An optional callback which will be triggered when results are available
     * @returns {Promise<TInstance[]>}
     */
    find(): Cursor<TDocument, TInstance>;
    /**
     * Returns all documents in the collection which match the conditions and wraps them as instances
     * @param {Object} conditions The MongoDB query dictating which documents to return
     * @returns {Promise<TInstance[]>}
     */
    find(conditions: { _id?: any, [key: string]: any } | any): Cursor<TDocument, TInstance>;
    /**
     * Returns all documents in the collection which match the conditions
     * @param {Object} conditions The MongoDB query dictating which documents to return
     * @param {Object} fields The fields to include or exclude from the document
     * @returns {Promise<TInstance[]>}
     */
    find(conditions: { _id?: any, [key: string]: any }  | any, fields: { [name: string]: number }): Cursor<TDocument, TInstance>;
    find(conditions?: { _id?: any, [key: string]: any } | any, fields?: any): Cursor<TDocument, TInstance> {
        conditions = conditions || {};
        fields = fields || {};

        if (!_.isPlainObject(conditions)) conditions = { _id: conditions };
        conditions = this._helpers.convertToDB(conditions);

        var cursor = this.collection.find(conditions, {
            fields: fields
        });

        return new Cursor<TDocument, TInstance>(this, conditions, cursor);
    }

    /**
     * Retrieves a single document from the collection and wraps it as an instance
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    get(callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Retrieves a single document from the collection with the given ID and wraps it as an instance
     * @param {any} id The document's unique _id field value in downstream format
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    get(id: any, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Retrieves a single document from the collection which matches the conditions
     * @param {Object} conditions The MongoDB query dictating which document to return
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    get(conditions: { _id?: any, [key: string]: any }, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Retrieves a single document from the collection with the given ID and wraps it as an instance
     * @param {any} id The document's unique _id field value in downstream format
     * @param {QueryOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    get(id: any, options: ModelOptions.QueryOptions, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Retrieves a single document from the collection which matches the conditions
     * @param {Object} conditions The MongoDB query dictating which document to return
     * @param {QueryOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    get(conditions: { _id?: any, [key: string]: any }, options: ModelOptions.QueryOptions, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    get(...args: any[]): Bluebird<TInstance> {
        return this.findOne.apply(this, args);
    }

    /**
     * Retrieves a single document from the collection and wraps it as an instance
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    findOne(callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Retrieves a single document from the collection with the given ID and wraps it as an instance
     * @param {any} id The document's unique _id field value in downstream format
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    findOne(id: any, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Retrieves a single document from the collection which matches the conditions
     * @param {Object} conditions The MongoDB query dictating which document to return
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    findOne(conditions: { _id?: any, [key: string]: any }, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Retrieves a single document from the collection with the given ID and wraps it as an instance
     * @param {any} id The document's unique _id field value in downstream format
     * @param {QueryOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    findOne(id: any, options: ModelOptions.QueryOptions, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Retrieves a single document from the collection which matches the conditions
     * @param {Object} conditions The MongoDB query dictating which document to return
     * @param {QueryOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    findOne(conditions: { _id?: any, [key: string]: any }, options: ModelOptions.QueryOptions, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    findOne(...args: any[]): Bluebird<TInstance> {
        var conditions: { _id?: any, [key: string]: any } = null;
        var options: ModelOptions.QueryOptions = null;
        var callback: General.Callback<TInstance> = null;

        for (var argI = 0; argI < args.length; argI++) {
            if (typeof args[argI] == 'function') callback = callback || args[argI];
            else if (_.isPlainObject(args[argI])) {
                if (conditions) options = args[argI];
                else conditions = args[argI];
            }
            else conditions = { _id: args[argI] };
        }

        conditions = conditions || {};
        options = options || {};

        _.defaults(options, {
            cache: true
        });

        return Bluebird.resolve().bind(this).then(() => {
            conditions = this._helpers.convertToDB(conditions);

            return this._cache.get<TDocument>(conditions);
        }).then((cachedDocument: TDocument) => {
            if (cachedDocument) return cachedDocument;
            return new Bluebird<any>((resolve, reject) => {
                this.collection.findOne(conditions, <MongoDB.CollectionFindOptions>{
                    fields: options.fields,
                    skip: options.skip,
                    sort: options.sort,
                    limit: options.limit
                },(err, result) => {
                        if (err) return reject(err);
                        return resolve(result);
                    });
            });
        }).then((document: TDocument) => {
            if (!document) return null;
            return this._handlers.documentReceived(conditions, document,(document, isNew?, isPartial?) => this._helpers.wrapDocument(document, isNew, isPartial), options);
        }).nodeify(callback);
    }

    /**
     * Inserts an object into the collection after validating it against this model's schema
     * @param {Object} object The object to insert into the collection
     * @param {function(Error, TInstance)} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    create(objects: TDocument, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Inserts an object into the collection after validating it against this model's schema
     * @param {Object} object The object to insert into the collection
     * @param {CreateOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    create(objects: TDocument, options: ModelOptions.CreateOptions, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Inserts the objects into the collection after validating them against this model's schema
     * @param {Object[]} objects The objects to insert into the collection
     * @param {function(Error, TInstance)} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    create(objects: TDocument[], callback?: General.Callback<TInstance[]>): Bluebird<TInstance[]>;
    /**
     * Inserts the objects into the collection after validating them against this model's schema
     * @param {Object[]} objects The objects to insert into the collection
     * @param {CreateOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    create(objects: TDocument[], options: ModelOptions.CreateOptions, callback?: General.Callback<TInstance[]>): Bluebird<TInstance[]>;
    create(...args: any[]): Bluebird<any> {
        return this.insert.apply(this, args);
    }

    /**
     * Inserts an object into the collection after validating it against this model's schema
     * @param {Object} object The object to insert into the collection
     * @param {function(Error, TInstance)} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    insert(objects: TDocument, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Inserts an object into the collection after validating it against this model's schema
     * @param {Object} object The object to insert into the collection
     * @param {CreateOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    insert(objects: TDocument, options: ModelOptions.CreateOptions, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Inserts the objects into the collection after validating them against this model's schema
     * @param {Object[]} objects The objects to insert into the collection
     * @param {function(Error, TInstance[])} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    insert(objects: TDocument[], callback?: General.Callback<TInstance[]>): Bluebird<TInstance[]>;
    /**
     * Inserts the objects into the collection after validating them against this model's schema
     * @param {Object[]} objects The objects to insert into the collection
     * @param {CreateOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance[])} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    insert(objects: TDocument[], options: ModelOptions.CreateOptions, callback?: General.Callback<TInstance[]>): Bluebird<TInstance[]>;
    insert(objs: TDocument | TDocument[], ...args: any[]): Bluebird<any> {
        var objects: TDocument[];
        var options: ModelOptions.CreateOptions = {};
        var callback: General.Callback<any> = null;
        if (typeof args[0] == 'function') callback = args[0];
        else {
            options = args[0];
            callback = args[1];
        }

        if (Array.isArray(objs))
            objects = <TDocument[]>objs;
        else
            objects = <TDocument[]>[objs];

        options = options || {};
        _.defaults(options, <ModelOptions.CreateOptions>{
            w: 'majority',
            forceServerObjectId: true
        });

        return Bluebird.resolve().then(() => {
            var queryOptions = { w: options.w, upsert: options.upsert, new: true };

            if (options.upsert) {
                var docs = this._handlers.creatingDocuments(objects);
                return docs.map((object: { _id: any; }) => {
                    return new Bluebird<any[]>((resolve, reject) => {
                        this.collection.findAndModify({ _id: object._id }, ["_id"], object, queryOptions,(err, result) => {
                            if (err) return reject(err);
                            return resolve(result);
                        });
                    });
                });
            }
            else
                return this._handlers.creatingDocuments(objects).then(objects => _.chunk(objects, 1000)).map((objects: any[]) => {
                    return new Bluebird<any[]>((resolve, reject) => {
                        this.collection.insertMany(objects, queryOptions,(err, result) => {
                            if (err) return reject(err);
                            return resolve(result.ops);
                        });
                    });
                }).then(results => _.flatten(results));
        }).map((inserted: any) => {
            return this._handlers.documentReceived(null, inserted,(document, isNew?, isPartial?) => this._helpers.wrapDocument(document, isNew, isPartial), { cache: options.cache });
        }).then((results: TInstance[]) => {
            if (Array.isArray(objs)) return results;
            return results[0];
        }).nodeify(callback);
    }

    /**
     * Updates the documents in the backing collection which match the conditions using the given update instructions
     * @param {Object} conditions The conditions which determine which documents will be updated
     * @param {Object} changes The changes to make to the documents
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     */
    update(conditions: { _id?: any, [key: string]: any } | any, changes: any, callback?: General.Callback<number>): Bluebird<number>;
    /**
     * Updates the documents in the backing collection which match the conditions using the given update instructions
     * @param {Object} conditions The conditions which determine which documents will be updated
     * @param {Object} changes The changes to make to the documents
     * @param {UpdateOptions} options The options which dictate how this function behaves
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     */
    update(conditions: { _id?: any, [key: string]: any } | any, changes: any, options: ModelOptions.UpdateOptions, callback?: General.Callback<number>): Bluebird<number>;
    update(conditions: { _id?: any, [key: string]: any } | any, changes: any, options?: ModelOptions.UpdateOptions, callback?: General.Callback<number>): Bluebird<number> {
        if (typeof options == 'function') {
            callback = <General.Callback<number>>options;
            options = {};
        }

        options = options || {};

        if (!_.isPlainObject(conditions)) conditions = {
            _id: conditions
        };

        _.defaults(options, {
            w: 'majority',
            multi: true
        });

        return Bluebird.resolve().then(() => {
            conditions = this._helpers.convertToDB(conditions);

            return new Bluebird<number>((resolve, reject) => {
                this.collection.updateMany(conditions, changes, options,(err, response) => {
                    if (err) return reject(err);

                    // New MongoDB 2.6+ response type
                    if (response.result && response.result.nModified !== undefined) return resolve(response.result.nModified);

                    // Legacy response type
                    return resolve(response.result.n);
                });
            })
        }).nodeify(callback);
    }

    /**
     * Counts the number of documents in the collection
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     * @returns {Promise<number>}
     */
    count(callback?: General.Callback<number>): Bluebird<number>;
    /**
     * Counts the number of documents in the collection which match the conditions provided
     * @param {Object} conditions The conditions which determine whether an object is counted or not
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     * @returns {Promise<number>}
     */
    count(conditions: { _id?: any, [key: string]: any } | any, callback?: General.Callback<number>): Bluebird<number>;
    count(conds?: any, callback?: General.Callback<number>): Bluebird<number> {
        var conditions: { _id?: any, [key: string]: any } = <{ _id?: any, [key: string]: any }>conds;
        if (typeof conds == 'function') {
            callback = <General.Callback<number>>conds;
            conditions = {};
        }

        conditions = conditions || {};

        if (!_.isPlainObject(conditions)) conditions = {
            _id: conditions
        };

        return Bluebird.resolve().then(() => {
            conditions = this._helpers.convertToDB(conditions);

            return new Bluebird<number>((resolve, reject) => {
                this.collection.count(conditions,(err, results) => {
                    if (err) return reject(err);
                    return resolve(results);
                });
            });
        }).nodeify(callback);
    }

    /**
     * Removes all documents from the collection
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     * @returns {Promise<number>}
     */
    remove(callback?: General.Callback<number>): Bluebird<number>;
    /**
     * Removes all documents from the collection which match the conditions
     * @param {Object} conditions The conditions determining whether an object is removed or not
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     * @returns {Promise<number>}
     */
    remove(conditions: { _id?: any, [key: string]: any } | any, callback?: General.Callback<number>): Bluebird<number>;
    /**
     * Removes all documents from the collection which match the conditions
     * @param {Object} conditions The conditions determining whether an object is removed or not
     * @param {Object} options The options controlling the way in which the function behaves
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     * @returns {Promise<number>}
     */
    remove(conditions: { _id?: any, [key: string]: any }, options: ModelOptions.RemoveOptions, callback?: General.Callback<number>): Bluebird<number>;
    remove(conds?: any, options?: ModelOptions.RemoveOptions, callback?: General.Callback<number>): Bluebird<number> {
        var conditions: { _id?: any, [key: string]: any } = <{ _id?: any, [key: string]: any }>conds;

        if (typeof options === 'function') {
            callback = <General.Callback<number>>options;
            options = {};
        }

        if (typeof conds == 'function') {
            callback = <General.Callback<number>>conds;
            options = {};
            conditions = {};
        }

        conditions = conditions || {};
        options = options || {};

        _.defaults(options, {
            w: 'majority'
        });

        if (!_.isPlainObject(conditions)) conditions = {
            _id: conditions
        };

        return Bluebird.resolve().then(() => {
            conditions = this._helpers.convertToDB(conditions);

            return new Bluebird<number>((resolve, reject) => {
                this.collection.remove(conditions, options,(err, response) => {
                    if (err) return reject(err);
                    return resolve(response.result.n);
                });
            });
        }).then((count) => {
            if (count === 1) this._cache.clear(conditions);
            return Bluebird.resolve(count);
        }).nodeify(callback);
    }

    aggregate<T>(pipeline: AggregationPipeline.Stage[]): Bluebird<T[]> {
        return new Bluebird<T[]>((resolve, reject) => {
            this.collection.aggregate(pipeline, (err, results) => {
                if(err) return reject(err);
                return resolve(results);
            });
        });
    }

    /**
     * Ensures that the given index is created for the collection
     * @param {Object} specification The index specification object used by MongoDB
     * @param {function(Error, String)} callback A callback which is triggered when the operation completes
     * @returns {Promise<String>} The name of the index
     */
    ensureIndex(specification: Index.IndexSpecification, callback?: General.Callback<string>): Bluebird<string>;
    /**
     * Ensures that the given index is created for the collection
     * @param {Object} specification The index specification object used by MongoDB
     * @param {MongoDB.IndexOptions} options The options dictating how the index is created and behaves
     * @param {function(Error, String)} callback A callback which is triggered when the operation completes
     * @returns {Promise<String>} The name of the index
     */
    ensureIndex(specification: Index.IndexSpecification, options: MongoDB.IndexOptions, callback?: General.Callback<string>): Bluebird<string>;
    ensureIndex(specification: Index.IndexSpecification, options?: MongoDB.IndexOptions, callback?: General.Callback<string>): Bluebird<string> {
        if (typeof options == 'function') {
            callback = <General.Callback<any>>options;
            options = {};
        }

        return new Bluebird<string>((resolve, reject) => {
            this.collection.ensureIndex(specification, options,(err, name: any) => {
                if (err) return reject(err);
                return resolve(name);
            });
        }).nodeify(callback);
    }

    /**
     * Ensures that all indexes defined in the model's options are created
     * @param {function(Error, String[])} callback A callback which is triggered when the operation completes
     * @returns {Promise<String[]>} The names of the indexes
     */
    ensureIndexes(callback?: General.Callback<string[]>): Bluebird<string[]> {
        return Bluebird.resolve(this._indexes).map((index: Index.Index | Index.IndexSpecification) => {
            return this.ensureIndex((<Index.Index>index).spec || <Index.IndexSpecification>index,(<Index.Index>index).options || {});
        }).nodeify(callback);
    }

    /**
     * Drops the index with the specified name if it exists in the collection
     * @param {String} name The name of the index to remove
     * @param {function(Error, Boolean)} callback A callback which is triggered when the operation completes
     * @returns {Promise<Boolean>} Whether the index was dropped
     */
    dropIndex(name: string, callback?: General.Callback<boolean>): Bluebird<boolean>;
    /**
     * Drops the index if it exists in the collection
     * @param {IndexSpecification} index The index to remove
     * @param {function(Error, Boolean)} callback A callback which is triggered when the operation completes
     * @returns {Promise<Boolean>} Whether the index was dropped
     */
    dropIndex(index: Index.IndexSpecification, callback?: General.Callback<boolean>): Bluebird<boolean>;
    dropIndex(specification: string | Index.IndexSpecification, callback?: General.Callback<boolean>): Bluebird<boolean> {
        var index: string;

        if (typeof (specification) === 'string') index = <string>specification;
        else {
            index = _(<Index.IndexSpecification>specification).map((direction, key) => key + '_' + direction).reduce<string>((x, y) => x + '_' + y);
        }

        return new Bluebird<boolean>((resolve, reject) => {
            this.collection.dropIndex(index,(err, result: { ok: number }) => {
                if (err) return reject(err);
                return resolve(<any>!!result.ok);
            });
        }).nodeify(callback);
    }

    /**
     * Removes all indexes (except for _id) from the collection
     * @param {function(Error, Boolean)} callback A callback which is triggered when the operation completes
     * @returns {Promise<Boolean>} Whether the indexes were dropped
     */
    dropIndexes(callback?: General.Callback<boolean>): Bluebird<boolean> {
        return new Bluebird<any>((resolve, reject) => {
            this.collection.dropAllIndexes((err, count) => {
                if (err) return reject(err);
                return resolve(count);
            });
        }).nodeify(callback);
    }
}
