Source: OldView.js

"use strict";

// Import external names locally
var Shared,
	Db,
	CollectionGroup,
	Collection,
	CollectionInit,
	CollectionGroupInit,
	DbInit;

Shared = require('./Shared');

/**
 * The view constructor.
 * @param viewName
 * @constructor
 */
var OldView = function (viewName) {
	this.init.apply(this, arguments);
};

OldView.prototype.init = function (viewName) {
	var self = this;

	this._name = viewName;
	this._listeners = {};
	this._query = {
		query: {},
		options: {}
	};

	// Register listeners for the CRUD events
	this._onFromSetData = function () {
		self._onSetData.apply(self, arguments);
	};

	this._onFromInsert = function () {
		self._onInsert.apply(self, arguments);
	};

	this._onFromUpdate = function () {
		self._onUpdate.apply(self, arguments);
	};

	this._onFromRemove = function () {
		self._onRemove.apply(self, arguments);
	};

	this._onFromChange = function () {
		if (self.debug()) { console.log('ForerunnerDB.OldView: Received change'); }
		self._onChange.apply(self, arguments);
	};
};

Shared.addModule('OldView', OldView);

CollectionGroup = require('./CollectionGroup');
Collection = require('./Collection');
CollectionInit = Collection.prototype.init;
CollectionGroupInit = CollectionGroup.prototype.init;
Db = Shared.modules.Db;
DbInit = Db.prototype.init;

Shared.mixin(OldView.prototype, 'Mixin.Events');

/**
 * Drops a view and all it's stored data from the database.
 * @returns {boolean} True on success, false on failure.
 */
OldView.prototype.drop = function () {
	if ((this._db || this._from) && this._name) {
		if (this.debug()) {
			console.log('ForerunnerDB.OldView: Dropping view ' + this._name);
		}

		this._state = 'dropped';

		this.emit('drop', this);

		if (this._db && this._db._oldViews) {
			delete this._db._oldViews[this._name];
		}

		if (this._from && this._from._oldViews) {
			delete this._from._oldViews[this._name];
		}

		return true;
	}

	return false;
};

OldView.prototype.debug = function () {
	// TODO: Make this function work
	return false;
};

/**
 * Gets / sets the DB the view is bound against. Automatically set
 * when the db.oldView(viewName) method is called.
 * @param db
 * @returns {*}
 */
OldView.prototype.db = function (db) {
	if (db !== undefined) {
		this._db = db;
		return this;
	}

	return this._db;
};

/**
 * Gets / sets the collection that the view derives it's data from.
 * @param {*} collection A collection instance or the name of a collection
 * to use as the data set to derive view data from.
 * @returns {*}
 */
OldView.prototype.from = function (collection) {
	if (collection !== undefined) {
		// Check if this is a collection name or a collection instance
		if (typeof(collection) === 'string') {
			if (this._db.collectionExists(collection)) {
				collection = this._db.collection(collection);
			} else {
				throw('ForerunnerDB.OldView "' + this.name() + '": Invalid collection in view.from() call.');
			}
		}

		// Check if the existing from matches the passed one
		if (this._from !== collection) {
			// Check if we already have a collection assigned
			if (this._from) {
				// Remove ourselves from the collection view lookup
				this.removeFrom();
			}

			this.addFrom(collection);
		}

		return this;
	}

	return this._from;
};

OldView.prototype.addFrom = function (collection) {
	//var self = this;

	this._from = collection;

	if (this._from) {
		this._from.on('setData', this._onFromSetData);
		//this._from.on('insert', this._onFromInsert);
		//this._from.on('update', this._onFromUpdate);
		//this._from.on('remove', this._onFromRemove);
		this._from.on('change', this._onFromChange);

		// Add this view to the collection's view lookup
		this._from._addOldView(this);
		this._primaryKey = this._from._primaryKey;

		this.refresh();
		return this;
	} else {
		throw('ForerunnerDB.OldView "' + this.name() + '": Cannot determine collection type in view.from()');
	}
};

OldView.prototype.removeFrom = function () {
	// Unsubscribe from events on this "from"
	this._from.off('setData', this._onFromSetData);
	//this._from.off('insert', this._onFromInsert);
	//this._from.off('update', this._onFromUpdate);
	//this._from.off('remove', this._onFromRemove);
	this._from.off('change', this._onFromChange);

	this._from._removeOldView(this);
};

