(function(F){

	F.Query = F.Class.extend(/** @lends FooTable.Query */{

		/**

		 * The query object is used to parse and test the filtering component's queries

		 * @constructs

		 * @extends FooTable.Class

		 * @param {string} query - The string value of the query.

		 * @param {string} [space="AND"] - How the query treats whitespace.

		 * @param {boolean} [connectors=true] - Whether or not to replace phrase connectors (+.-_) with spaces.

		 * @param {boolean} [ignoreCase=true] - Whether or not ignore case when matching.

		 * @returns {FooTable.Query}

		 */

		construct: function(query, space, connectors, ignoreCase){

			/* PRIVATE */

			/**

			 * Holds the previous value of the query and is used internally in the {@link FooTable.Query#val} method.

			 * @type {string}

			 * @private

			 */

			this._original = null;

			/**

			 * Holds the value for the query. Access to this variable is provided through the {@link FooTable.Query#val} method.

			 * @type {string}

			 * @private

			 */

			this._value = null;

			/* PUBLIC */

			/**

			 * A string specifying how the query treats whitespace. Can be either "OR" or "AND".

			 * @type {string}

			 */

			this.space = F.is.string(space) && (space == 'OR' || space == 'AND') ? space : 'AND';

			/**

			 * Whether or not to replace phrase connectors (+.-_) with spaces before executing the query.

			 * @instance

			 * @type {boolean}

			 */

			this.connectors = F.is.boolean(connectors) ? connectors : true;

			/**

			 * Whether or not ignore case when matching.

			 * @instance

			 * @type {boolean}

			 */

			this.ignoreCase = F.is.boolean(ignoreCase) ? ignoreCase : true;

			/**

			 * The left side of the query if one exists. OR takes precedence over AND.

			 * @type {FooTable.Query}

			 * @example <caption>The below shows what is meant by the "left" side of a query</caption>

			 * query = "Dave AND Mary" - "Dave" is the left side of the query.

			 * query = "Dave AND Mary OR John" - "Dave and Mary" is the left side of the query.

			 */

			this.left = null;

			/**

			 * The right side of the query if one exists. OR takes precedence over AND.

			 * @type {FooTable.Query}

			 * @example <caption>The below shows what is meant by the "right" side of a query</caption>

			 * query = "Dave AND Mary" - "Mary" is the right side of the query.

			 * query = "Dave AND Mary OR John" - "John" is the right side of the query.

			 */

			this.right = null;

			/**

			 * The parsed parts of the query. This contains the information used to actually perform a match against a string.

			 * @type {Array}

			 */

			this.parts = [];

			/**

			 * The type of operand to apply to the results of the individual parts of the query.

			 * @type {string}

			 */

			this.operator = null;

			this.val(query);

		},

		/**

		 * Gets or sets the value for the query. During set the value is parsed setting all properties as required.

		 * @param {string} [value] - If supplied the value to set for this query.

		 * @returns {(string|undefined)}

		 */

		val: function(value){

			// get

			if (F.is.emptyString(value)) return this._value;



			// set

			if (F.is.emptyString(this._original)) this._original = value;

			else if (this._original == value) return;



			this._value = value;

			this._parse();

		},

		/**

		 * Tests the supplied string against the query.

		 * @param {string} str - The string to test.

		 * @returns {boolean}

		 */

		match: function(str){

			if (F.is.emptyString(this.operator) || this.operator === 'OR')

				return this._left(str, false) || this._match(str, false) || this._right(str, false);

			if (this.operator === 'AND')

				return this._left(str, true) && this._match(str, true) && this._right(str, true);

		},

		/**

		 * Matches this queries parts array against the supplied string.

		 * @param {string} str - The string to test.

		 * @param {boolean} def - The default value to return based on the operand.

		 * @returns {boolean}

		 * @private

		 */

		_match: function(str, def){

			var self = this, result = false, empty = F.is.emptyString(str);

			if (F.is.emptyArray(self.parts) && self.left instanceof F.Query) return def;

			if (F.is.emptyArray(self.parts)) return result;

			if (self.space === 'OR'){

				// with OR we give the str every part to test and if any match it is a success, we do exit early if a negated match occurs

				F.arr.each(self.parts, function(p){

					if (p.empty && empty){

						result = true;

						if (p.negate){

							result = false;

							return result;

						}

					} else {

						var match = (p.exact ? F.str.containsExact : F.str.contains)(str, p.query, self.ignoreCase);

						if (match && !p.negate) result = true;

						if (match && p.negate) {

							result = false;

							return result;

						}

					}

				});

			} else {

				// otherwise with AND we check until the first failure and then exit

				result = true;

				F.arr.each(self.parts, function(p){

					if (p.empty){

						if ((!empty && !p.negate) || (empty && p.negate)) result = false;

						return result;

					} else {

						var match = (p.exact ? F.str.containsExact : F.str.contains)(str, p.query, self.ignoreCase);

						if ((!match && !p.negate) || (match && p.negate)) result = false;

						return result;

					}

				});

			}

			return result;

		},

		/**

		 * Matches the left side of the query if one exists with the supplied string.

		 * @param {string} str - The string to test.

		 * @param {boolean} def - The default value to return based on the operand.

		 * @returns {boolean}

		 * @private

		 */

		_left: function(str, def){

			return (this.left instanceof F.Query) ? this.left.match(str) : def;

		},

		/**

		 * Matches the right side of the query if one exists with the supplied string.

		 * @param {string} str - The string to test.

		 * @param {boolean} def - The default value to return based on the operand.

		 * @returns {boolean}

		 * @private

		 */

		_right: function(str, def){

			return (this.right instanceof F.Query) ? this.right.match(str) : def;

		},

		/**

		 * Parses the private {@link FooTable.Query#_value} property and populates the object.

		 * @private

		 */

		_parse: function(){

			if (F.is.emptyString(this._value)) return;

			// OR takes precedence so test for it first

			if (/\sOR\s/.test(this._value)){

				// we have an OR so split the value on the first occurrence of OR to get the left and right sides of the statement

				this.operator = 'OR';

				var or = this._value.split(/(?:\sOR\s)(.*)?/);

				this.left = new F.Query(or[0], this.space, this.connectors, this.ignoreCase);

				this.right = new F.Query(or[1], this.space, this.connectors, this.ignoreCase);

			} else if (/\sAND\s/.test(this._value)) {

				// there are no more OR's so start with AND

				this.operator = 'AND';

				var and = this._value.split(/(?:\sAND\s)(.*)?/);

				this.left = new F.Query(and[0], this.space, this.connectors, this.ignoreCase);

				this.right = new F.Query(and[1], this.space, this.connectors, this.ignoreCase);

			} else {

				// we have no more statements to parse so set the parts array by parsing each part of the remaining query

				var self = this;

				this.parts = F.arr.map(this._value.match(/(?:[^\s"]+|"[^"]*")+/g), function(str){

					return self._part(str);

				});

			}

		},

		/**

		 * Parses a single part of a query into an object to use during matching.

		 * @param {string} str - The string representation of the part.

		 * @returns {{query: string, negate: boolean, phrase: boolean, exact: boolean}}

		 * @private

		 */

		_part: function(str){

			var p = {

				query: str,

				negate: false,

				phrase: false,

				exact: false,

				empty: false

			};

			// support for NEGATE operand - (minus sign). Remove this first so we can get onto phrase checking

			if (F.str.startsWith(p.query, '-')){

				p.query = F.str.from(p.query, '-');

				p.negate = true;

			}

			// support for PHRASES (exact matches)

			if (/^"(.*?)"$/.test(p.query)){ // if surrounded in quotes strip them and nothing else

				p.query = p.query.replace(/^"(.*?)"$/, '$1');

				p.phrase = true;

				p.exact = true;

			} else if (this.connectors && /(?:\w)+?([-_\+\.])(?:\w)+?/.test(p.query)) { // otherwise replace supported phrase connectors (-_+.) with spaces

				p.query = p.query.replace(/(?:\w)+?([-_\+\.])(?:\w)+?/g, function(match, p1){

					return match.replace(p1, ' ');

				});

				p.phrase = true;

			}

			p.empty = p.phrase && F.is.emptyString(p.query);

			return p;

		}

	});



})(FooTable);