/**
* FoxHound Query Generation Library
*
* @license MIT
*
* @author Steven Velozo <steven@velozo.com>
* @module FoxHound
*/

// We use Underscore.js for utility
var libUnderscore = require('underscore');

// Load our base parameters skeleton object
var baseParameters = require('./Parameters.js');

/**
* FoxHound Query Generation Library Main Class
*
* @class FoxHound
* @constructor
*/
var FoxHound = function()
{
	function createNew(pFable, pFromParameters)
	{
		// If a valid Fable object isn't passed in, return a constructor
		if ((typeof(pFable) !== 'object') || (!pFable.hasOwnProperty('fable')))
		{
			return {new: createNew};
		}

		var _Fable = pFable;

		// The default parameters config object, used as a template for all new
		// queries created from this query.
		var _DefaultParameters = (typeof(pFromParameters) === 'undefined') ? {} : pFromParameters;

		// The parameters config object for the current query.  This is the only
		// piece of internal state that is important to operation.
		var _Parameters = false;

		// The unique identifier for a query
		var _UUID = _Fable.getUUID();

		// The log level, for debugging chattiness.
		var _LogLevel = 0;

		// The dialect to use when generating queries
		var _Dialect = false;

		/**
		* Clone the current FoxHound Query into a new Query object, copying all
		* parameters as the new default.  Clone also copies the log level.
		*
		* @method clone
		* @return {Object} Returns a cloned Query.  This is still chainable.
		*/
		var clone = function()
		{
			var tmpFoxHound = createNew(_Fable, baseParameters)
				.setScope(_Parameters.scope)
				.setBegin(_Parameters.begin)
				.setCap(_Parameters.cap);

			// Schema is the only part of a query that carries forward.
			tmpFoxHound.query.schema = _Parameters.query.schema;

			if (_Parameters.dataElements)
			{
				tmpFoxHound.parameters.dataElements = _Parameters.dataElements.slice(); // Copy the array of dataElements
			}
			if (_Parameters.sort)
			{
				tmpFoxHound.parameters.sort = _Parameters.sort.slice(); // Copy the sort array.
				// TODO: Fix the side affect nature of these being objects in the array .. they are technically clones of the previous.
			}
			if (_Parameters.filter)
			{
				tmpFoxHound.parameters.filter = _Parameters.filter.slice(); // Copy the filter array.
				// TODO: Fix the side affect nature of these being objects in the array .. they are technically clones of the previous.
			}

			return tmpFoxHound;
		};


		/**
		* Reset the parameters of the FoxHound Query to the Default.  Default
		* parameters were set during object construction.
		*
		* @method resetParameters
		* @return {Object} Returns the current Query for chaining.
		*/
		var resetParameters = function()
		{
			_Parameters = libUnderscore.extend({}, baseParameters, _DefaultParameters);
			_Parameters.query = ({
				disableAutoIdentity: false,
				disableAutoDateStamp: false,
				disableAutoUserStamp: false,
				disableDeleteTracking: false,
				body: false,
				schema: false,   // The schema to intersect with our records
				IDUser: 0,       // The user to stamp into records
				UUID: _Fable.getUUID(), // A UUID for this record
				records: false,  // The records to be created or changed
				parameters: {}
			});

			_Parameters.result = ({
				executed: false, // True once we've run a query.
				value: false,    // The return value of the last query run
				error: false     // The error message of the last run query
			});

			return this;
		};
		resetParameters();
		
		/**
		* Reset the parameters of the FoxHound Query to the Default.  Default
		* parameters were set during object construction.
		*
		* @method mergeParameters
		* @param {Object} pFromParameters A Parameters Object to merge from
		* @return {Object} Returns the current Query for chaining.
		*/
		var mergeParameters = function(pFromParameters)
		{
			_Parameters = libUnderscore.extend({}, _Parameters, pFromParameters);
			return this;
		};


		/**
		* Set the the Logging level.
		*
		* The log levels are:
		*    0  -  Don't log anything
		*    1  -  Log queries
		*    2  -  Log queries and non-parameterized queries
		*    3  -  Log everything
		*
		* @method setLogLevel
		* @param {Number} pLogLevel The log level for our object
		* @return {Object} Returns the current Query for chaining.
		*/
		var setLogLevel = function(pLogLevel)
		{
			var tmpLogLevel = 0;

			if (typeof(pLogLevel) === 'number' && (pLogLevel % 1) === 0)
			{
				tmpLogLevel = pLogLevel;
			}

			_LogLevel = tmpLogLevel;

			return this;
		};


		/**
		* Set the Scope for the Query.  *Scope* is the source for the data being
		* pulled.  In TSQL this would be the _table_, whereas in MongoDB this
		* would be the _collection_.
		*
		* A scope can be either a string, or an array (for JOINs and such).
		*
		* @method setScope
		* @param {String} pScope A Scope for the Query.
		* @return {Object} Returns the current Query for chaining.
		*/
		var setScope = function(pScope)
		{
			var tmpScope = false;

			if (typeof(pScope) === 'string')
			{
				tmpScope = pScope;
			}
			else if (pScope !== false)
			{
				_Fable.log.error('Scope set failed.  You must pass in a string or array.', {queryUUID:_UUID, parameters:_Parameters, invalidScope:pScope});
			}

			_Parameters.scope = tmpScope;

			if (_LogLevel > 2)
			{
				_Fable.log.info('Scope set: '+tmpScope, {queryUUID:_UUID, parameters:_Parameters});
			}

			return this;
		};


		/**
		* Set the Data Elements for the Query.  *Data Elements* are the fields
		* being pulled by the query.  In TSQL this would be the _columns_,
		* whereas in MongoDB this would be the _fields_.
		*
		* The passed values can be either a string, or an array.
		*
		* @method setDataElements
		* @param {String} pDataElements The Data Element(s) for the Query.
		* @return {Object} Returns the current Query for chaining.
		*/
		var setDataElements = function(pDataElements)
		{
			var tmpDataElements = false;

			if (Array.isArray(pDataElements))
			{
				// TODO: Check each entry of the array are all strings
				tmpDataElements = pDataElements;
			}
			if (typeof(pDataElements) === 'string')
			{
				tmpDataElements = [pDataElements];
			}

			_Parameters.dataElements = tmpDataElements;

			if (_LogLevel > 2)
			{
				_Fable.log.info('Data Elements set', {queryUUID:_UUID, parameters:_Parameters});
			}

			return this;
		};


		/**
		* Set the sort data element
		*
		* The passed values can be either a string, an object or an array of objects.
		*
		* The Sort object has two values:
		* {Column:'Birthday', Direction:'Ascending'}
		*
		* @method setSort
		* @param {String} pSort The sort criteria(s) for the Query.
		* @return {Object} Returns the current Query for chaining.
		*/
		var setSort = function(pSort)
		{
			var tmpSort = false;

			if (Array.isArray(pSort))
			{
				// TODO: Check each entry of the array are all conformant sort objects
				tmpSort = pSort;
			}
			else if (typeof(pSort) === 'string')
			{
				// Default to ascending
				tmpSort = [{Column:pSort, Direction:'Ascending'}];
			}
			else if (typeof(pSort) === 'object')
			{
				// TODO: Check that this sort entry conforms to a sort entry
				tmpSort = [pSort];
			}

			_Parameters.sort = tmpSort;

			if (_LogLevel > 2)
			{
				_Fable.log.info('Sort set', {queryUUID:_UUID, parameters:_Parameters});
			}

			return this;
		};

		/**
		* Set the join data element
		*
		* The passed values can be either an object or an array of objects.
		*
		* The join object has four values:
		* {Type:'INNER JOIN', Table:'Test', From:'Test.ID', To:'Scope.IDItem'}
		*
		* @method setJoin
		* @param {Object} pJoin The join criteria(s) for the Query.
		* @return {Object} Returns the current Query for chaining.
		*/
		var setJoin = function(pJoin)
		{
			_Parameters.join = [];

			if (Array.isArray(pJoin))
			{
				pJoin.forEach(function(join)
				{
					addJoin(join.Table, join.From, join.To, join.Type);
				});
			}
			else if (typeof(pJoin) === 'object')
			{
				addJoin(pJoin.Table, pJoin.From, pJoin.To, pJoin.Type);
			}

			return this;
		};


		/**
		* Add a sort data element
		*
		* The passed values can be either a string, an object or an array of objects.
		*
		* The Sort object has two values:
		* {Column:'Birthday', Direction:'Ascending'}
		*
		* @method setSort
		* @param {String} pSort The sort criteria to add to the Query.
		* @return {Object} Returns the current Query for chaining.
		*/
		var addSort = function(pSort)
		{
			var tmpSort = false;

			if (typeof(pSort) === 'string')
			{
				// Default to ascending
				tmpSort = {Column:pSort, Direction:'Ascending'};
			}
			if (typeof(pSort) === 'object')
			{
				// TODO: Check that this sort entry conforms to a sort entry
				tmpSort = pSort;
			}

			if (!_Parameters.sort)
			{
				_Parameters.sort = [];
			}

			_Parameters.sort.push(tmpSort);

			if (_LogLevel > 2)
			{
				_Fable.log.info('Sort set', {queryUUID:_UUID, parameters:_Parameters});
			}

			return this;
		};


		/**
		* Set the the Begin index for the Query.  *Begin* is the index at which
		* a query should start returning rows.  In TSQL this would be the n
		* parameter of ```LIMIT 1,n```, whereas in MongoDB this would be the
		* n in ```skip(n)```.
		*
		* The passed value must be an Integer >= 0.
		*
		* @method setBegin
		* @param {Number} pBeginAmount The index to begin returning Query data.
		* @return {Object} Returns the current Query for chaining.
		*/
		var setBegin = function(pBeginAmount)
		{
			var tmpBegin = false;

			// Test if it is an integer > -1
			// http://jsperf.com/numbers-and-integers
			if (typeof(pBeginAmount) === 'number' && (pBeginAmount % 1) === 0 && pBeginAmount >= 0)
			{
				tmpBegin = pBeginAmount;
			}
			else if (pBeginAmount !== false)
			{
				_Fable.log.error('Begin set failed; non-positive or non-numeric argument.', {queryUUID:_UUID, parameters:_Parameters, invalidBeginAmount:pBeginAmount});
			}

			_Parameters.begin = tmpBegin;

			if (_LogLevel > 2)
			{
				_Fable.log.info('Begin set: '+pBeginAmount, {queryUUID:_UUID, parameters:_Parameters});
			}

			return this;
		};


		/**
		* Set the the Cap for the Query.  *Cap* is the maximum number of records
		* a Query should return in a set.  In TSQL this would be the n
		* parameter of ```LIMIT n```, whereas in MongoDB this would be the
		* n in ```limit(n)```.
		*
		* The passed value must be an Integer >= 0.
		*
		* @method setCap
		* @param {Number} pCapAmount The maximum records for the Query set.
		* @return {Object} Returns the current Query for chaining.
		*/
		var setCap = function(pCapAmount)
		{
			var tmpCapAmount = false;

			if (typeof(pCapAmount) === 'number' && (pCapAmount % 1) === 0 && pCapAmount >= 0)
			{
				tmpCapAmount = pCapAmount;
			}
			else if (pCapAmount !== false)
			{
				_Fable.log.error('Cap set failed; non-positive or non-numeric argument.', {queryUUID:_UUID, parameters:_Parameters, invalidCapAmount:pCapAmount});
			}


			_Parameters.cap = tmpCapAmount;

			if (_LogLevel > 2)
			{
				_Fable.log.info('Cap set to: '+tmpCapAmount, {queryUUID:_UUID, parameters:_Parameters});
			}

			return this;
		};


		/**
		* Set the filter expression
		*
		* The passed values can be either an object or an array of objects.
		*
		* The Filter object has a minimum of two values (which expands to the following):
		* {Column:'Name', Value:'John'}
		* {Column:'Name', Operator:'EQ', Value:'John', Connector:'And', Parameter:'Name'}
		*
		* @method setFilter
		* @param {String} pFilter The filter(s) for the Query.
		* @return {Object} Returns the current Query for chaining.
		*/
		var setFilter = function(pFilter)
		{
			var tmpFilter = false;

			if (Array.isArray(pFilter))
			{
				// TODO: Check each entry of the array are all conformant Filter objects
				tmpFilter = pFilter;
			}
			else if (typeof(pFilter) === 'object')
			{
				// TODO: Check that this Filter entry conforms to a Filter entry
				tmpFilter = [pFilter];
			}

			_Parameters.filter = tmpFilter;

			if (_LogLevel > 2)
			{
				_Fable.log.info('Filter set', {queryUUID:_UUID, parameters:_Parameters});
			}

			return this;
		};



		/**
		* Add a filter expression
		*
		* {Column:'Name', Operator:'EQ', Value:'John', Connector:'And', Parameter:'Name'}
		*
		* @method addFilter
		* @return {Object} Returns the current Query for chaining.
		*/
		var addFilter = function(pColumn, pValue, pOperator, pConnector, pParameter)
		{
			if (typeof(pColumn) !== 'string')
			{
				_Fable.log.warn('Tried to add an invalid query filter column', {queryUUID:_UUID, parameters:_Parameters});
				return this;
			}
			if (typeof(pValue) === 'undefined')
			{
				_Fable.log.warn('Tried to add an invalid query filter value', {queryUUID:_UUID, parameters:_Parameters, invalidColumn:pColumn});
				return this;
			}
			var tmpOperator = (typeof(pOperator) === 'undefined') ? '=' : pOperator;
			var tmpConnector = (typeof(pConnector) === 'undefined') ? 'AND' : pConnector;
			var tmpParameter = (typeof(pParameter) === 'undefined') ? pColumn : pParameter;

			//support table.field notation (mysql2 requires this)
			tmpParameter = tmpParameter.replace('.', '_');

			var tmpFilter = (
				{
					Column: pColumn,
					Operator: tmpOperator,
					Value: pValue,
					Connector: tmpConnector,
					Parameter: tmpParameter
				});

			if (!Array.isArray(_Parameters.filter))
			{
				_Parameters.filter = [tmpFilter];
			}
			else
			{
				_Parameters.filter.push(tmpFilter);
			}

			if (_LogLevel > 2)
			{
				_Fable.log.info('Added a filter', {queryUUID:_UUID, parameters:_Parameters, newFilter:tmpFilter});
			}

			return this;
		};

		/**
		* Add a join expression
		*
		* {Type:'INNER JOIN', Table:'Test', From:'Test.ID', To:'Scope.IDItem'}
		*
		* @method addJoin
		* @return {Object} Returns the current Query for chaining.
		*/
		var addJoin = function(pTable, pFrom, pTo, pType)
		{
			if (typeof(pTable) !== 'string')
			{
				_Fable.log.warn('Tried to add an invalid query join table', {queryUUID:_UUID, parameters:_Parameters});
				return this;
			}
			if (typeof(pFrom) === 'undefined' || typeof(pTo) === 'undefined')
			{
				_Fable.log.warn('Tried to add an invalid query join field', {queryUUID:_UUID, parameters:_Parameters});
				return this;
			}
			//sanity check the join fields
			if (pFrom.indexOf(pTable)!=0)
			{
				_Fable.log.warn('Tried to add an invalid query join field, join must come FROM the join table!', {queryUUID:_UUID, parameters:_Parameters, invalidField:pFrom});
				return this;
			}
			if (pTo.indexOf('.')<=0)
			{
				_Fable.log.warn('Tried to add an invalid query join field, join must go TO a field on another table ([table].[field])!', {queryUUID:_UUID, parameters:_Parameters, invalidField:pTo});
				return this;
			}
			
			var tmpType = (typeof(pType) === 'undefined') ?  'INNER JOIN' : pType;

			var tmpJoin = (
				{
					Type: tmpType,
					Table: pTable,
					From: pFrom,
					To: pTo
				});

			if (!Array.isArray(_Parameters.join))
			{
				_Parameters.join = [tmpJoin];
			}
			else
			{
				_Parameters.join.push(tmpJoin);
			}

			if (_LogLevel > 2)
			{
				_Fable.log.info('Added a join', {queryUUID:_UUID, parameters:_Parameters});
			}

			return this;
		};



		/**
		* Add a record (for UPDATE and INSERT)
		*
		*
		* @method addRecord
		* @param {Object} pRecord The record to add.
		* @return {Object} Returns the current Query for chaining.
		*/
		var addRecord = function(pRecord)
		{
			if (typeof(pRecord) !== 'object')
			{
				_Fable.log.warn('Tried to add an invalid record to the query -- records must be an object', {queryUUID:_UUID, parameters:_Parameters});
				return this;
			}

			if (!Array.isArray(_Parameters.query.records))
			{
				_Parameters.query.records = [pRecord];
			}
			else
			{
				_Parameters.query.records.push(pRecord);
			}

			if (_LogLevel > 2)
			{
				_Fable.log.info('Added a record to the query', {queryUUID:_UUID, parameters:_Parameters, newRecord:pRecord});
			}

			return this;
		};



		/**
		* Set the Dialect for Query generation.
		*
		* This function expects a string, case sensitive, which matches both the
		* folder and filename
		*
		* @method setDialect
		* @param {String} pDialectName The dialect for query generation.
		* @return {Object} Returns the current Query for chaining.
		*/
		var setDialect = function(pDialectName)
		{
			if (typeof(pDialectName) !== 'string')
			{
				_Fable.log.warn('Dialect set to English - invalid name', {queryUUID:_UUID, parameters:_Parameters, invalidDialect:pDialectName});
				return setDialect('English');
			}

			var tmpDialectModuleFile = './dialects/'+pDialectName+'/FoxHound-Dialect-'+pDialectName+'.js';

			try
			{
				var tmpDialectModule = require(tmpDialectModuleFile);
				_Dialect = tmpDialectModule;
				if (_LogLevel > 2)
				{
					_Fable.log.info('Dialog set to: '+pDialectName, {queryUUID:_UUID, parameters:_Parameters, dialectModuleFile:tmpDialectModuleFile});
				}
			}
			catch (pError)
			{
				_Fable.log.error('Dialect not set - require load problem', {queryUUID:_UUID, parameters:_Parameters, dialectModuleFile:tmpDialectModuleFile, invalidDialect:pDialectName, error:pError});
				setDialect('English');
			}

			return this;
		};

		/**
		* User to use for this query
		*
		* @method setIDUser
		*/
		var setIDUser = function(pIDUser)
		{

			var tmpUserID = 0;

			if (typeof(pIDUser) === 'number' && (pIDUser % 1) === 0 && pIDUser >= 0)
			{
				tmpUserID = pIDUser;
			}
			else if (pIDUser !== false)
			{
				_Fable.log.error('User set failed; non-positive or non-numeric argument.', {queryUUID:_UUID, parameters:_Parameters, invalidIDUser:pIDUser});
			}


			_Parameters.userID = tmpUserID;
			_Parameters.query.IDUser = tmpUserID;

			if (_LogLevel > 2)
			{
				_Fable.log.info('IDUser set to: '+tmpUserID, {queryUUID:_UUID, parameters:_Parameters});
			}

			return this;
		};

		/**
		* Flag to disable auto identity
		*
		* @method setDisableAutoIdentity
		*/
		var setDisableAutoIdentity = function(pFlag)
		{
			_Parameters.query.disableAutoIdentity = pFlag;

			return this; //chainable
		};

		/**
		* Flag to disable auto datestamp
		*
		* @method setDisableAutoDateStamp
		*/
		var setDisableAutoDateStamp = function(pFlag)
		{
			_Parameters.query.disableAutoDateStamp = pFlag;

			return this; //chainable
		};

		/**
		* Flag to disable auto userstamp
		*
		* @method setDisableAutoUserStamp
		*/
		var setDisableAutoUserStamp = function(pFlag)
		{
			_Parameters.query.disableAutoUserStamp = pFlag;

			return this; //chainable
		};

		/**
		* Flag to disable delete tracking
		*
		* @method setDisableDeleteTracking
		*/
		var setDisableDeleteTracking = function(pFlag)
		{
			_Parameters.query.disableDeleteTracking = pFlag;

			return this; //chainable
		};

		/**
		* Check that a valid Dialect has been set
		*
		* If there has not been a dialect set, it defaults to English.
		* TODO: Have the json configuration define a "default" dialect.
		*
		* @method checkDialect
		*/
		var checkDialect = function()
		{
			if (_Dialect === false)
			{
				setDialect('English');
			}
		};


		var buildCreateQuery = function()
		{
			checkDialect();
			_Parameters.query.body = _Dialect.Create(_Parameters);
			return this;
		};

		var buildReadQuery = function()
		{
			checkDialect();
			_Parameters.query.body = _Dialect.Read(_Parameters);
			return this;
		};

		var buildUpdateQuery = function()
		{
			checkDialect();
			_Parameters.query.body = _Dialect.Update(_Parameters);
			return this;
		};

		var buildDeleteQuery = function()
		{
			checkDialect();
			_Parameters.query.body = _Dialect.Delete(_Parameters);
			return this;
		};

		var buildCountQuery = function()
		{
			checkDialect();
			_Parameters.query.body = _Dialect.Count(_Parameters);
			return this;
		};

		/**
		* Container Object for our Factory Pattern
		*/
		var tmpNewFoxHoundObject = (
		{
			resetParameters: resetParameters,
			mergeParameters: mergeParameters,

			setLogLevel: setLogLevel,

			setScope: setScope,
			setIDUser: setIDUser,
			setDataElements: setDataElements,
			setBegin: setBegin,
			setCap: setCap,
			setFilter: setFilter,
			addFilter: addFilter,
			setSort: setSort,
			addSort: addSort,
			setJoin: setJoin,
			addJoin: addJoin,

			addRecord: addRecord,
			setDisableAutoIdentity: setDisableAutoIdentity,
			setDisableAutoDateStamp: setDisableAutoDateStamp,
			setDisableAutoUserStamp: setDisableAutoUserStamp,
			setDisableDeleteTracking: setDisableDeleteTracking,

			setDialect: setDialect,

			buildCreateQuery: buildCreateQuery,
			buildReadQuery: buildReadQuery,
			buildUpdateQuery: buildUpdateQuery,
			buildDeleteQuery: buildDeleteQuery,
			buildCountQuery: buildCountQuery,

			clone: clone,
			new: createNew
		});

		/**
		 * Query
		 *
		 * @property query
		 * @type Object
		 */
		Object.defineProperty(tmpNewFoxHoundObject, 'query',
			{
				get: function() { return _Parameters.query; },
				set: function(pQuery) { _Parameters.query = pQuery; },
				enumerable: true
			});

		/**
		 * Result
		 *
		 * @property result
		 * @type Object
		 */
		Object.defineProperty(tmpNewFoxHoundObject, 'result',
			{
				get: function() { return _Parameters.result; },
				set: function(pResult) { _Parameters.result = pResult; },
				enumerable: true
			});

		/**
		 * Query Parameters
		 *
		 * @property parameters
		 * @type Object
		 */
		Object.defineProperty(tmpNewFoxHoundObject, 'parameters',
			{
				get: function() { return _Parameters; },
				set: function(pParameters) { _Parameters = pParameters; },
				enumerable: true
			});

		/**
		 * Dialect
		 *
		 * @property dialect
		 * @type Object
		 */
		Object.defineProperty(tmpNewFoxHoundObject, 'dialect',
			{
				get: function() { return _Dialect; },
				enumerable: true
			});

		/**
		 * Universally Unique Identifier
		 *
		 * @property uuid
		 * @type String
		 */
		Object.defineProperty(tmpNewFoxHoundObject, 'uuid',
			{
				get: function() { return _UUID; },
				enumerable: true
			});

		/**
		 * Log Level
		 *
		 * @property logLevel
		 * @type Integer
		 */
		Object.defineProperty(tmpNewFoxHoundObject, 'logLevel',
			{
				get: function() { return _LogLevel; },
				enumerable: true
			});

		return tmpNewFoxHoundObject;
	}

	return createNew();
};

module.exports = new FoxHound();