/**
 * Gets the primary key for this view from the assigned collection.
 * @returns {String}
 */
OldView.prototype.primaryKey = function () {
	if (this._from) {
		return this._from.primaryKey();
	}

	return undefined;
};

/**
 * Gets / sets the query that the view uses to build it's data set.
 * @param {Object=} query
 * @param {Boolean=} options An options object.
 * @param {Boolean=} refresh Whether to refresh the view data after
 * this operation. Defaults to true.
 * @returns {*}
 */
OldView.prototype.queryData = function (query, options, refresh) {
	if (query !== undefined) {
		this._query.query = query;
	}

	if (options !== undefined) {
		this._query.options = options;
	}

	if (query !== undefined || options !== undefined) {
		if (refresh === undefined || refresh === true) {
			this.refresh();
		}

		return this;
	}

	return this._query;
};

/**
 * Add data to the existing query.
 * @param {Object} obj The data whose keys will be added to the existing
 * query object.
 * @param {Boolean} overwrite Whether or not to overwrite data that already
 * exists in the query object. Defaults to true.
 * @param {Boolean=} refresh Whether or not to refresh the view data set
 * once the operation is complete. Defaults to true.
 */
OldView.prototype.queryAdd = function (obj, overwrite, refresh) {
	var query = this._query.query,
		i;

	if (obj !== undefined) {
		// Loop object properties and add to existing query
		for (i in obj) {
			if (obj.hasOwnProperty(i)) {
				if (query[i] === undefined || (query[i] !== undefined && overwrite)) {
					query[i] = obj[i];
				}
			}
		}
	}

	if (refresh === undefined || refresh === true) {
		this.refresh();
	}
};

/**
 * Remove data from the existing query.
 * @param {Object} obj The data whose keys will be removed from the existing
 * query object.
 * @param {Boolean=} refresh Whether or not to refresh the view data set
 * once the operation is complete. Defaults to true.
 */
OldView.prototype.queryRemove = function (obj, refresh) {
	var query = this._query.query,
		i;

	if (obj !== undefined) {
		// Loop object properties and add to existing query
		for (i in obj) {
			if (obj.hasOwnProperty(i)) {
				delete query[i];
			}
		}
	}

	if (refresh === undefined || refresh === true) {
		this.refresh();
	}
};

/**
 * Gets / sets the query being used to generate the view data.
 * @param {Object=} query The query to set.
 * @param {Boolean=} refresh Whether to refresh the view data after
 * this operation. Defaults to true.
 * @returns {*}
 */
OldView.prototype.query = function (query, refresh) {
	if (query !== undefined) {
		this._query.query = query;

		if (refresh === undefined || refresh === true) {
			this.refresh();
		}
		return this;
	}

	return this._query.query;
};

/**
 * Gets / sets the query options used when applying sorting etc to the
 * view data set.
 * @param {Object=} options An options object.
 * @param {Boolean=} refresh Whether to refresh the view data after
 * this operation. Defaults to true.
 * @returns {*}
 */
OldView.prototype.queryOptions = function (options, refresh) {
	if (options !== undefined) {
		this._query.options = options;

		if (refresh === undefined || refresh === true) {
			this.refresh();
		}
		return this;
	}

	return this._query.options;
};

/**
 * Refreshes the view data and diffs between previous and new data to
 * determine if any events need to be triggered or DOM binds updated.
 */
