# Source: BinaryTree.js

    "use strict";

    var Shared = require('./Shared'),
    	Path = require('./Path'),
    	sharedPathSolver = new Path();

    var BinaryTree = function (data, compareFunc, hashFunc) {
    	this.init.apply(this, arguments);
    };

    BinaryTree.prototype.init = function (data, index, primaryKey, compareFunc, hashFunc) {
    	this._store = [];
    	this._keys = [];

    	if (primaryKey !== undefined) { this.primaryKey(primaryKey); }
    	if (index !== undefined) { this.index(index); }
    	if (compareFunc !== undefined) { this.compareFunc(compareFunc); }
    	if (hashFunc !== undefined) { this.hashFunc(hashFunc); }
    	if (data !== undefined) { this.data(data); }
    };

    Shared.addModule('BinaryTree', BinaryTree);
    Shared.mixin(BinaryTree.prototype, 'Mixin.ChainReactor');
    Shared.mixin(BinaryTree.prototype, 'Mixin.Sorting');
    Shared.mixin(BinaryTree.prototype, 'Mixin.Common');

    Shared.synthesize(BinaryTree.prototype, 'compareFunc');
    Shared.synthesize(BinaryTree.prototype, 'hashFunc');
    Shared.synthesize(BinaryTree.prototype, 'indexDir');
    Shared.synthesize(BinaryTree.prototype, 'primaryKey');
    Shared.synthesize(BinaryTree.prototype, 'keys');
    Shared.synthesize(BinaryTree.prototype, 'index', function (index) {
    	if (index !== undefined) {
    		if (this.debug()) {
    			console.log('Setting index', index, sharedPathSolver.parse(index, true));
    		}

    		// Convert the index object to an array of key val objects
    		this.keys(sharedPathSolver.parse(index, true));
    	}

    	return this.$super.call(this, index);
    });

    /**
     * Remove all data from the binary tree.
     */
    BinaryTree.prototype.clear = function () {
    	delete this._data;
    	delete this._left;
    	delete this._right;

    	this._store = [];
    };

    /**
     * Sets this node's data object. All further inserted documents that
     * match this node's key and value will be pushed via the push()
     * method into the this._store array. When deciding if a new data
     * should be created left, right or middle (pushed) of this node the
     * new data is checked against the data set via this method.
     * @param val
     * @returns {*}
     */
    BinaryTree.prototype.data = function (val) {
    	if (val !== undefined) {
    		this._data = val;

    		if (this._hashFunc) { this._hash = this._hashFunc(val); }
    		return this;
    	}

    	return this._data;
    };

    /**
     * Pushes an item to the binary tree node's store array.
     * @param {*} val The item to add to the store.
     * @returns {*}
     */
    BinaryTree.prototype.push = function (val) {
    	if (val !== undefined) {
    		this._store.push(val);
    		return this;
    	}

    	return false;
    };

    /**
     * Pulls an item from the binary tree node's store array.
     * @param {*} val The item to remove from the store.
     * @returns {*}
     */
    BinaryTree.prototype.pull = function (val) {
    	if (val !== undefined) {
    		var index = this._store.indexOf(val);

    		if (index > -1) {
    			this._store.splice(index, 1);
    			return this;
    		}
    	}

    	return false;
    };

    /**
     * Default compare method. Can be overridden.
     * @param a
     * @param b
     * @returns {Number}
     * @private
     */
    BinaryTree.prototype._compareFunc = function (a, b) {
    	// Loop the index array
    	var i,
    		indexData,
    		result = 0;

    	for (i = 0; i < this._keys.length; i++) {
    		indexData = this._keys[i];

    		if (indexData.value === 1) {
    			result = this.sortAscIgnoreUndefined(sharedPathSolver.get(a, indexData.path), sharedPathSolver.get(b, indexData.path));
    		} else if (indexData.value === -1) {
    			result = this.sortDescIgnoreUndefined(sharedPathSolver.get(a, indexData.path), sharedPathSolver.get(b, indexData.path));
    		}

    		if (this.debug()) {
    			console.log('Compared %s with %s order %d in path %s and result was %d', sharedPathSolver.get(a, indexData.path), sharedPathSolver.get(b, indexData.path), indexData.value, indexData.path, result);
    		}

    		if (result !== 0) {
    			if (this.debug()) {
    				console.log('Retuning result %d', result);
    			}
    			return result;
    		}
    	}

    	if (this.debug()) {
    		console.log('Retuning result %d', result);
    	}

    	return result;
    };

    /**
     * Default hash function. Can be overridden.
     * @param obj
     * @private
     */
    BinaryTree.prototype._hashFunc = function (obj) {
    	/*var i,
    		indexData,
    		hash = '';

    	for (i = 0; i < this._keys.length; i++) {
    		indexData = this._keys[i];

    		if (hash) { hash += '_'; }
    		hash += obj[indexData.path];
    	}

    	return hash;*/

    	return obj[this._keys[0].path];
    };

    /**
     * Removes (deletes reference to) either left or right child if the passed
     * node matches one of them.
     * @param {BinaryTree} node The node to remove.
     */
    BinaryTree.prototype.removeChildNode = function (node) {
    	if (this._left === node) {
    		// Remove left
    		delete this._left;
    	} else if (this._right === node) {
    		// Remove right
    		delete this._right;
    	}
    };

    /**
     * Returns the branch this node matches (left or right).
     * @param node
     * @returns {String}
     */
    BinaryTree.prototype.nodeBranch = function (node) {
    	if (this._left === node) {
    		return 'left';
    	} else if (this._right === node) {
    		return 'right';
    	}
    };

    /**
     * Inserts a document into the binary tree.
     * @param data
     * @returns {*}
     */
    BinaryTree.prototype.insert = function (data) {
    	var result,
    		inserted,
    		failed,
    		i;

    	if (data instanceof Array) {
    		// Insert array of data
    		inserted = [];
    		failed = [];

    		for (i = 0; i < data.length; i++) {
    			if (this.insert(data[i])) {
    				inserted.push(data[i]);
    			} else {
    				failed.push(data[i]);
    			}
    		}

    		return {
    			inserted: inserted,
    			failed: failed
    		};
    	}

    	if (this.debug()) {
    		console.log('Inserting', data);
    	}

    	if (!this._data) {
    		if (this.debug()) {
    			console.log('Node has no data, setting data', data);
    		}
    		// Insert into this node (overwrite) as there is no data
    		this.data(data);
    		//this.push(data);
    		return true;
    	}

    	result = this._compareFunc(this._data, data);

    	if (result === 0) {
    		if (this.debug()) {
    			console.log('Data is equal (currrent, new)', this._data, data);
    		}

    		//this.push(data);

    		// Less than this node
    		if (this._left) {
    			// Propagate down the left branch
    			this._left.insert(data);
    		} else {
    			// Assign to left branch
    			this._left = new BinaryTree(data, this._index, this._binaryTree, this._compareFunc, this._hashFunc);
    			this._left._parent = this;
    		}

    		return true;
    	}

    	if (result === -1) {
    		if (this.debug()) {
    			console.log('Data is greater (currrent, new)', this._data, data);
    		}

    		// Greater than this node
    		if (this._right) {
    			// Propagate down the right branch
    			this._right.insert(data);
    		} else {
    			// Assign to right branch
    			this._right = new BinaryTree(data, this._index, this._binaryTree, this._compareFunc, this._hashFunc);
    			this._right._parent = this;
    		}

    		return true;
    	}

    	if (result === 1) {
    		if (this.debug()) {
    			console.log('Data is less (currrent, new)', this._data, data);
    		}

    		// Less than this node
    		if (this._left) {
    			// Propagate down the left branch
    			this._left.insert(data);
    		} else {
    			// Assign to left branch
    			this._left = new BinaryTree(data, this._index, this._binaryTree, this._compareFunc, this._hashFunc);
    			this._left._parent = this;
    		}

    		return true;
    	}

    	return false;
    };

    BinaryTree.prototype.remove = function (data) {
    	var pk = this.primaryKey(),
    		result,
    		removed,
    		i;

    	if (data instanceof Array) {
    		// Insert array of data
    		removed = [];

    		for (i = 0; i < data.length; i++) {
    			if (this.remove(data[i])) {
    				removed.push(data[i]);
    			}
    		}

    		return removed;
    	}

    	if (this.debug()) {
    		console.log('Removing', data);
    	}

    	if (this._data[pk] === data[pk]) {
    		// Remove this node
    		return this._remove(this);
    	}

    	// Compare the data to work out which branch to send the remove command down
    	result = this._compareFunc(this._data, data);

    	if (result === -1 && this._right) {
    		return this._right.remove(data);
    	}

    	if (result === 1 && this._left) {
    		return this._left.remove(data);
    	}

    	return false;
    };

    BinaryTree.prototype._remove = function (node) {
    	var leftNode,
    		rightNode;

    	if (this._left) {
    		// Backup branch data
    		leftNode = this._left;
    		rightNode = this._right;

    		// Copy data from left node
    		this._left = leftNode._left;
    		this._right = leftNode._right;
    		this._data = leftNode._data;
    		this._store = leftNode._store;

    		if (rightNode) {
    			// Attach the rightNode data to the right-most node
    			// of the leftNode
    			leftNode.rightMost()._right = rightNode;
    		}
    	} else if (this._right) {
    		// Backup branch data
    		rightNode = this._right;

    		// Copy data from right node
    		this._left = rightNode._left;
    		this._right = rightNode._right;
    		this._data = rightNode._data;
    		this._store = rightNode._store;
    	} else {
    		this.clear();
    	}

    	return true;
    };

    BinaryTree.prototype.leftMost = function () {
    	if (!this._left) {
    		return this;
    	} else {
    		return this._left.leftMost();
    	}
    };

    BinaryTree.prototype.rightMost = function () {
    	if (!this._right) {
    		return this;
    	} else {
    		return this._right.rightMost();
    	}
    };

    /**
     * Searches the binary tree for all matching documents based on the data
     * passed (query).
     * @param {Object} data The data / document to use for lookups.
     * @param {Object} options An options object.
     * @param {Operation} op An optional operation instance. Pass undefined
     * if not being used.
     * @param {Array=} resultArr The results passed between recursive calls.
     * Do not pass anything into this argument when calling externally.
     * @returns {*|Array}
     */
    BinaryTree.prototype.lookup = function (data, options, op, resultArr) {
    	var result = this._compareFunc(this._data, data);

    	resultArr = resultArr || [];

    	if (result === 0) {
    		if (this._left) { this._left.lookup(data, options, op, resultArr); }
    		resultArr.push(this._data);
    		if (this._right) { this._right.lookup(data, options, op, resultArr); }
    	}

    	if (result === -1) {
    		if (this._right) { this._right.lookup(data, options, op, resultArr); }
    	}

    	if (result === 1) {
    		if (this._left) { this._left.lookup(data, options, op, resultArr); }
    	}

    	return resultArr;
    };

    /**
     * Returns the entire binary tree ordered.
     * @param {String} type
     * @param resultArr
     * @returns {*|Array}
     */
    BinaryTree.prototype.inOrder = function (type, resultArr) {
    	resultArr = resultArr || [];

    	if (this._left) {
    		this._left.inOrder(type, resultArr);
    	}

    	switch (type) {
    		case 'hash':
    			resultArr.push(this._hash);
    			break;

    		case 'data':
    			resultArr.push(this._data);
    			break;

    		default:
    			resultArr.push({
    				key: this._data,
    				arr: this._store
    			});
    			break;
    	}

    	if (this._right) {
    		this._right.inOrder(type, resultArr);
    	}

    	return resultArr;
    };

    /**
     * Searches the binary tree for all matching documents based on the regular
     * expression passed.
     * @param path
     * @param val
     * @param regex
     * @param {Array=} resultArr The results passed between recursive calls.
     * Do not pass anything into this argument when calling externally.
     * @returns {*|Array}
     */
    BinaryTree.prototype.startsWith = function (path, val, regex, resultArr) {
    	var reTest,
    		thisDataPathVal = sharedPathSolver.get(this._data, path),
    		thisDataPathValSubStr = thisDataPathVal.substr(0, val.length),
    		result;

    	//regex = regex || new RegExp('^' + val);
    	resultArr = resultArr || [];

    	if (resultArr._visitedCount === undefined) { resultArr._visitedCount = 0; }
    	resultArr._visitedCount++;
    	resultArr._visitedNodes = resultArr._visitedNodes || [];
    	resultArr._visitedNodes.push(thisDataPathVal);

    	result = this.sortAscIgnoreUndefined(thisDataPathValSubStr, val);
    	reTest = thisDataPathValSubStr === val;

    	if (result === 0) {
    		if (this._left) { this._left.startsWith(path, val, regex, resultArr); }
    		if (reTest) { resultArr.push(this._data); }
    		if (this._right) { this._right.startsWith(path, val, regex, resultArr); }
    	}

    	if (result === -1) {
    		if (reTest) { resultArr.push(this._data); }
    		if (this._right) { this._right.startsWith(path, val, regex, resultArr); }
    	}

    	if (result === 1) {
    		if (this._left) { this._left.startsWith(path, val, regex, resultArr); }
    		if (reTest) { resultArr.push(this._data); }
    	}

    	return resultArr;
    };

    /*BinaryTree.prototype.find = function (type, search, resultArr) {
    	resultArr = resultArr || [];

    	if (this._left) {
    		this._left.find(type, search, resultArr);
    	}

    	// Check if this node's data is greater or less than the from value
    	var fromResult = this.sortAsc(this._data[key], from),
    			toResult = this.sortAsc(this._data[key], to);

    	if ((fromResult === 0 || fromResult === 1) && (toResult === 0 || toResult === -1)) {
    		// This data node is greater than or equal to the from value,
    		// and less than or equal to the to value so include it
    		switch (type) {
    			case 'hash':
    				resultArr.push(this._hash);
    				break;

    			case 'data':
    				resultArr.push(this._data);
    				break;

    			default:
    				resultArr.push({
    					key: this._data,
    					arr: this._store
    				});
    				break;
    		}
    	}

    	if (this._right) {
    		this._right.find(type, search, resultArr);
    	}

    	return resultArr;
    };*/

    /**
     *
     * @param {String} type
     * @param {String} key The data key / path to range search against.
     * @param {Number} from Range search from this value (inclusive)
     * @param {Number} to Range search to this value (inclusive)
     * @param {Array=} resultArr Leave undefined when calling (internal use),
     * passes the result array between recursive calls to be returned when
     * the recursion chain completes.
     * @param {Path=} pathResolver Leave undefined when calling (internal use),
     * caches the path resolver instance for performance.
     * @returns {Array} Array of matching document objects
     */
    BinaryTree.prototype.findRange = function (type, key, from, to, resultArr, pathResolver) {
    	resultArr = resultArr || [];
    	pathResolver = pathResolver || new Path(key);

    	if (this._left) {
    		this._left.findRange(type, key, from, to, resultArr, pathResolver);
    	}

    	// Check if this node's data is greater or less than the from value
    	var pathVal = pathResolver.value(this._data),
    		fromResult = this.sortAscIgnoreUndefined(pathVal, from),
    		toResult = this.sortAscIgnoreUndefined(pathVal, to);

    	if ((fromResult === 0 || fromResult === 1) && (toResult === 0 || toResult === -1)) {
    		// This data node is greater than or equal to the from value,
    		// and less than or equal to the to value so include it
    		switch (type) {
    			case 'hash':
    				resultArr.push(this._hash);
    				break;

    			case 'data':
    				resultArr.push(this._data);
    				break;

    			default:
    				resultArr.push({
    					key: this._data,
    					arr: this._store
    				});
    				break;
    		}
    	}

    	if (this._right) {
    		this._right.findRange(type, key, from, to, resultArr, pathResolver);
    	}

    	return resultArr;
    };

    /*BinaryTree.prototype.findRegExp = function (type, key, pattern, resultArr) {
    	resultArr = resultArr || [];

    	if (this._left) {
    		this._left.findRegExp(type, key, pattern, resultArr);
    	}

    	// Check if this node's data is greater or less than the from value
    	var fromResult = this.sortAsc(this._data[key], from),
    			toResult = this.sortAsc(this._data[key], to);

    	if ((fromResult === 0 || fromResult === 1) && (toResult === 0 || toResult === -1)) {
    		// This data node is greater than or equal to the from value,
    		// and less than or equal to the to value so include it
    		switch (type) {
    			case 'hash':
    				resultArr.push(this._hash);
    				break;

    			case 'data':
    				resultArr.push(this._data);
    				break;

    			default:
    				resultArr.push({
    					key: this._data,
    					arr: this._store
    				});
    				break;
    		}
    	}

    	if (this._right) {
    		this._right.findRegExp(type, key, pattern, resultArr);
    	}

    	return resultArr;
    };*/

    /**
     * Determines if the passed query and options object will be served
     * by this index successfully or not and gives a score so that the
     * DB search system can determine how useful this index is in comparison
     * to other indexes on the same collection.
     * @param query
     * @param queryOptions
     * @param matchOptions
     * @returns {{matchedKeys: Array, totalKeyCount: Number, score: number}}
     */
    BinaryTree.prototype.match = function (query, queryOptions, matchOptions) {
    	// Check if the passed query has data in the keys our index
    	// operates on and if so, is the query sort matching our order
    	var indexKeyArr,
    		queryArr,
    		matchedKeys = [],
    		matchedKeyCount = 0,
    		i;

    	indexKeyArr = sharedPathSolver.parseArr(this._index, {
    		verbose: true
    	});

    	queryArr = sharedPathSolver.parseArr(query, matchOptions && matchOptions.pathOptions ? matchOptions.pathOptions : {
    		ignore:/\$/,
    		verbose: true
    	});

    	// Loop the query array and check the order of keys against the
    	// index key array to see if this index can be used
    	for (i = 0; i < indexKeyArr.length; i++) {
    		if (queryArr[i] === indexKeyArr[i]) {
    			matchedKeyCount++;
    			matchedKeys.push(queryArr[i]);
    		}
    	}

    	return {
    		matchedKeys: matchedKeys,
    		totalKeyCount: queryArr.length,
    		score: matchedKeyCount
    	};

    	//return sharedPathSolver.countObjectPaths(this._keys, query);
    };

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