
/*jshint esversion: 6 */
( function( window ) {
	'use strict';

	var isNumber = function( str ) {
		return String( str ).isNumeric() && ! String( str ).startsWith( '0' );
	};

	var empty = function( mixedVar ) {
		var undef;
		var key;
		var i;
		var len;
		var emptyValues = [ undef, null, false, 0, '', '0' ];
		for ( i = 0, len = emptyValues.length; i < len; i++ ) {
			if ( mixedVar === emptyValues[ i ] ) {
				return true;
			}
		}
		if ( typeof mixedVar === 'object' ) {
			for ( key in mixedVar ) {
				if ( mixedVar.hasOwnProperty( key ) ) {
					return false;
				}
			}
			return true;
		}
		return false;
	};

	var array_key_exists = function( key, search ) {
		if ( ! search || ( search.constructor !== Array && search.constructor !== Object ) ) {
			return false;
		}
		return key in search;
	};

	var getNumberOfParameters = function( func ) {
		const ARROW = true;
		const FUNC_ARGS = ARROW ? /^(function)?\s*[^\(]*\(\s*([^\)]*)\)/m : /^(function)\s*[^\(]*\(\s*([^\)]*)\)/m;
		const FUNC_ARG_SPLIT = /,/;
		const FUNC_ARG = /^\s*(_?)(.+?)\1\s*$/;
		const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;

		return ( ( func || '' )
			.toString()
			.replace( STRIP_COMMENTS, '' )
			.match( FUNC_ARGS ) || [ '', '', '' ] )[ 2 ]
			.split( FUNC_ARG_SPLIT )
			.map( function( arg ) {
				return arg.replace( FUNC_ARG, function( all, underscore, name ) {
					return name.split( '=' )[ 0 ].trim();
				} );
			} )
			.filter( String )
			.length;
	};

	var getNumberOfRequiredParameters = function( func ) {
		return func.length;
	};

	var is_scalar = function( mixedVar ) {
		return ( /boolean|number|string/ ).test( typeof mixedVar );
	};

	var gettype = function( obj ) {
		return window.jQuery.epoAPI.util.getType( obj );
	};

	var floatval = function( s, d ) {
		var n;

		if ( ! ( typeof s === 'string' || typeof s === 'number' ) || isNaN( s ) ) {
			if ( d !== undefined ) {
				return d;
			}
			return 0;
		}
		n = parseFloat( s );
		if ( isNaN( n ) ) {
			if ( d !== undefined ) {
				return d;
			}
			return s;
		}

		return n;
	};

	var strcmp = function( str1, str2 ) {
		return ( ( str1 === str2 ) ? 0 : ( ( str1 > str2 ) ? 1 : -1 ) );
	};

	var bindec = function( binaryString ) {
		binaryString = ( binaryString + '' ).replace( /[^01]/gi, '' );
		return parseInt( binaryString, 2 );
	};

	var decbin = function( number ) {
		if ( number < 0 ) {
			number = 0xFFFFFFFF + number + 1;
		}
		return parseInt( number, 10 ).toString( 2 );
	};

	var dechex = function( number ) {
		if ( number < 0 ) {
			number = 0xFFFFFFFF + number + 1;
		}
		return parseInt( number, 10 ).toString( 16 );
	};

	var decoct = function( number ) {
		if ( number < 0 ) {
			number = 0xFFFFFFFF + number + 1;
		}
		return parseInt( number, 10 ).toString( 8 );
	};

	var deg2rad = function( angle ) {
		return angle * 0.017453292519943295; // (angle / 180) * Math.PI;
	};

	var roundWithPrecision = function( num, dec = 0 ) {
		var num_sign = num >= 0 ? 1 : -1;
		return dec === 0 ? Math.round( num ) : parseFloat( ( Math.round( ( num * Math.pow( 10, dec ) ) + ( num_sign * 0.0001 ) ) / Math.pow( 10, dec ) ).toFixed( dec ) );
	};

	var hexdec = function( hexString ) {
		hexString = ( hexString + '' ).replace( /[^a-f0-9]/gi, '' );
		return parseInt( hexString, 16 );
	};

	var octdec = function( octString ) {
		octString = ( octString + '' ).replace( /[^0-7]/gi, '' );
		return parseInt( octString, 8 );
	};

	var hypot = function( x, y ) {
		var t;
		x = Math.abs( x );
		y = Math.abs( y );
		t = Math.min( x, y );
		x = Math.max( x, y );
		t = t / x;
		return x * Math.sqrt( 1 + ( t * t ) ) || null;
	};

	var THEMECOMPLETE_EPO_MATH = {
		variables: {},
		on_var_not_found: undefined,
		on_var_validation: undefined,
		operators: {},
		functions: {},
		cache: {},
		construct: function() {
			this.add_defaults();
			this.set_division_by_zero_to_zero();
			return this;
		},
		add_operator: function( $operator ) {
			this.operators[ $operator.operator ] = $operator;
			return this;
		},
		evaluate: function( $expression, $cache = true ) {
			return this.execute( $expression, $cache );
		},
		parse: function( $expression ) {
			var $tokens;
			var result;
			var $calculator;
			var count = 0;
			var char;

			for ( char of $expression ) {
				if ( char === '(' ) {
					count++;
				} else if ( char === ')' ) {
					count--;
				}

				// If count becomes negative at any point, return false
				if ( count < 0 ) {
					return false;
				}
			}

			// If count is not zero at the end, parentheses are not balanced
			if ( count !== 0 ) {
				return false;
			}

			$tokens = new THEMECOMPLETE_EPO_MATH_Tokenizer( $expression, this.operators );
			$tokens = $tokens.tokenize().build_reverse_polish_notation();
			$calculator = new THEMECOMPLETE_EPO_MATH_Calculator( this.functions, this.operators );

			result = $calculator.calculate( $tokens, this.variables, this.on_var_not_found, this );
			if ( 'number' !== gettype( result ) || isNaN( result ) ) {
				return false;
			}
			return result;
		},
		execute: function( $expression, $cache = true, $canreturnstring = false ) {
			var $cache_key = $expression;
			var $calculator;
			var $tokens;
			var result;

			if ( ! array_key_exists( $cache_key, this.cache ) ) {
				$tokens = new THEMECOMPLETE_EPO_MATH_Tokenizer( $expression, this.operators );
				$tokens = $tokens.tokenize().build_reverse_polish_notation();
				if ( $cache ) {
					this.cache[ $cache_key ] = $tokens;
				}
			} else {
				$tokens = this.cache[ $cache_key ];
			}

			$calculator = new THEMECOMPLETE_EPO_MATH_Calculator( this.functions, this.operators );

			result = $calculator.calculate( $tokens, this.variables, this.on_var_not_found, this );

			if ( false === $canreturnstring && ( 'number' !== gettype( result ) || isNaN( result ) ) ) {
				result = 0;
			}

			return result;
		},
		add_function: function( $name, $function ) {
			this.functions[ $name ] = new THEMECOMPLETE_EPO_MATH_CustomFunction( $name, $function );
			return this;
		},
		get_vars: function() {
			return this.variables;
		},
		get_var: function( $variable ) {
			if ( ! array_key_exists( $variable, this.variables ) ) {
				if ( this.on_var_not_found && 'function' === typeof this.on_var_not_found ) {
					this.on_var_not_found( $variable );
				}
				return THEMECOMPLETE_EPO_MATH_Error.trigger( 'Variable (' + $variable + ') not set', 'UnknownVariableError', 0 );
			}
			return this.variables[ $variable ];
		},
		set_var: function( $variable, $value ) {
			if ( this.on_var_validation && 'function' === typeof this.on_var_validation ) {
				$value = this.on_var_validation( $variable, $value );
			}
			this.variables[ $variable ] = $value;
			return this;
		},
		var_exists: function( $variable ) {
			return array_key_exists( $variable, this.variables );
		},
		set_vars: function( $variables, $clear = true ) {
			var $this = this;
			if ( $clear ) {
				this.remove_vars();
			}
			$variables.forFach( function( $value, $name ) {
				$this.set_var( $name, $value );
			} );
			return this;
		},
		set_var_not_found_handler: function( $handler ) {
			this.on_var_not_found = $handler;
			return this;
		},
		set_var_validation_handler: function( $handler ) {
			this.on_var_validation = $handler;
			return this;
		},
		remove_var: function( $variable ) {
			delete this.variables[ $variable ];
			return this;
		},
		remove_vars: function() {
			this.variables = {};
			this.on_var_not_found = null;
			return this;
		},
		get_operators: function() {
			return this.operators;
		},
		get_functions: function() {
			return this.functions;
		},
		remove_operator: function( $operator ) {
			delete this.operators[ $operator ];
		},
		set_division_by_zero_to_zero: function() {
			this.add_operator(
				new THEMECOMPLETE_EPO_MATH_Operator(
					'/',
					false,
					180,
					function( $a, $b ) {
						$a = Number( $a );
						$b = Number( $b );
						return 0 == $b ? 0 : $a / $b; // eslint-disable-line eqeqeq
					}
				)
			);
			return this;
		},
		get_cache: function() {
			return this.cache;
		},
		clear_cache: function() {
			this.cache = [];
		},
		add_defaults: function() {
			var $this = this;
			var default_operators = this.default_operators();
			var default_functions = this.default_functions();
			Object.keys( default_operators ).forEach( function( $name ) {
				var $operator = default_operators[ $name ];
				$this.add_operator( new THEMECOMPLETE_EPO_MATH_Operator( $name, $operator[ 2 ], $operator[ 1 ], $operator[ 0 ] ) );
			} );
			Object.keys( default_functions ).forEach( function( $name ) {
				var $callable = default_functions[ $name ];
				$this.add_function( $name, $callable );
			} );
			this.on_var_validation = this.default_var_validation;
			this.variables = this.default_vars();
			return this;
		},
		default_operators: function() {
			return {
				'+': [
					function( $a, $b ) {
						$a = floatval( $a, 0 );
						$b = floatval( $b, 0 );
						return $a + $b;
					},
					170,
					false
				],
				'-': [
					function( $a, $b ) {
						$a = floatval( $a, 0 );
						$b = floatval( $b, 0 );
						return $a - $b;
					},
					170,
					false
				],
				// unary positive token.
				uPos: [
					function( $a ) {
						$a = floatval( $a, 0 );
						return $a;
					},
					200,
					false
				],
				// unary minus token.
				uNeg: [
					function( $a ) {
						$a = floatval( $a, 0 );
						return 0 - $a;
					},
					200,
					false
				],
				'*': [
					function( $a, $b ) {
						$a = floatval( $a, 0 );
						$b = floatval( $b, 0 );
						return $a * $b;
					},
					180,
					false
				],
				'/': [
					function( $a, $b ) {
						$a = floatval( $a, 0 );
						$b = floatval( $b, 0 );
						if ( empty( $b ) ) {
							return THEMECOMPLETE_EPO_MATH_Error.trigger( 'Division By Zero', 'DivisionByZeroError', 0 );
						}
						return $a / $b;
					},
					180,
					false
				],
				'^': [
					function( $a, $b ) {
						$a = floatval( $a, 0 );
						$b = floatval( $b, 0 );
						return Math.pow( $a, $b );
					},
					220,
					true
				],
				'%': [
					function( $a, $b ) {
						$a = floatval( $a, 0 );
						$b = floatval( $b, 0 );
						return $a % $b;
					},
					180,
					false
				],
				'&&': [
					function( $a, $b ) {
						$a = String( $a );
						$b = String( $b );
						if ( $a.isNumeric() ) {
							$a = floatval( $a, 0 );
						}
						if ( $b.isNumeric() ) {
							$b = floatval( $b, 0 );
						}
						return $a && $b ? 1 : 0;
					},
					100,
					false
				],
				'||': [
					function( $a, $b ) {
						$a = String( $a );
						$b = String( $b );
						if ( $a.isNumeric() ) {
							$a = floatval( $a, 0 );
						}
						if ( $b.isNumeric() ) {
							$b = floatval( $b, 0 );
						}
						return $a || $b ? 1 : 0;
					},
					90,
					false
				],
				'==': [
					function( $a, $b ) {
						$a = String( $a );
						$b = String( $b );
						if ( $a.isNumeric() && $b.isNumeric() ) {
							return floatval( $a ) === floatval( $b ) ? 1 : 0;
						}
						return 0 === strcmp( $a, $b ) ? 1 : 0;
					},
					140,
					false
				],
				'!=': [
					function( $a, $b ) {
						$a = String( $a );
						$b = String( $b );
						if ( $a.isNumeric() && $b.isNumeric() ) {
							return floatval( $a ) !== floatval( $b ) ? 1 : 0;
						}
						return 0 !== strcmp( $a, $b ) ? 1 : 0;
					},
					140,
					false
				],
				'>=': [
					function( $a, $b ) {
						$a = String( $a );
						$b = String( $b );
						if ( $a.isNumeric() ) {
							$a = floatval( $a, 0 );
						}
						if ( $b.isNumeric() ) {
							$b = floatval( $b, 0 );
						}
						return $a >= $b ? 1 : 0;
					},
					150,
					false
				],
				'>': [
					function( $a, $b ) {
						$a = String( $a );
						$b = String( $b );
						if ( $a.isNumeric() ) {
							$a = floatval( $a, 0 );
						}
						if ( $b.isNumeric() ) {
							$b = floatval( $b, 0 );
						}
						return $a > $b ? 1 : 0;
					},
					150,
					false
				],
				'<=': [
					function( $a, $b ) {
						$a = String( $a );
						$b = String( $b );
						if ( $a.isNumeric() ) {
							$a = floatval( $a, 0 );
						}
						if ( $b.isNumeric() ) {
							$b = floatval( $b, 0 );
						}
						return $a <= $b ? 1 : 0;
					},
					150,
					false
				],
				'<': [
					function( $a, $b ) {
						$a = String( $a );
						$b = String( $b );
						if ( $a.isNumeric() ) {
							$a = floatval( $a, 0 );
						}
						if ( $b.isNumeric() ) {
							$b = floatval( $b, 0 );
						}
						return $a < $b ? 1 : 0;
					},
					150,
					false
				]
			};
		},
		default_functions: function() {
			var $this = this;
			return {
				abs: Math.abs,
				acos: Math.acos,
				acosh: Math.acosh,
				arcsin: Math.asin,
				arcctg: function( $arg ) {
					return ( Math.PI / 2 ) - Math.atan( $arg );
				},
				arccot: function( $arg ) {
					return ( Math.PI / 2 ) - Math.atan( $arg );
				},
				arccotan: function( $arg ) {
					return ( Math.PI / 2 ) - Math.atan( $arg );
				},
				arcsec: function( $arg ) {
					return Math.acos( 1 / $arg );
				},
				arccosec: function( $arg ) {
					return Math.asin( 1 / $arg );
				},
				arccsc: function( $arg ) {
					return Math.asin( 1 / $arg );
				},
				arccos: Math.acos,
				arctan: Math.atan,
				arctg: Math.atan,
				array: function( ...$args ) {
					return $args;
				},
				asin: Math.asin,
				atan: Math.atan,
				atan2: Math.atan2,
				atanh: Math.atanh,
				atn: Math.atan,

				avg: function( $arg1, ...$args ) {
					var sum;
					if ( Array.isArray( $arg1 ) ) {
						if ( 0 === $arg1.length ) {
							return THEMECOMPLETE_EPO_MATH_Error.trigger( 'Array must contain at least one element!', 'InvalidArgumentError', 0 );
						}
					}
					$args = [].concat.apply( [], [ $arg1, ...$args ] );

					sum = $args.reduce( function( previousValue, currentValue ) {
						return floatval( currentValue, 0 ) + floatval( previousValue, 0 );
					} );
					return sum / $args.length;
				},
				average: function( $arg1, ...$args ) {
					var sum;
					if ( Array.isArray( $arg1 ) ) {
						if ( 0 === $arg1.length ) {
							return THEMECOMPLETE_EPO_MATH_Error.trigger( 'Array must contain at least one element!', 'InvalidArgumentError', 0 );
						}
					}
					$args = [].concat.apply( [], [ $arg1, ...$args ] );

					sum = $args.reduce( function( previousValue, currentValue ) {
						return floatval( currentValue, 0 ) + floatval( previousValue, 0 );
					} );
					return sum / $args.length;
				},
				bindec: bindec,
				ceil: Math.ceil,
				cos: Math.cos,
				cosec: function( $arg ) {
					return Math.sin( 1 / $arg );
				},
				csc: function( $arg ) {
					return Math.sin( 1 / $arg );
				},
				cosh: Math.cosh,
				ctg: function( $arg ) {
					return Math.cos( $arg ) / Math.sin( $arg );
				},
				cot: function( $arg ) {
					return Math.cos( $arg ) / Math.sin( $arg );
				},
				cotan: function( $arg ) {
					return Math.cos( $arg ) / Math.sin( $arg );
				},
				cotg: function( $arg ) {
					return Math.cos( $arg ) / Math.sin( $arg );
				},
				ctn: function( $arg ) {
					return Math.cos( $arg ) / Math.sin( $arg );
				},
				decbin: decbin,
				dechex: dechex,
				decoct: decoct,
				deg2rad: deg2rad,
				exp: Math.exp,
				expm1: Math.expm1,
				floor: Math.floor,
				int: Math.floor,
				fmod: function( $arg1, $arg2 ) {
					return $arg1 % $arg2;
				},
				hexdec: hexdec,
				hypot: hypot,
				if: function( ...$args ) {
					var $exres;
					var $expr;
					var $trueval;
					var $falseval;
					var $canreturnstring;
					$expr = $args[ 0 ];
					$trueval = $args[ 1 ];
					$falseval = $args[ 2 ];
					if ( 'string' === gettype( $trueval ) ) {
						$trueval = "'" + $trueval.replace( /'/g, "\\'" ) + "'";
					}
					if ( 'string' === gettype( $falseval ) ) {
						$falseval = "'" + $falseval.replace( /'/g, "\\'" ) + "'";
					}
					$canreturnstring = $args[ 3 ] !== undefined ? $args[ 3 ] : false;
					if ( 0 === $expr || 1 === $expr || true === $expr || false === $expr ) {
						$exres = $expr;
					} else {
						$exres = $this.execute( $expr, true, $canreturnstring );
					}
					if ( $exres ) {
						return $this.execute( $trueval, true, $canreturnstring );
					}
					return $this.execute( $falseval, true, $canreturnstring );
				},
				intdiv: function( $arg1, $arg2 ) {
					return Math.trunc( Math.trunc( $arg1 ) / Math.trunc( $arg2 ) );
				},
				ln: Math.log,
				lg: Math.log10,
				log: Math.log,
				log1p: Math.log1p,
				max: function( $arg1, ...$args ) {
					var $array;
					if ( Array.isArray( $arg1 ) && 0 === $arg1.length ) {
						return THEMECOMPLETE_EPO_MATH_Error.trigger( 'Array must contain at least one element!', 'InvalidArgumentError', 0 );
					}
					$array = Array.isArray( $arg1 ) ? $arg1 : [ $arg1, ...$args ];
					$array = $array.map( floatval );

					return Math.max( ...$array );
				},
				min: function( $arg1, ...$args ) {
					var $array;
					if ( Array.isArray( $arg1 ) && 0 === $arg1.length ) {
						return THEMECOMPLETE_EPO_MATH_Error.trigger( 'Array must contain at least one element!', 'InvalidArgumentError', 0 );
					}
					$array = Array.isArray( $arg1 ) ? $arg1 : [ $arg1, ...$args ];
					$array = $array.map( floatval );

					return Math.min( ...$array );
				},
				octdec: octdec,
				pi: function() {
					return Math.PI;
				},
				pow: Math.pow,
				rad2deg: function( angle ) {
					return angle * 57.29577951308232; // angle / Math.PI * 180
				},
				round: roundWithPrecision,
				sin: Math.sin,
				sinh: Math.sinh,
				sec: function( $arg ) {
					return 1 / Math.cos( $arg );
				},
				sqrt: Math.sqrt,
				tan: Math.tan,
				tanh: Math.tanh,
				tn: Math.tan,
				tg: Math.tan,
				lookuptable: function( field, lookupTableId ) {
					var lookupTables;
					var x;
					var y;
					var table;
					var xColumn;
					var price = 0;
					var tableNum = 0;
					var TMEPOJS = window.TMEPOJS;

					if ( TMEPOJS ) {
						lookupTables = window.jQuery.epoAPI.util.parseJSON( TMEPOJS.lookupTables );
						if ( lookupTables ) {
							if ( Array.isArray( lookupTableId ) ) {
								tableNum = lookupTableId[ 1 ];
								lookupTableId = lookupTableId[ 0 ];
							}
							if ( empty( lookupTableId ) ) {
								return 0;
							}
							if ( empty( tableNum ) ) {
								tableNum = 0;
							}
							if ( Array.isArray( field ) ) {
								x = String( field[ 0 ] );
								y = String( field[ 1 ] );
							} else {
								x = String( field );
								y = '';
							}

							table = lookupTables[ lookupTableId ];
							if ( table ) {
								table = table[ tableNum ];
								if ( table ) {
									table = table.data;
									xColumn = table[ x ];
									if ( xColumn === undefined && x && x !== undefined ) {
										if ( x.isNumeric() && floatval( x ) === 0 ) {
											xColumn = table[ Object.keys( table )[ 0 ] ];
										} else if ( x ) {
											x = $this.find_lookup_table_index( x, table );
											xColumn = table[ x ];
										}
									}
									if ( xColumn !== undefined ) {
										if ( y && y !== undefined ) {
											y = $this.find_lookup_table_index( y, xColumn );
										} else {
											// fetch the first row since this means we want
											// a single row result
											y = Object.keys( xColumn )[ 0 ];
										}
										if ( y === 'max' ) {
											price = floatval( xColumn[ Object.keys( xColumn )[ Object.keys( xColumn ).length - 1 ] ] );
										} else {
											price = floatval( xColumn[ y ] );
										}
									}
								}
							}
						}
					}
					return price;
				},
				concat: function( $arg1, ...$args ) {
					var $array;
					if ( Array.isArray( $arg1 ) && 0 === $arg1.length ) {
						return THEMECOMPLETE_EPO_MATH_Error.trigger( 'Array must contain at least one element!', 'InvalidArgumentError', 0 );
					}
					$array = Array.isArray( $arg1 ) ? $arg1 : [ $arg1, ...$args ];

					return $array.join( '' );
				}
			};
		},
		find_lookup_table_index: function( value, table ) {
			var r;
			var keys = Object.keys( table );
			var hasNumericValuesOrMaxLast = function( arr ) {
				var lastElement = arr[ arr.length - 1 ];
				var areAllNumericValues = arr.every( function( val ) {
					return String( val ).isNumeric();
				} );
				return areAllNumericValues || lastElement === 'max';
			};

			if ( hasNumericValuesOrMaxLast( keys ) ) {
				value = isNumber( value ) ? floatval( value ) : value;
				r = keys.map( function( n ) {
					return isNumber( n ) ? floatval( n ) : n;
				} ).reduce( function( a, b ) {
					if ( b === 'max' && value > a ) {
						return b;
					}
					if ( a === 'max' && value > b ) {
						return a;
					}
					if ( a < b ) {
						if ( value > a && value <= b ) {
							return b;
						}
					} else {
						if ( ( value > b && value <= a ) || ( value > a || b === 'max' ) ) {
							return a;
						}
						return b;
					}
					if ( value > b ) {
						return b;
					}
					return a;
				} );
				keys = keys.map( function( n ) {
					return isNumber( n ) ? floatval( n ) : n;
				} );

				if ( value > Math.max( ...keys ) || value < Math.min( ...keys ) ) {
					return false;
				}
			} else {
				r = value;
			}

			return r;
		},
		default_vars: function() {
			return {
				pi: 3.141592653589793,
				e: 2.718281828459045
			};
		},
		default_var_validation: function( $variable, $value ) {
			if ( ! is_scalar( $value ) && ! Array.isArray( $value ) && undefined !== $value ) {
				return THEMECOMPLETE_EPO_MATH_Error.trigger( 'Variable (' + $variable + ') type (' + gettype( $value ) + ') is not scalar or array!', 0 );
			}
			return $value;
		}
	};

	var THEMECOMPLETE_EPO_MATH_Tokenizer = function( $input, $operators ) {
		var node = {
			tokens: [],
			input: '',
			operators: {},
			number_buffer: '',
			string_buffer: '',
			allow_negative: true,
			in_single_quoted_string: false,
			in_double_quoted_string: false,
			is_number: function( ch ) {
				return ch >= '0' && ch <= '9';
			},
			is_alpha: function( ch ) {
				return ( ch >= 'a' && ch <= 'z' ) || ( ch >= 'A' && ch <= 'Z' ) || '_' === ch;
			},
			is_dot: function( ch ) {
				return '.' === ch;
			},
			is_lp: function( ch ) {
				return '(' === ch;
			},
			is_rp: function( ch ) {
				return ')' === ch;
			},
			is_comma: function( ch ) {
				return ',' === ch;
			},
			empty_number_buffer_as_literal: function() {
				if ( this.number_buffer.length ) {
					this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.LITERAL, this.number_buffer ) );
					this.number_buffer = '';
				}
			},
			empty_str_buffer_as_variable: function() {
				if ( '' !== this.string_buffer ) {
					this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.VARIABLE, this.string_buffer ) );
					this.string_buffer = '';
				}
			},
			tokenize: function() {
				var is_last_char_escape = false;
				var $this = this;
				var $token_test = [];

				this.tokens = [];

				this.input.toString().split( '' ).forEach( function( ch ) {
					switch ( true ) {
						case $this.in_single_quoted_string:
							if ( '\\' === ch ) {
								if ( is_last_char_escape ) {
									$this.string_buffer += '\\';
									is_last_char_escape = false;
								} else {
									is_last_char_escape = true;
								}
								break;
							} else if ( "'" === ch ) {
								if ( is_last_char_escape ) {
									$this.string_buffer += "'";
									is_last_char_escape = false;
								} else {
									$this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.STRING, $this.string_buffer ) );
									$this.in_single_quoted_string = false;
									$this.string_buffer = '';
								}
								break;
							}
							if ( is_last_char_escape ) {
								$this.string_buffer += '\\';
								is_last_char_escape = false;
							}
							$this.string_buffer += ch;
							break;
						case $this.in_double_quoted_string:
							if ( '\\' === ch ) {
								if ( is_last_char_escape ) {
									$this.string_buffer += '\\';
									is_last_char_escape = false;
								} else {
									is_last_char_escape = true;
								}
								break;
							} else if ( '"' === ch ) {
								if ( is_last_char_escape ) {
									$this.string_buffer += '"';
									is_last_char_escape = false;
								} else {
									$this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.STRING, $this.string_buffer ) );
									$this.in_double_quoted_string = false;
									$this.string_buffer = '';
								}
								break;
							}
							if ( is_last_char_escape ) {
								$this.string_buffer += '\\';
								is_last_char_escape = false;
							}
							$this.string_buffer += ch;
							break;
						case '[' === ch:
							$this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.FUNCTION, 'array' ) );
							$this.allow_negative = true;
							$this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.LEFTPARENTHESIS, '' ) );
							break;
						case ' ' === ch || '\n' === ch || '\r' === ch || '\t' === ch:
							/**
							 * In case those tokens must not be ingored use the following
							 *
							 * $this.empty_number_buffer_as_literal();
							 * $this.empty_str_buffer_as_variable();
							 * $this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.SPACE, '' ) );
							 */
							break;
						case $this.is_number( ch ):
							if ( '' !== $this.string_buffer ) {
								$this.string_buffer += ch;
								break;
							}
							$this.number_buffer += ch;
							$this.allow_negative = false;
							break;
						case 'e' === ch.toLowerCase():
							if ( $this.number_buffer.length && -1 !== $this.number_buffer.indexOf( '.' ) ) {
								$this.number_buffer += 'e';
								$this.allow_negative = false;
								break;
							}
							/* falls through */
						case $this.is_alpha( ch ):
							if ( $this.number_buffer.length ) {
								$this.empty_number_buffer_as_literal();
								$this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.OPERATOR, '*' ) );
							}
							$this.allow_negative = false;
							$this.string_buffer += ch;
							break;
						case '"' === ch:
							$this.in_double_quoted_string = true;
							break;
						case "'" === ch:
							$this.in_single_quoted_string = true;
							break;
						case $this.is_dot( ch ):
							$this.number_buffer += ch;
							$this.allow_negative = false;
							break;
						case $this.is_lp( ch ):
							if ( '' !== $this.string_buffer ) {
								$this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.FUNCTION, $this.string_buffer ) );
								$this.string_buffer = '';
							} else if ( $this.number_buffer.length ) {
								$this.empty_number_buffer_as_literal();
								$this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.OPERATOR, '*' ) );
							}
							$this.allow_negative = true;
							$this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.LEFTPARENTHESIS, ch ) );
							break;
						case $this.is_rp( ch ) || ']' === ch :
							$this.empty_number_buffer_as_literal();
							$this.empty_str_buffer_as_variable();
							$this.allow_negative = false;
							$this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.RIGHTPARENTHESIS, ch ) );
							break;
						case $this.is_comma( ch ):
							$this.empty_number_buffer_as_literal();
							$this.empty_str_buffer_as_variable();
							$this.allow_negative = true;
							$this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.PARAMSEPARATOR, ch ) );
							break;
						default:
							// special case for unary operations.
							if ( '-' === ch || '+' === ch ) {
								if ( $this.allow_negative ) {
									$this.allow_negative = false;
									$this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.OPERATOR, '-' === ch ? 'uNeg' : 'uPos' ) );
									break;
								}
								// could be in exponent, in which case negative should be added to the number_buffer.
								if ( $this.number_buffer && 'e' === $this.number_buffer[ $this.number_buffer.length - 1 ] ) {
									$this.number_buffer += ch;
									break;
								}
							}
							$this.empty_number_buffer_as_literal();
							$this.empty_str_buffer_as_variable();

							if ( '$' !== ch ) {
								if ( $this.tokens.length > 0 ) {
									if ( THEMECOMPLETE_EPO_MATH_Constants.OPERATOR === $this.tokens[ $this.tokens.length - 1 ].type ) {
										$this.tokens[ $this.tokens.length - 1 ].value += ch;
									} else {
										$this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.OPERATOR, ch ) );
									}
								} else {
									$this.tokens.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.OPERATOR, ch ) );
								}
							}
							$this.allow_negative = true;
					}
				} );
				this.empty_number_buffer_as_literal();
				this.empty_str_buffer_as_variable();

				this.tokens.forEach( function( $token, $key ) {
					$token_test[ $key ] = $token.type;
				} );
				$token_test.forEach( function( $type, $key ) {
					if ( $key > 0 && 'space' === $type && 'variable' === $token_test[ $key + 1 ] && 'variable' === $token_test[ $key - 1 ] ) {
						$this.tokens[ $key ] = new THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.LITERAL, 0 );
						$this.tokens.splice( $key + 1, 1 );
						$this.tokens.splice( $key - 1, 1 );
					}
				} );
				return this;
			},
			build_reverse_polish_notation: function() {
				var $tokens = [];
				var $stack = [];
				var $param_counter = [];
				var $ctoken;
				var $f;
				var $op1;
				var $op2;
				var $this = this;

				try {
					this.tokens.forEach( function( $token ) {
						switch ( $token.type ) {
							case THEMECOMPLETE_EPO_MATH_Constants.LITERAL:
							case THEMECOMPLETE_EPO_MATH_Constants.VARIABLE:
							case THEMECOMPLETE_EPO_MATH_Constants.STRING:
								$tokens.push( $token );

								if ( $param_counter.length > 0 && 0 === $param_counter[ $param_counter.length - 1 ] ) {
									$param_counter.push( $param_counter.pop() + 1 );
								}

								break;

							case THEMECOMPLETE_EPO_MATH_Constants.FUNCTION:
								if ( $param_counter.length > 0 && 0 === $param_counter[ $param_counter.length - 1 ] ) {
									$param_counter.push( $param_counter.pop() + 1 );
								}
								$stack.push( $token );
								$param_counter.push( 0 );

								break;

							case THEMECOMPLETE_EPO_MATH_Constants.LEFTPARENTHESIS:
								$stack.push( $token );

								break;

							case THEMECOMPLETE_EPO_MATH_Constants.PARAMSEPARATOR:
								while ( THEMECOMPLETE_EPO_MATH_Constants.LEFTPARENTHESIS !== $stack[ $stack.length - 1 ].type ) {
									if ( 0 === $stack.length ) {
										return THEMECOMPLETE_EPO_MATH_Error.trigger( 'Incorrect Brackets', 'IncorrectBracketsError', $tokens );
									}
									$tokens.push( $stack.pop() );
								}
								$param_counter.push( $param_counter.pop() + 1 );

								break;

							case THEMECOMPLETE_EPO_MATH_Constants.OPERATOR:
								if ( ! array_key_exists( $token.value, $this.operators ) ) {
									return THEMECOMPLETE_EPO_MATH_Error.trigger( $token.value, 'UnknownOperatorError', $tokens );
								}
								$op1 = $this.operators[ $token.value ];

								while ( $stack.length > 0 && THEMECOMPLETE_EPO_MATH_Constants.OPERATOR === $stack[ $stack.length - 1 ].type ) {
									if ( ! array_key_exists( $stack[ $stack.length - 1 ].value, $this.operators ) ) {
										return THEMECOMPLETE_EPO_MATH_Error.trigger( $stack[ $stack.length - 1 ].value, 'UnknownOperatorError', $tokens );
									}
									$op2 = $this.operators[ $stack[ $stack.length - 1 ].value ];

									if ( $op2.priority >= $op1.priority ) {
										$tokens.push( $stack.pop() );

										continue;
									}

									break;
								}
								$stack.push( $token );

								break;

							case THEMECOMPLETE_EPO_MATH_Constants.RIGHTPARENTHESIS:
								while ( true ) {
									try {
										$ctoken = $stack.pop();

										if ( THEMECOMPLETE_EPO_MATH_Constants.LEFTPARENTHESIS === $ctoken.type ) {
											break;
										}
										$tokens.push( $ctoken );
									} catch ( $e ) {
										return THEMECOMPLETE_EPO_MATH_Error.trigger( 'Incorrect Brackets', 'IncorrectBracketsError', $tokens );
									}
								}

								if ( $stack.length > 0 && THEMECOMPLETE_EPO_MATH_Constants.FUNCTION === $stack[ $stack.length - 1 ].type ) {
									$f = $stack.pop();
									$f.param_count = $param_counter.pop();
									$tokens.push( $f );
								}

								break;

							case THEMECOMPLETE_EPO_MATH_Constants.SPACE:
								// do nothing.
						}
					} );
				} catch ( $e ) {
					$tokens = [];
					return THEMECOMPLETE_EPO_MATH_Error.trigger( $e, 'Error', $tokens );
				}

				while ( 0 !== $stack.length ) {
					if ( THEMECOMPLETE_EPO_MATH_Constants.LEFTPARENTHESIS === $stack[ $stack.length - 1 ].type || THEMECOMPLETE_EPO_MATH_Constants.RIGHTPARENTHESIS === $stack[ $stack.length - 1 ].type ) {
						return THEMECOMPLETE_EPO_MATH_Error.trigger( 'Incorrect Brackets', 'IncorrectBracketsError', $tokens );
					}

					if ( THEMECOMPLETE_EPO_MATH_Constants.SPACE === $stack[ $stack.length - 1 ].type ) {
						$stack.pop();
						continue;
					}
					$tokens.push( $stack.pop() );
				}

				return $tokens;
			}
		};
		node.input = $input;
		node.operators = $operators;
		return node;
	};

	var THEMECOMPLETE_EPO_MATH_Constants = {
		LITERAL: 'literal',
		VARIABLE: 'variable',
		OPERATOR: 'operator',
		LEFTPARENTHESIS: 'LP',
		RIGHTPARENTHESIS: 'RP',
		FUNCTION: 'function',
		PARAMSEPARATOR: 'separator',
		STRING: 'string',
		SPACE: 'space'

	};

	var THEMECOMPLETE_EPO_MATH_Token = function( $type, $value, $name ) {
		var node = {
			type: THEMECOMPLETE_EPO_MATH_Constants.LITERAL,
			value: undefined,
			name: undefined,
			param_count: null
		};
		node.type = $type;
		node.value = $value;
		node.name = $name;
		return node;
	};

	var THEMECOMPLETE_EPO_MATH_Operator = function( $operator, $is_right_assoc, $priority, $function ) {
		var node = {
			operator: '',
			is_right_assoc: false,
			priority: 0,
			function: null,
			places: 0
		};
		node.operator = $operator;
		node.is_right_assoc = $is_right_assoc;
		node.priority = $priority;
		node.function = $function;
		node.places = getNumberOfParameters( $function );

		node.execute = function( $stack ) {
			var $args = [];
			var $i;
			var $result;

			if ( $stack.length < this.places ) {
				// Empty the $stack
				$stack.splice( 0, $stack.length );
				return THEMECOMPLETE_EPO_MATH_Error.trigger( 'Incorrect Expression', 'IncorrectExpressionError', new THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.LITERAL, 0 ) );
			}

			for ( $i = 0; $i < this.places; $i++ ) {
				$args.unshift( $stack.pop().value );
			}

			$result = this.function.apply( null, $args );
			if ( 'number' !== gettype( $result ) && 'string' !== gettype( $result ) ) {
				$result = 0;
			}

			return new THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.LITERAL, $result );
		};

		return node;
	};

	var THEMECOMPLETE_EPO_MATH_CustomFunction = function( $name, $function ) {
		var node = {
			name: '',
			function: undefined,
			required_param_count: undefined
		};

		node.name = $name;
		node.function = $function;
		node.required_param_count = getNumberOfRequiredParameters( $function );

		node.execute = function( $stack, $param_count_in_stack ) {
			var $args = [];
			var $i;
			var $argument;
			var $result;
			if ( $param_count_in_stack < this.required_param_count ) {
				$param_count_in_stack = this.required_param_count;
			}
			// We skip this section.
			if ( $param_count_in_stack < this.required_param_count ) {
				// Empty the $stack
				$stack.splice( 0, $stack.length );
				return THEMECOMPLETE_EPO_MATH_Error.trigger( this.name, 'IncorrectNumberOfFunctionParametersError', new THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.LITERAL, 0 ) );
			}

			if ( $param_count_in_stack > 0 ) {
				for ( $i = 0; $i < $param_count_in_stack; $i++ ) {
					$argument = $stack.length ? $stack.pop().value : 0;
					if ( null === $argument ) {
						$argument = '0';
						$args.push( $argument );
					} else {
						$args.unshift( $argument );
					}
				}
			}

			$result = this.function.apply( null, $args );

			if ( $result === Infinity || $result === -Infinity ) {
				$result = 0;
			}

			return THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.LITERAL, $result );
		};

		return node;
	};

	var THEMECOMPLETE_EPO_MATH_Calculator = function( $functions, $operators ) {
		var node = {
			functions: {},
			operators: {}
		};
		node.functions = $functions;
		node.operators = $operators;

		node.calculate = function( $tokens, $variables, $on_var_not_found = null, $math_object = false ) {
			var $this = this;
			/** Array of THEMECOMPLETE_EPO_MATH_Token */
			var $stack = [];
			var $result;

			if ( empty( $tokens ) ) {
				return 0;
			}
			try {
				$tokens.forEach( function( $token ) {
					var $variable;
					var $value;
					if ( THEMECOMPLETE_EPO_MATH_Constants.LITERAL === $token.type || THEMECOMPLETE_EPO_MATH_Constants.STRING === $token.type ) {
						$stack.push( $token );
					} else if ( THEMECOMPLETE_EPO_MATH_Constants.VARIABLE === $token.type ) {
						$variable = $token.value;

						$value = null;

						if ( array_key_exists( $variable, $variables ) ) {
							$value = $variables[ $variable ];
						} else if ( $on_var_not_found && 'function' === typeof $on_var_not_found ) {
							$value = $on_var_not_found( $variable );
						} else {
							$value = $variable;
							$math_object.variables[ $variable ] = $value;
							$variables[ $variable ] = $value;
						}
						$stack.push( THEMECOMPLETE_EPO_MATH_Token( THEMECOMPLETE_EPO_MATH_Constants.LITERAL, $value, $variable ) );
					} else if ( THEMECOMPLETE_EPO_MATH_Constants.FUNCTION === $token.type ) {
						if ( ! array_key_exists( $token.value, $this.functions ) ) {
							$math_object.add_function(
								$token.value,
								function() {
									return 0;
								}
							);
							$this.functions = $math_object.functions;
							if ( ! array_key_exists( $token.value, $this.functions ) ) {
								return THEMECOMPLETE_EPO_MATH_Error.trigger( $token.value, 'UnknownFunctionError', 0 );
							}
							THEMECOMPLETE_EPO_MATH_Error.trigger( $token.value, 'UnknownFunctionError', 0 );
						}
						$stack.push( $this.functions[ $token.value ].execute( $stack, $token.param_count ) );
					} else if ( THEMECOMPLETE_EPO_MATH_Constants.OPERATOR === $token.type ) {
						if ( ! array_key_exists( $token.value, $this.operators ) ) {
							return THEMECOMPLETE_EPO_MATH_Error.trigger( $token.value, 'UnknownOperatorError', 0 );
						}
						$stack.push( $this.operators[ $token.value ].execute( $stack ) );
					}
				} );
			} catch ( $e ) {
				if ( window.TMEPOJS && window.TMEPOJS.WP_DEBUG ) {
					window.console.log( $e );
				}
			}
			$result = $stack.pop();

			if ( undefined === $result || null === $result || ! empty( $stack ) ) {
				return THEMECOMPLETE_EPO_MATH_Error.trigger( 'Stack must be empty', 'IncorrectExpressionError', 0 );
			}

			if ( false === $result.value ) {
				$result.value = 0;
			}

			if ( true === $result.value ) {
				$result.value = 1;
			}

			if ( 'string' === gettype( $result.value ) && $result.value.isNumeric() ) {
				$result.value = floatval( $result.value );
			}

			return $result.value;
		};
		return node;
	};

	var THEMECOMPLETE_EPO_MATH_Error = {
		trigger: function( $msg, $code = '', $return = false ) {
			if ( window.TMEPOJS && window.TMEPOJS.WP_DEBUG ) {
				window.console.log( $code + '\n' + $msg );
				window.console.trace();
			}
			if ( window.TMEPOGLOBALADMINJS && window.TMEPOGLOBALADMINJS.WP_DEBUG ) {
				return $msg;
			}
			return $return;
		}
	};

	window.tcmexp = THEMECOMPLETE_EPO_MATH.construct();
}( window ) );