OldView.prototype.refresh = function (force) {
	if (this._from) {
		// Take a copy of the data before updating it, we will use this to
		// "diff" between the old and new data and handle DOM bind updates
		var oldData = this._data,
			oldDataArr,
			oldDataItem,
			newData,
			newDataArr,
			query,
			primaryKey,
			dataItem,
			inserted = [],
			updated = [],
			removed = [],
			operated = false,
			i;

		if (this.debug()) {
			console.log('ForerunnerDB.OldView: Refreshing view ' + this._name);
			console.log('ForerunnerDB.OldView: Existing data: ' + (typeof(this._data) !== "undefined"));
			if (typeof(this._data) !== "undefined") {
				console.log('ForerunnerDB.OldView: Current data rows: ' + this._data.find().length);
			}
			//console.log(OldView.prototype.refresh.caller);
		}

		// Query the collection and update the data
		if (this._query) {
			if (this.debug()) {
				console.log('ForerunnerDB.OldView: View has query and options, getting subset...');
			}
			// Run query against collection
			//console.log('refresh with query and options', this._query.options);
			this._data = this._from.subset(this._query.query, this._query.options);
			//console.log(this._data);
		} else {
			// No query, return whole collection
			if (this._query.options) {
				if (this.debug()) {
					console.log('ForerunnerDB.OldView: View has options, getting subset...');
				}
				this._data = this._from.subset({}, this._query.options);
			} else {
				if (this.debug()) {
					console.log('ForerunnerDB.OldView: View has no query or options, getting subset...');
				}
				this._data = this._from.subset({});
			}
		}

		// Check if there was old data
		if (!force && oldData) {
			if (this.debug()) {
				console.log('ForerunnerDB.OldView: Refresh not forced, old data detected...');
			}

			// Now determine the difference
			newData = this._data;

			if (oldData.subsetOf() === newData.subsetOf()) {
				if (this.debug()) {
					console.log('ForerunnerDB.OldView: Old and new data are from same collection...');
				}
				newDataArr = newData.find();
				oldDataArr = oldData.find();
				primaryKey = newData._primaryKey;

				// The old data and new data were derived from the same parent collection
				// so scan the data to determine changes
				for (i = 0; i < newDataArr.length; i++) {
					dataItem = newDataArr[i];

					query = {};
					query[primaryKey] = dataItem[primaryKey];

					// Check if this item exists in the old data
					oldDataItem = oldData.find(query)[0];

					if (!oldDataItem) {
						// New item detected
						inserted.push(dataItem);
					} else {
						// Check if an update has occurred
						if (JSON.stringify(oldDataItem) !== JSON.stringify(dataItem)) {
							// Updated / already included item detected
							updated.push(dataItem);
						}
					}
				}

				// Now loop the old data and check if any records were removed
				for (i = 0; i < oldDataArr.length; i++) {
					dataItem = oldDataArr[i];

					query = {};
					query[primaryKey] = dataItem[primaryKey];

					// Check if this item exists in the old data
					if (!newData.find(query)[0]) {
						// Removed item detected
						removed.push(dataItem);
					}
				}

				if (this.debug()) {
					console.log('ForerunnerDB.OldView: Removed ' + removed.length + ' rows');
					console.log('ForerunnerDB.OldView: Inserted ' + inserted.length + ' rows');
					console.log('ForerunnerDB.OldView: Updated ' + updated.length + ' rows');
				}

				// Now we have a diff of the two data sets, we need to get the DOM updated
				if (inserted.length) {
					this._onInsert(inserted, []);
					operated = true;
				}

				if (updated.length) {
					this._onUpdate(updated, []);
					operated = true;
				}

				if (removed.length) {
					this._onRemove(removed, []);
					operated = true;
				}
			} else {
				// The previous data and the new data are derived from different collections
				// and can therefore not be compared, all data is therefore effectively "new"
				// so first perform a remove of all existing data then do an insert on all new data
				if (this.debug()) {
					console.log('ForerunnerDB.OldView: Old and new data are from different collections...');
				}
				removed = oldData.find();

				if (removed.length) {
					this._onRemove(removed);
					operated = true;
				}

				inserted = newData.find();

				if (inserted.length) {
					this._onInsert(inserted);
					operated = true;
				}
			}
		} else {
			// Force an update as if the view never got created by padding all elements
			// to the insert
			if (this.debug()) {
				console.log('ForerunnerDB.OldView: Forcing data update', newDataArr);
			}

			this._data = this._from.subset(this._query.query, this._query.options);
			newDataArr = this._data.find();

			if (this.debug()) {
				console.log('ForerunnerDB.OldView: Emitting change event with data', newDataArr);
			}
			this._onInsert(newDataArr, []);
		}

		if (this.debug()) { console.log('ForerunnerDB.OldView: Emitting change'); }
		this.emit('change');
	}

	return this;
};

/**
 * Returns the number of documents currently in the view.
 * @returns {Number}
 */
OldView.prototype.count = function () {
	return this._data && this._data._data ? this._data._data.length : 0;
};

/**
 * Queries the view data. See Collection.find() for more information.
 * @returns {*}
 */
OldView.prototype.find = function () {
	if (this._data) {
		if (this.debug()) {
			console.log('ForerunnerDB.OldView: Finding data in view collection...', this._data);
		}

		return this._data.find.apply(this._data, arguments);
	} else {
		return [];
	}
};

