# Source: OldView.Bind.js

    "use strict";

    // Grab the view class
    var Shared,
    	Core,
    	OldView,
    	OldViewInit;

    Shared = require('./Shared');
    Core = Shared.modules.Core;
    OldView = Shared.modules.OldView;
    OldViewInit = OldView.prototype.init;

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

    	this._binds = [];
    	this._renderStart = 0;
    	this._renderEnd = 0;

    	this._deferQueue = {
    		insert: [],
    		update: [],
    		remove: [],
    		upsert: [],
    		_bindInsert: [],
    		_bindUpdate: [],
    		_bindRemove: [],
    		_bindUpsert: []
    	};

    	this._deferThreshold = {
    		insert: 100,
    		update: 100,
    		remove: 100,
    		upsert: 100,
    		_bindInsert: 100,
    		_bindUpdate: 100,
    		_bindRemove: 100,
    		_bindUpsert: 100
    	};

    	this._deferTime = {
    		insert: 100,
    		update: 1,
    		remove: 1,
    		upsert: 1,
    		_bindInsert: 100,
    		_bindUpdate: 1,
    		_bindRemove: 1,
    		_bindUpsert: 1
    	};

    	OldViewInit.apply(this, arguments);

    	// Hook view events to update binds
    	this.on('insert', function (successArr, failArr) {
    		self._bindEvent('insert', successArr, failArr);
    	});

    	this.on('update', function (successArr, failArr) {
    		self._bindEvent('update', successArr, failArr);
    	});

    	this.on('remove', function (successArr, failArr) {
    		self._bindEvent('remove', successArr, failArr);
    	});

    	this.on('change', self._bindChange);
    };

    /**
     * Binds a selector to the insert, update and delete events of a particular
     * view and keeps the selector in sync so that updates are reflected on the
     * web page in real-time.
     *
     * @param {String} selector The jQuery selector string to get target elements.
     * @param {Object} options The options object.
     */
    OldView.prototype.bind = function (selector, options) {
    	if (options && options.template) {
    		this._binds[selector] = options;
    	} else {
    		throw('ForerunnerDB.OldView "' + this.name() + '": Cannot bind data to element, missing options information!');
    	}

    	return this;
    };

    /**
     * Un-binds a selector from the view changes.
     * @param {String} selector The jQuery selector string to identify the bind to remove.
     * @returns {Collection}
     */
    OldView.prototype.unBind = function (selector) {
    	delete this._binds[selector];
    	return this;
    };

    /**
     * Returns true if the selector is bound to the view.
     * @param {String} selector The jQuery selector string to identify the bind to check for.
     * @returns {boolean}
     */
    OldView.prototype.isBound = function (selector) {
    	return Boolean(this._binds[selector]);
    };

    /**
     * Sorts items in the DOM based on the bind settings and the passed item array.
     * @param {String} selector The jQuery selector of the bind container.
     * @param {Array} itemArr The array of items used to determine the order the DOM
     * elements should be in based on the order they are in, in the array.
     */
    OldView.prototype.bindSortDom = function (selector, itemArr) {
    	var container = window.jQuery(selector),
    		arrIndex,
    		arrItem,
    		domItem;

    	if (this.debug()) {
    		console.log('ForerunnerDB.OldView.Bind: Sorting data in DOM...', itemArr);
    	}

    	for (arrIndex = 0; arrIndex < itemArr.length; arrIndex++) {
    		arrItem = itemArr[arrIndex];

    		// Now we've done our inserts into the DOM, let's ensure
    		// they are still ordered correctly
    		domItem = container.find('#' + arrItem[this._primaryKey]);

    		if (domItem.length) {
    			if (arrIndex === 0) {
    				if (this.debug()) {
    					console.log('ForerunnerDB.OldView.Bind: Sort, moving to index 0...', domItem);
    				}
    				container.prepend(domItem);
    			} else {
    				if (this.debug()) {
    					console.log('ForerunnerDB.OldView.Bind: Sort, moving to index ' + arrIndex + '...', domItem);
    				}
    				domItem.insertAfter(container.children(':eq(' + (arrIndex - 1) + ')'));
    			}
    		} else {
    			if (this.debug()) {
    				console.log('ForerunnerDB.OldView.Bind: Warning, element for array item not found!', arrItem);
    			}
    		}
    	}
    };

    OldView.prototype.bindRefresh = function (obj) {
    	var binds = this._binds,
    		bindKey,
    		bind;

    	if (!obj) {
    		// Grab current data
    		obj = {
    			data: this.find()
    		};
    	}

    	for (bindKey in binds) {
    		if (binds.hasOwnProperty(bindKey)) {
    			bind = binds[bindKey];

    			if (this.debug()) { console.log('ForerunnerDB.OldView.Bind: Sorting DOM...'); }
    			this.bindSortDom(bindKey, obj.data);

    			if (bind.afterOperation) {
    				bind.afterOperation();
    			}

    			if (bind.refresh) {
    				bind.refresh();
    			}
    		}
    	}
    };

    /**
     * Renders a bind view data to the DOM.
     * @param {String} bindSelector The jQuery selector string to use to identify
     * the bind target. Must match the selector used when defining the original bind.
     * @param {Function=} domHandler If specified, this handler method will be called
     * with the final HTML for the view instead of the DB handling the DOM insertion.
     */
    OldView.prototype.bindRender = function (bindSelector, domHandler) {
    	// Check the bind exists
    	var bind = this._binds[bindSelector],
    		domTarget = window.jQuery(bindSelector),
    		allData,
    		dataItem,
    		itemHtml,
    		finalHtml = window.jQuery('<ul></ul>'),
    		bindCallback,
    		i;

    	if (bind) {
    		allData = this._data.find();

    		bindCallback = function (itemHtml) {
    			finalHtml.append(itemHtml);
    		};

    		// Loop all items and add them to the screen
    		for (i = 0; i < allData.length; i++) {
    			dataItem = allData[i];

    			itemHtml = bind.template(dataItem, bindCallback);
    		}

    		if (!domHandler) {
    			domTarget.append(finalHtml.html());
    		} else {
    			domHandler(bindSelector, finalHtml.html());
    		}
    	}
    };

    OldView.prototype.processQueue = function (type, callback) {
    	var queue = this._deferQueue[type],
    		deferThreshold = this._deferThreshold[type],
    		deferTime = this._deferTime[type];

    	if (queue.length) {
    		var self = this,
    			dataArr;

    		// Process items up to the threshold
    		if (queue.length) {
    			if (queue.length > deferThreshold) {
    				// Grab items up to the threshold value
    				dataArr = queue.splice(0, deferThreshold);
    			} else {
    				// Grab all the remaining items
    				dataArr = queue.splice(0, queue.length);
    			}

    			this._bindEvent(type, dataArr, []);
    		}

    		// Queue another process
    		setTimeout(function () {
    			self.processQueue(type, callback);
    		}, deferTime);
    	} else {
    		if (callback) { callback(); }
    		this.emit('bindQueueComplete');
    	}
    };

    OldView.prototype._bindEvent = function (type, successArr, failArr) {
    	/*var queue = this._deferQueue[type],
    		deferThreshold = this._deferThreshold[type],
    		deferTime = this._deferTime[type];*/

    	var binds = this._binds,
    		unfilteredDataSet = this.find({}),
    		filteredDataSet,
    		bindKey;

    	// Check if the number of inserts is greater than the defer threshold
    	/*if (successArr && successArr.length > deferThreshold) {
    	 // Break up upsert into blocks
    	 this._deferQueue[type] = queue.concat(successArr);

    	 // Fire off the insert queue handler
    	 this.processQueue(type);

    	 return;
    	 } else {*/
    	for (bindKey in binds) {
    		if (binds.hasOwnProperty(bindKey)) {
    			if (binds[bindKey].reduce) {
    				filteredDataSet = this.find(binds[bindKey].reduce.query, binds[bindKey].reduce.options);
    			} else {
    				filteredDataSet = unfilteredDataSet;
    			}

    			switch (type) {
    				case 'insert':
    					this._bindInsert(bindKey, binds[bindKey], successArr, failArr, filteredDataSet);
    					break;

    				case 'update':
    					this._bindUpdate(bindKey, binds[bindKey], successArr, failArr, filteredDataSet);
    					break;

    				case 'remove':
    					this._bindRemove(bindKey, binds[bindKey], successArr, failArr, filteredDataSet);
    					break;
    			}
    		}
    	}
    	//}
    };

    OldView.prototype._bindChange = function (newDataArr) {
    	if (this.debug()) {
    		console.log('ForerunnerDB.OldView.Bind: Bind data change, refreshing bind...', newDataArr);
    	}

    	this.bindRefresh(newDataArr);
    };

    OldView.prototype._bindInsert = function (selector, options, successArr, failArr, all) {
    	var container = window.jQuery(selector),
    		itemElem,
    		itemHtml,
    		makeCallback,
    		i;

    	makeCallback = function (itemElem, insertedItem, failArr, all) {
    		return function (itemHtml) {
    			// Check if there is custom DOM insert method
    			if (options.insert) {
    				options.insert(itemHtml, insertedItem, failArr, all);
    			} else {
    				// Handle the insert automatically
    				// Add the item to the container
    				if (options.prependInsert) {
    					container.prepend(itemHtml);

    				} else {
    					container.append(itemHtml);
    				}
    			}

    			if (options.afterInsert) {
    				options.afterInsert(itemHtml, insertedItem, failArr, all);
    			}
    		};
    	};

    	// Loop the inserted items
    	for (i = 0; i < successArr.length; i++) {
    		// Check for existing item in the container
    		itemElem = container.find('#' + successArr[i][this._primaryKey]);

    		if (!itemElem.length) {
    			itemHtml = options.template(successArr[i], makeCallback(itemElem, successArr[i], failArr, all));
    		}
    	}
    };

    OldView.prototype._bindUpdate = function (selector, options, successArr, failArr, all) {
    	var container = window.jQuery(selector),
    		itemElem,
    		makeCallback,
    		i;

    	makeCallback = function (itemElem, itemData) {
    		return function (itemHtml) {
    			// Check if there is custom DOM insert method
    			if (options.update) {
    				options.update(itemHtml, itemData, all, itemElem.length ? 'update' : 'append');
    			} else {
    				if (itemElem.length) {
    					// An existing item is in the container, replace it with the
    					// new rendered item from the updated data
    					itemElem.replaceWith(itemHtml);
    				} else {
    					// The item element does not already exist, append it
    					if (options.prependUpdate) {
    						container.prepend(itemHtml);
    					} else {
    						container.append(itemHtml);
    					}
    				}
    			}

    			if (options.afterUpdate) {
    				options.afterUpdate(itemHtml, itemData, all);
    			}
    		};
    	};

    	// Loop the updated items
    	for (i = 0; i < successArr.length; i++) {
    		// Check for existing item in the container
    		itemElem = container.find('#' + successArr[i][this._primaryKey]);

    		options.template(successArr[i], makeCallback(itemElem, successArr[i]));
    	}
    };

    OldView.prototype._bindRemove = function (selector, options, successArr, failArr, all) {
    	var container = window.jQuery(selector),
    		itemElem,
    		makeCallback,
    		i;

    	makeCallback = function (itemElem, data, all) {
    		return function () {
    			if (options.remove) {
    				options.remove(itemElem, data, all);
    			} else {
    				itemElem.remove();

    				if (options.afterRemove) {
    					options.afterRemove(itemElem, data, all);
    				}
    			}
    		};
    	};

    	// Loop the removed items
    	for (i = 0; i < successArr.length; i++) {
    		// Check for existing item in the container
    		itemElem = container.find('#' + successArr[i][this._primaryKey]);

    		if (itemElem.length) {
    			if (options.beforeRemove) {
    				options.beforeRemove(itemElem, successArr[i], all, makeCallback(itemElem, successArr[i], all));
    			} else {
    				if (options.remove) {
    					options.remove(itemElem, successArr[i], all);
    				} else {
    					itemElem.remove();

    					if (options.afterRemove) {
    						options.afterRemove(itemElem, successArr[i], all);
    					}
    				}
    			}
    		}
    	}
    };