# Source: Path.js

    "use strict";

    var Shared = require('./Shared');

    /**
     * Path object used to resolve object paths and retrieve data from
     * objects by using paths.
     * @param {String=} path The path to assign.
     * @constructor
     */
    var Path = function (path) {
    	this.init.apply(this, arguments);
    };

    Path.prototype.init = function (path) {
    	if (path) {
    		this.path(path);
    	}
    };

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

    /**
     * Gets / sets the given path for the Path instance.
     * @param {String=} path The path to assign.
     */
    Path.prototype.path = function (path) {
    	if (path !== undefined) {
    		this._path = this.clean(path);
    		this._pathParts = this._path.split('.');
    		return this;
    	}

    	return this._path;
    };

    /**
     * Tests if the passed object has the paths that are specified and that
     * a value exists in those paths.
     * @param {Object} testKeys The object describing the paths to test for.
     * @param {Object} testObj The object to test paths against.
     * @returns {Boolean} True if the object paths exist.
     */
    Path.prototype.hasObjectPaths = function (testKeys, testObj) {
    	var result = true,
    		i;

    	for (i in testKeys) {
    		if (testKeys.hasOwnProperty(i)) {
    			if (testObj[i] === undefined) {
    				return false;
    			}

    			if (typeof testKeys[i] === 'object') {
    				// Recurse object
    				result = this.hasObjectPaths(testKeys[i], testObj[i]);

    				// Should we exit early?
    				if (!result) {
    					return false;
    				}
    			}
    		}
    	}

    	return result;
    };

    /**
     * Counts the total number of key endpoints in the passed object.
     * @param {Object} testObj The object to count key endpoints for.
     * @returns {Number} The number of endpoints.
     */
    Path.prototype.countKeys = function (testObj) {
    	var totalKeys = 0,
    		i;

    	for (i in testObj) {
    		if (testObj.hasOwnProperty(i)) {
    			if (testObj[i] !== undefined) {
    				if (typeof testObj[i] !== 'object') {
    					totalKeys++;
    				} else {
    					totalKeys += this.countKeys(testObj[i]);
    				}
    			}
    		}
    	}

    	return totalKeys;
    };

    /**
     * Tests if the passed object has the paths that are specified and that
     * a value exists in those paths and if so returns the number matched.
     * @param {Object} testKeys The object describing the paths to test for.
     * @param {Object} testObj The object to test paths against.
     * @returns {Object} Stats on the matched keys
     */
    Path.prototype.countObjectPaths = function (testKeys, testObj) {
    	var matchData,
    		matchedKeys = {},
    		matchedKeyCount = 0,
    		totalKeyCount = 0,
    		i;

    	for (i in testObj) {
    		if (testObj.hasOwnProperty(i)) {
    			if (typeof testObj[i] === 'object') {
    				// The test / query object key is an object, recurse
    				matchData = this.countObjectPaths(testKeys[i], testObj[i]);

    				matchedKeys[i] = matchData.matchedKeys;
    				totalKeyCount += matchData.totalKeyCount;
    				matchedKeyCount += matchData.matchedKeyCount;
    			} else {
    				// The test / query object has a property that is not an object so add it as a key
    				totalKeyCount++;

    				// Check if the test keys also have this key and it is also not an object
    				if (testKeys && testKeys[i] && typeof testKeys[i] !== 'object') {
    					matchedKeys[i] = true;
    					matchedKeyCount++;
    				} else {
    					matchedKeys[i] = false;
    				}
    			}
    		}
    	}

    	return {
    		matchedKeys: matchedKeys,
    		matchedKeyCount: matchedKeyCount,
    		totalKeyCount: totalKeyCount
    	};
    };

    /**
     * Takes a non-recursive object and converts the object hierarchy into
     * a path string.
     * @param {Object} obj The object to parse.
     * @param {Boolean=} withValue If true will include a 'value' key in the returned
     * object that represents the value the object path points to.
     * @returns {Object}
     */
    Path.prototype.parse = function (obj, withValue) {
    	var paths = [],
    		path = '',
    		resultData,
    		i, k;

    	for (i in obj) {
    		if (obj.hasOwnProperty(i)) {
    			// Set the path to the key
    			path = i;

    			if (typeof(obj[i]) === 'object') {
    				if (withValue) {
    					resultData = this.parse(obj[i], withValue);

    					for (k = 0; k < resultData.length; k++) {
    						paths.push({
    							path: path + '.' + resultData[k].path,
    							value: resultData[k].value
    						});
    					}
    				} else {
    					resultData = this.parse(obj[i]);

    					for (k = 0; k < resultData.length; k++) {
    						paths.push({
    							path: path + '.' + resultData[k].path
    						});
    					}
    				}
    			} else {
    				if (withValue) {
    					paths.push({
    						path: path,
    						value: obj[i]
    					});
    				} else {
    					paths.push({
    						path: path
    					});
    				}
    			}
    		}
    	}

    	return paths;
    };

    /**
     * Takes a non-recursive object and converts the object hierarchy into
     * an array of path strings that allow you to target all possible paths
     * in an object.
     *
     * The options object accepts an "ignore" field with a regular expression
     * as the value. If any key matches the expression it is not included in
     * the results.
     *
     * The options object accepts a boolean "verbose" field. If set to true
     * the results will include all paths leading up to endpoints as well as
     * they endpoints themselves.
     *
     * @returns {Array}
     */
    Path.prototype.parseArr = function (obj, options) {
    	options = options || {};
    	return this._parseArr(obj, '', [], options);
    };

    Path.prototype._parseArr = function (obj, path, paths, options) {
    	var i,
    		newPath = '';

    	path = path || '';
    	paths = paths || [];

    	for (i in obj) {
    		if (obj.hasOwnProperty(i)) {
    			if (!options.ignore || (options.ignore && !options.ignore.test(i))) {
    				if (path) {
    					newPath = path + '.' + i;
    				} else {
    					newPath = i;
    				}

    				if (typeof(obj[i]) === 'object') {
    					if (options.verbose) {
    						paths.push(newPath);
    					}

    					this._parseArr(obj[i], newPath, paths, options);
    				} else {
    					paths.push(newPath);
    				}
    			}
    		}
    	}

    	return paths;
    };

    /**
     * Sets a value on an object for the specified path.
     * @param {Object} obj The object to update.
     * @param {String} path The path to update.
     * @param {*} val The value to set the object path to.
     * @returns {*}
     */
    Path.prototype.set = function (obj, path, val) {
    	if (obj !== undefined && path !== undefined) {
    		var pathParts,
    				part;

    		path = this.clean(path);
    		pathParts = path.split('.');

    		part = pathParts.shift();

    		if (pathParts.length) {
    			// Generate the path part in the object if it does not already exist
    			obj[part] = obj[part] || {};

    			// Recurse
    			this.set(obj[part], pathParts.join('.'), val);
    		} else {
    			// Set the value
    			obj[part] = val;
    		}
    	}

    	return obj;
    };

    /**
     * Retrieves all the values inside an object based on the passed
     * path string. Will automatically traverse any arrays it encounters
     * and assumes array indexes are not part of the specifed path.
     * @param {Object|Array} obj An object or array of objects to
     * scan paths for.
     * @param {String} path The path string delimited by a period.
     * @return {Array} An array of values found at the end of each path
     * termination.
     */
    Path.prototype.aggregate = function (obj, path) {
    	var pathParts,
    		part,
    		values = [],
    		i;

    	// First, check if the object we are given
    	// is an array. If so, loop it and work on
    	// the objects inside
    	if (obj instanceof Array) {
    		// Loop array and get path data from each sub object
    		for (i = 0; i < obj.length; i++) {
    			values = values.concat(this.aggregate(obj[i], path));
    		}

    		return values;
    	}

    	if (path.indexOf('.') === -1) {
    		// No further parts to navigate
    		// Return an array so the value can be concatenated on return via array.concat()
    		return [obj[path]];
    	}

    	pathParts = path.split('.');

    	// Grab the next part of our path
    	part = pathParts.shift();
    	values = values.concat(this.aggregate(obj[part], pathParts.join('.')));

    	return values;
    };

    /**
     * Gets a single value from the passed object and given path.
     * @param {Object} obj The object to inspect.
     * @param {String} path The path to retrieve data from.
     * @returns {*}
     */
    Path.prototype.get = function (obj, path) {
    	return this.value(obj, path)[0];
    };

    /**
     * Gets the value(s) that the object contains for the currently assigned path string.
     * @param {Object} obj The object to evaluate the path against.
     * @param {String=} path A path to use instead of the existing one passed in path().
     * @param {Object=} options An optional options object.
     * @returns {Array} An array of values for the given path.
     */
    Path.prototype.value = function (obj, path, options) {
    	var pathParts,
    		arr,
    		arrCount,
    		objPart,
    		objPartParent,
    		valuesArr,
    		returnArr,
    		i, k;

    	// Detect early exit
    	if (path && path.indexOf('.') === -1) {
    		if (options && options.skipArrCheck) {
    			return [obj[path]];
    		}

    		if (!(obj instanceof Array)) {
    			return [obj[path]];
    		}
    	}

    	if (obj !== undefined && typeof obj === 'object') {
    		if (!options || options && !options.skipArrCheck) {
    			// Check if we were passed an array of objects and if so,
    			// iterate over the array and return the value from each
    			// array item
    			if (obj instanceof Array) {
    				returnArr = [];

    				for (i = 0; i < obj.length; i++) {
    					returnArr.push(this.get(obj[i], path));
    				}

    				return returnArr;
    			}
    		}

    		valuesArr = [];

    		if (path !== undefined) {
    			path = this.clean(path);
    			pathParts = path.split('.');
    		}

    		arr = pathParts || this._pathParts;
    		arrCount = arr.length;
    		objPart = obj;

    		for (i = 0; i < arrCount; i++) {
    			objPart = objPart[arr[i]];

    			if (objPartParent instanceof Array) {
    				// Search inside the array for the next key
    				for (k = 0; k < objPartParent.length; k++) {
    					valuesArr = valuesArr.concat(this.value(objPartParent, k + '.' + arr[i], {skipArrCheck: true}));
    				}

    				return valuesArr;
    			} else {
    				if (!objPart || typeof(objPart) !== 'object') {
    					break;
    				}
    			}

    			objPartParent = objPart;
    		}

    		return [objPart];
    	} else {
    		return [];
    	}
    };

    /**
     * Push a value to an array on an object for the specified path.
     * @param {Object} obj The object to update.
     * @param {String} path The path to the array to push to.
     * @param {*} val The value to push to the array at the object path.
     * @returns {*}
     */
    Path.prototype.push = function (obj, path, val) {
    	if (obj !== undefined && path !== undefined) {
    		var pathParts,
    			part;

    		path = this.clean(path);
    		pathParts = path.split('.');

    		part = pathParts.shift();

    		if (pathParts.length) {
    			// Generate the path part in the object if it does not already exist
    			obj[part] = obj[part] || {};

    			// Recurse
    			this.set(obj[part], pathParts.join('.'), val);
    		} else {
    			// Set the value
    			obj[part] = obj[part] || [];

    			if (obj[part] instanceof Array) {
    				obj[part].push(val);
    			} else {
    				throw('ForerunnerDB.Path: Cannot push to a path whose endpoint is not an array!');
    			}
    		}
    	}

    	return obj;
    };

    /**
     * Gets the value(s) that the object contains for the currently assigned path string
     * with their associated keys.
     * @param {Object} obj The object to evaluate the path against.
     * @param {String=} path A path to use instead of the existing one passed in path().
     * @returns {Array} An array of values for the given path with the associated key.
     */
    Path.prototype.keyValue = function (obj, path) {
    	var pathParts,
    		arr,
    		arrCount,
    		objPart,
    		objPartParent,
    		objPartHash,
    		i;

    	if (path !== undefined) {
    		path = this.clean(path);
    		pathParts = path.split('.');
    	}

    	arr = pathParts || this._pathParts;
    	arrCount = arr.length;
    	objPart = obj;

    	for (i = 0; i < arrCount; i++) {
    		objPart = objPart[arr[i]];

    		if (!objPart || typeof(objPart) !== 'object') {
    			objPartHash = arr[i] + ':' + objPart;
    			break;
    		}

    		objPartParent = objPart;
    	}

    	return objPartHash;
    };

    /**
     * Sets a value on an object for the specified path.
     * @param {Object} obj The object to update.
     * @param {String} path The path to update.
     * @param {*} val The value to set the object path to.
     * @returns {*}
     */
    Path.prototype.set = function (obj, path, val) {
    	if (obj !== undefined && path !== undefined) {
    		var pathParts,
    			part;

    		path = this.clean(path);
    		pathParts = path.split('.');

    		part = pathParts.shift();

    		if (pathParts.length) {
    			// Generate the path part in the object if it does not already exist
    			obj[part] = obj[part] || {};

    			// Recurse
    			this.set(obj[part], pathParts.join('.'), val);
    		} else {
    			// Set the value
    			obj[part] = val;
    		}
    	}

    	return obj;
    };

    /**
     * Removes leading period (.) from string and returns it.
     * @param {String} str The string to clean.
     * @returns {*}
     */
    Path.prototype.clean = function (str) {
    	if (str.substr(0, 1) === '.') {
    		str = str.substr(1, str.length -1);
    	}

    	return str;
    };

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