/**
 * Inserts into view data via the view collection. See Collection.insert() for more information.
 * @returns {*}
 */
OldView.prototype.insert = function () {
	if (this._from) {
		// Pass the args through to the from object
		return this._from.insert.apply(this._from, arguments);
	} else {
		return [];
	}
};

/**
 * Updates into view data via the view collection. See Collection.update() for more information.
 * @returns {*}
 */
OldView.prototype.update = function () {
	if (this._from) {
		// Pass the args through to the from object
		return this._from.update.apply(this._from, arguments);
	} else {
		return [];
	}
};

/**
 * Removed from view data via the view collection. See Collection.remove() for more information.
 * @returns {*}
 */
OldView.prototype.remove = function () {
	if (this._from) {
		// Pass the args through to the from object
		return this._from.remove.apply(this._from, arguments);
	} else {
		return [];
	}
};

OldView.prototype._onSetData = function (newDataArr, oldDataArr) {
	this.emit('remove', oldDataArr, []);
	this.emit('insert', newDataArr, []);
	//this.refresh();
};

OldView.prototype._onInsert = function (successArr, failArr) {
	this.emit('insert', successArr, failArr);
	//this.refresh();
};

OldView.prototype._onUpdate = function (successArr, failArr) {
	this.emit('update', successArr, failArr);
	//this.refresh();
};

OldView.prototype._onRemove = function (successArr, failArr) {
	this.emit('remove', successArr, failArr);
	//this.refresh();
};

OldView.prototype._onChange = function () {
	if (this.debug()) { console.log('ForerunnerDB.OldView: Refreshing data'); }
	this.refresh();
};

// Extend collection with view init
Collection.prototype.init = function () {
	this._oldViews = [];
	CollectionInit.apply(this, arguments);
};

/**
 * Adds a view to the internal view lookup.
 * @param {View} view The view to add.
 * @returns {Collection}
 * @private
 */
Collection.prototype._addOldView = function (view) {
	if (view !== undefined) {
		this._oldViews[view._name] = view;
	}

	return this;
};

/**
 * Removes a view from the internal view lookup.
 * @param {View} view The view to remove.
 * @returns {Collection}
 * @private
 */
Collection.prototype._removeOldView = function (view) {
	if (view !== undefined) {
		delete this._oldViews[view._name];
	}

	return this;
};

// Extend collection with view init
CollectionGroup.prototype.init = function () {
	this._oldViews = [];
	CollectionGroupInit.apply(this, arguments);
};

/**
 * Adds a view to the internal view lookup.
 * @param {View} view The view to add.
 * @returns {Collection}
 * @private
 */
CollectionGroup.prototype._addOldView = function (view) {
	if (view !== undefined) {
		this._oldViews[view._name] = view;
	}

	return this;
};

/**
 * Removes a view from the internal view lookup.
 * @param {View} view The view to remove.
 * @returns {Collection}
 * @private
 */
CollectionGroup.prototype._removeOldView = function (view) {
	if (view !== undefined) {
		delete this._oldViews[view._name];
	}

	return this;
};

// Extend DB with views init
Db.prototype.init = function () {
	this._oldViews = {};
	DbInit.apply(this, arguments);
};

/**
 * Gets a view by it's name.
 * @param {String} viewName The name of the view to retrieve.
 * @returns {*}
 */
Db.prototype.oldView = function (viewName) {
	if (!this._oldViews[viewName]) {
		if (this.debug()) {
			console.log('ForerunnerDB.OldView: Creating view ' + viewName);
		}
	}

	this._oldViews[viewName] = this._oldViews[viewName] || new OldView(viewName).db(this);
	return this._oldViews[viewName];
};

/**
 * Determine if a view with the passed name already exists.
 * @param {String} viewName The name of the view to check for.
 * @returns {boolean}
 */
Db.prototype.oldViewExists = function (viewName) {
	return Boolean(this._oldViews[viewName]);
};

/**
 * Returns an array of views the DB currently has.
 * @returns {Array} An array of objects containing details of each view
 * the database is currently managing.
 */
Db.prototype.oldViews = function () {
	var arr = [],
		i;

	for (i in this._oldViews) {
		if (this._oldViews.hasOwnProperty(i)) {
			arr.push({
				name: i,
				count: this._oldViews[i].count()
			});
		}
	}

	return arr;
};

Shared.finishModule('OldView');
module.exports = OldView;