<?php
if (defined('WFWAF_VERSION') && !defined('WFWAF_RUN_COMPLETE')) {

require_once dirname(__FILE__) . '/lexer.php';

class wfWAFSQLiParser extends wfWAFBaseParser {

	const FLAG_PARSE_MYSQL_PORTABLE_COMMENTS = wfWAFSQLiLexer::FLAG_TOKENIZE_MYSQL_PORTABLE_COMMENTS;

	/**
	 * @param string $param
	 * @return bool
	 */
	public static function testForSQLi($param) {
		static $instance;
		static $tests;
		if (!$instance) {
			$instance = new self(new wfWAFSQLiLexer());
		}
		if (!$tests) {
			// SQL statement and token count for lexer
			$tests = array(
				array('%s', 1),
				array('SELECT * FROM t WHERE i = %s ', 8),
				array("SELECT * FROM t WHERE i = '%s' ", 8),
				array('SELECT * FROM t WHERE i = "%s" ', 8),
				array('SELECT * FROM t WHERE i = (%s) ', 10),
				array("SELECT * FROM t WHERE i = ('%s') ", 10),
				array('SELECT * FROM t WHERE i = ("%s") ', 10),
				array('SELECT * FROM t WHERE i = ((%s)) ', 12),
				array("SELECT * FROM t WHERE i = (('%s')) ", 12),
				array('SELECT * FROM t WHERE i = (("%s")) ', 12),
				array('SELECT * FROM t WHERE i = (((%s))) ', 14),
				array("SELECT * FROM t WHERE i = ((('%s'))) ", 14),
				array('SELECT * FROM t WHERE i = ((("%s"))) ', 14),

				array('SELECT * FROM t WHERE i = %s and j = (1
) ', 14),
				array("SELECT * FROM t WHERE i = '%s' and j = (1
) ", 14),
				array('SELECT * FROM t WHERE i = "%s" and j = (1
) ', 14),

				array('SELECT MATCH(t) AGAINST (%s) from t ', 11),
				array("SELECT MATCH(t) AGAINST ('%s') from t ", 11),
				array('SELECT MATCH(t) AGAINST ("%s") from t ', 11),

//				array('SELECT CASE WHEN %s THEN 1 ELSE 0 END from t ', 11),
//				array("SELECT CASE WHEN '%s' THEN 1 ELSE 0 END from t ", 11),
//				array('SELECT CASE WHEN "%s" THEN 1 ELSE 0 END from t ', 11),
//
//				array('SELECT (CASE WHEN (%s) THEN 1 ELSE 0 END) from t ', 15),
//				array("SELECT (CASE WHEN ('%s') THEN 1 ELSE 0 END) from t ", 15),
//				array('SELECT (CASE WHEN ("%s") THEN 1 ELSE 0 END) from t ', 15),

				array('SELECT * FROM (select %s) ', 7),
				array("SELECT * FROM (select '%s') ", 7),
				array('SELECT * FROM (select "%s") ', 7),
				array('SELECT * FROM (select (%s)) ', 9),
				array("SELECT * FROM (select ('%s')) ", 9),
				array('SELECT * FROM (select ("%s")) ', 9),
				array('SELECT * FROM (select ((%s))) ', 11),
				array("SELECT * FROM (select (('%s'))) ", 11),
				array('SELECT * FROM (select (("%s"))) ', 11),
//
//				array('SELECT * FROM t JOIN t2 on i = %s ', 10),
//				array("SELECT * FROM t JOIN t2 on i = '%s' ", 10),
//				array('SELECT * FROM t JOIN t2 on i = "%s" ', 10),
//				array('SELECT * FROM t JOIN t2 on i = (%s) ', 12),
//				array("SELECT * FROM t JOIN t2 on i = ('%s') ", 12),
//				array('SELECT * FROM t JOIN t2 on i = ("%s") ', 12),
//				array('SELECT * FROM t JOIN t2 on i = ((%s)) ', 14),
//				array("SELECT * FROM t JOIN t2 on i = (('%s')) ", 14),
//				array('SELECT * FROM t JOIN t2 on i = (("%s")) ', 14),
//				array('SELECT * FROM t JOIN t2 on i = (((%s))) ', 16),
//				array("SELECT * FROM t JOIN t2 on i = ((('%s'))) ", 16),
//				array('SELECT * FROM t JOIN t2 on i = ((("%s"))) ', 16),

				array('SELECT * FROM %s ', 4),
				array('INSERT INTO t (col) VALUES (%s) ', 10),
				array("INSERT INTO t (col) VALUES ('%s') ", 10),
				array('INSERT INTO t (col) VALUES ("%s") ', 10),
				array('UPDATE t1 SET col1 = %s ', 6),
				array('UPDATE t1 SET col1 = \'%s\' ', 6),
			);
		}
		$lexerFlags = array(0, wfWAFSQLiLexer::FLAG_TOKENIZE_MYSQL_PORTABLE_COMMENTS);
		foreach ($lexerFlags as $flags) {
			foreach ($tests as $test) {
//				$startTime = microtime(true);
				list($sql, $expectedTokenCount) = $test;
				try {
					$instance->setFlags($flags);
					$instance->setSubject(sprintf($sql, $param));
					if (($instance->hasMoreThanNumTokens($expectedTokenCount) && $instance->evaluate())
						|| $instance->hasMultiplePortableCommentVersions()) {
//						printf("%s took %f seconds\n", $sql, microtime(true) - $startTime);
						return true;
					}
//					printf("%s took %f seconds\n", $sql, microtime(true) - $startTime);
				} catch (wfWAFParserSyntaxError $e) {

				}
			}
		}
		return false;
	}

	private $subject;

	/**
	 * @var int
	 */
	private $flags;
	/** @var wfWAFSQLiLexer */
	protected $lexer;
	private $portableCommentVersions = array();

	private $intervalUnits = array(
		'SECOND',
		'MINUTE',
		'HOUR',
		'DAY_SYM',
		'WEEK',
		'MONTH',
		'QUARTER',
		'YEAR',
		'SECOND_MICROSECOND',
		'MINUTE_MICROSECOND',
		'MINUTE_SECOND',
		'HOUR_MICROSECOND',
		'HOUR_SECOND',
		'HOUR_MINUTE',
		'DAY_MICROSECOND',
		'DAY_SECOND',
		'DAY_MINUTE',
		'DAY_HOUR',
		'YEAR_MONTH',
	);

	private $reservedWords = array(
		"_FILENAME",
		"ACCESSIBLE",
		"ADD",
		"ALL",
		"ALTER",
		"ANALYZE",
		"AND",
		"AS",
		"ASC",
		"ASENSITIVE",
		"BEFORE",
		"BETWEEN",
		"BIGINT",
		"BINARY",
		"BLOB",
		"BOTH",
		"BY",
		"CALL",
		"CASCADE",
		"CASE",
		"CHANGE",
		"CHAR",
		"CHARACTER",
		"CHECK",
		"COLLATE",
		"COLUMN",
		"CONDITION",
		"CONSTRAINT",
		"CONTINUE",
		"CONVERT",
		"CREATE",
		"CROSS",
		"CURRENT_DATE",
		"CURRENT_TIME",
		"CURRENT_TIMESTAMP",
		"CURRENT_USER",
		"CURSOR",
		"DATABASE",
		"DATABASES",
		"DAY_HOUR",
		"DAY_MICROSECOND",
		"DAY_MINUTE",
		"DAY_SECOND",
		"DEC",
		"DECIMAL",
		"DECLARE",
		"DEFAULT",
		"DELAYED",
		"DELETE",
		"DESC",
		"DESCRIBE",
		"DETERMINISTIC",
		"DISTINCT",
		"DISTINCTROW",
		"DIV",
		"DOUBLE",
		"DROP",
//		"DUAL", // works as a table name ???
		"EACH",
		"ELSE",
		"ELSEIF",
		"ENCLOSED",
		"ESCAPED",
		"EXISTS",
		"EXIT",
		"EXPLAIN",
		"FALSE",
		"FETCH",
		"FLOAT",
		"FLOAT4",
		"FLOAT8",
		"FOR",
		"FORCE",
		"FOREIGN",
		"FROM",
		"FULLTEXT",
		"GRANT",
		"GROUP",
		"HAVING",
		"HIGH_PRIORITY",
		"HOUR_MICROSECOND",
		"HOUR_MINUTE",
		"HOUR_SECOND",
		"IF",
		"IGNORE",
		"IN",
		"INDEX",
		"INFILE",
		"INNER",
		"INOUT",
		"INSENSITIVE",
		"INSERT",
		"INT",
		"INT1",
		"INT2",
		"INT3",
		"INT4",
		"INT8",
		"INTEGER",
		"INTERVAL",
		"INTO",
		"IS",
		"ITERATE",
		"JOIN",
		"KEY",
		"KEYS",
		"KILL",
		"LEADING",
		"LEAVE",
		"LEFT",
		"LIKE",
		"LIMIT",
		"LINEAR",
		"LINES",
		"LOAD",
		"LOCALTIME",
		"LOCALTIMESTAMP",
		"LOCK",
		"LONG",
		"LONGBLOB",
		"LONGTEXT",
		"LOOP",
		"LOW_PRIORITY",
		"MASTER_SSL_VERIFY_SERVER_CERT",
		"MATCH",
		"MAXVALUE",
		"MEDIUMBLOB",
		"MEDIUMINT",
		"MEDIUMTEXT",
		"MIDDLEINT",
		"MINUTE_MICROSECOND",
		"MINUTE_SECOND",
		"MOD",
		"MODIFIES",
		"NATURAL",
		"NOT",
		"NO_WRITE_TO_BINLOG",
		"NULL",
		"NUMERIC",
		"ON",
		"OPTIMIZE",
		"OPTION",
		"OPTIONALLY",
		"OR",
		"ORDER",
		"OUT",
		"OUTER",
		"OUTFILE",
		"PRECISION",
		"PRIMARY",
		"PROCEDURE",
		"PURGE",
		"RANGE",
		"READ",
		"READS",
		"READ_WRITE",
		"REAL",
		"REFERENCES",
		"REGEXP",
		"RELEASE",
		"RENAME",
		"REPEAT",
		"REPLACE",
		"REQUIRE",
		"RESIGNAL",
		"RESTRICT",
		"RETURN",
		"REVOKE",
		"RIGHT",
		"RLIKE",
		"SCHEMA",
		"SCHEMAS",
		"SECOND_MICROSECOND",
		"SELECT",
		"SENSITIVE",
		"SEPARATOR",
		"SET",
		"SHOW",
		"SIGNAL",
		"SMALLINT",
		"SPATIAL",
		"SPECIFIC",
		"SQL",
		"SQLEXCEPTION",
		"SQLSTATE",
		"SQLWARNING",
		"SQL_BIG_RESULT",
		"SQL_CALC_FOUND_ROWS",
		"SQL_SMALL_RESULT",
		"SSL",
		"STARTING",
		"STRAIGHT_JOIN",
		"TABLE",
		"TERMINATED",
		"THEN",
		"TINYBLOB",
		"TINYINT",
		"TINYTEXT",
		"TO",
		"TRAILING",
		"TRIGGER",
		"TRUE",
		"UNDO",
		"UNION",
		"UNIQUE",
		"UNLOCK",
		"UNSIGNED",
		"UPDATE",
		"USAGE",
		"USE",
		"USING",
		"UTC_DATE",
		"UTC_TIME",
		"UTC_TIMESTAMP",
		"VALUES",
		"VARBINARY",
		"VARCHAR",
		"VARCHARACTER",
		"VARYING",
		"WHEN",
		"WHERE",
		"WHILE",
		"WITH",
		"WRITE",
		"XOR",
		"YEAR_MONTH",
		"ZEROFILL",
	);
	private $keywords = array(
		"ACCESSIBLE",
		"ACTION",
		"ADD",
		"AFTER",
		"AGAINST",
		"AGGREGATE",
		"ALGORITHM",
		"ALL",
		"ALTER",
		"ANALYZE",
		"AND",
		"ANY",
		"AS",
		"ASC",
		"ASCII",
		"ASENSITIVE",
		"AT",
		"AUTHORS",
		"AUTOEXTEND_SIZE",
		"AUTO_INCREMENT",
		"AVG",
		"AVG_ROW_LENGTH",
		"BACKUP",
		"BEFORE",
		"BEGIN",
		"BETWEEN",
		"BIGINT",
		"BINARY",
		"BINLOG",
		"BIT",
		"BLOB",
		"BLOCK",
		"BOOL",
		"BOOLEAN",
		"BOTH",
		"BTREE",
		"BY",
		"BYTE",
		"CACHE",
		"CALL",
		"CASCADE",
		"CASCADED",
		"CASE",
		"CATALOG_NAME",
		"CHAIN",
		"CHANGE",
		"CHANGED",
		"CHAR",
		"CHARACTER",
		"CHARSET",
		"CHECK",
		"CHECKSUM",
		"CIPHER",
		"CLASS_ORIGIN",
		"CLIENT",
		"CLOSE",
		"COALESCE",
		"CODE",
		"COLLATE",
		"COLLATION",
		"COLUMN",
		"COLUMNS",
		"COLUMN_NAME",
		"COMMENT",
		"COMMIT",
		"COMMITTED",
		"COMPACT",
		"COMPLETION",
		"COMPRESSED",
		"CONCURRENT",
		"CONDITION",
		"CONNECTION",
		"CONSISTENT",
		"CONSTRAINT",
		"CONSTRAINT_CATALOG",
		"CONSTRAINT_NAME",
		"CONSTRAINT_SCHEMA",
		"CONTAINS",
		"CONTEXT",
		"CONTINUE",
		"CONTRIBUTORS",
		"CONVERT",
		"CPU",
		"CREATE",
		"CROSS",
		"CUBE",
		"CURRENT_DATE",
		"CURRENT_TIME",
		"CURRENT_TIMESTAMP",
		"CURRENT_USER",
		"CURSOR",
		"CURSOR_NAME",
		"DATA",
		"DATABASE",
		"DATABASES",
		"DATAFILE",
		"DATE",
		"DATETIME",
		"DAY",
		"DAY_HOUR",
		"DAY_MICROSECOND",
		"DAY_MINUTE",
		"DAY_SECOND",
		"DEALLOCATE",
		"DEC",
		"DECIMAL",
		"DECLARE",
		"DEFAULT",
		"DEFINER",
		"DELAYED",
		"DELAY_KEY_WRITE",
		"DELETE",
		"DESC",
		"DESCRIBE",
		"DES_KEY_FILE",
		"DETERMINISTIC",
		"DIRECTORY",
		"DISABLE",
		"DISCARD",
		"DISK",
		"DISTINCT",
		"DISTINCTROW",
		"DIV",
		"DO",
		"DOUBLE",
		"DROP",
		"DUAL",
		"DUMPFILE",
		"DUPLICATE",
		"DYNAMIC",
		"EACH",
		"ELSE",
		"ELSEIF",
		"ENABLE",
		"ENCLOSED",
		"END",
		"ENDS",
		"ENGINE",
		"ENGINES",
		"ENUM",
		"ERROR",
		"ERRORS",
		"ESCAPE",
		"ESCAPED",
		"EVENT",
		"EVENTS",
		"EVERY",
		"EXECUTE",
		"EXISTS",
		"EXIT",
		"EXPANSION",
		"EXPLAIN",
		"EXTENDED",
		"EXTENT_SIZE",
		"FALSE",
		"FAST",
		"FAULTS",
		"FETCH",
		"FIELDS",
		"FILE",
		"FIRST",
		"FIXED",
		"FLOAT",
		"FLOAT4",
		"FLOAT8",
		"FLUSH",
		"FOR",
		"FORCE",
		"FOREIGN",
		"FOUND",
		"FRAC_SECOND",
		"FROM",
		"FULL",
		"FULLTEXT",
		"FUNCTION",
		"GENERAL",
		"GEOMETRY",
		"GEOMETRYCOLLECTION",
		"GET_FORMAT",
		"GLOBAL",
		"GRANT",
		"GRANTS",
		"GROUP",
		"HANDLER",
		"HASH",
		"HAVING",
		"HELP",
		"HIGH_PRIORITY",
		"HOST",
		"HOSTS",
		"HOUR",
		"HOUR_MICROSECOND",
		"HOUR_MINUTE",
		"HOUR_SECOND",
		"IDENTIFIED",
		"IF",
		"IGNORE",
		"IGNORE_SERVER_IDS",
		"IMPORT",
		"IN",
		"INDEX",
		"INDEXES",
		"INFILE",
		"INITIAL_SIZE",
		"INNER",
		"INNOBASE",
		"INNODB",
		"INOUT",
		"INSENSITIVE",
		"INSERT",
		"INSERT_METHOD",
		"INSTALL",
		"INT",
		"INT1",
		"INT2",
		"INT3",
		"INT4",
		"INT8",
		"INTEGER",
		"INTERVAL",
		"INTO",
		"INVOKER",
		"IO",
		"IO_THREAD",
		"IPC",
		"IS",
		"ISOLATION",
		"ISSUER",
		"ITERATE",
		"JOIN",
		"KEY",
		"KEYS",
		"KEY_BLOCK_SIZE",
		"KILL",
		"LANGUAGE",
		"LAST",
		"LEADING",
		"LEAVE",
		"LEAVES",
		"LEFT",
		"LESS",
		"LEVEL",
		"LIKE",
		"LIMIT",
		"LINEAR",
		"LINES",
		"LINESTRING",
		"LIST",
		"LOAD",
		"LOCAL",
		"LOCALTIME",
		"LOCALTIMESTAMP",
		"LOCK",
		"LOCKS",
		"LOGFILE",
		"LOGS",
		"LONG",
		"LONGBLOB",
		"LONGTEXT",
		"LOOP",
		"LOW_PRIORITY",
		"MASTER",
		"MASTER_CONNECT_RETRY",
		"MASTER_HEARTBEAT_PERIOD",
		"MASTER_HOST",
		"MASTER_LOG_FILE",
		"MASTER_LOG_POS",
		"MASTER_PASSWORD",
		"MASTER_PORT",
		"MASTER_SERVER_ID",
		"MASTER_SSL",
		"MASTER_SSL_CA",
		"MASTER_SSL_CAPATH",
		"MASTER_SSL_CERT",
		"MASTER_SSL_CIPHER",
		"MASTER_SSL_KEY",
		"MASTER_SSL_VERIFY_SERVER_CERT",
		"MASTER_USER",
		"MATCH",
		"MAXVALUE",
		"MAX_CONNECTIONS_PER_HOUR",
		"MAX_QUERIES_PER_HOUR",
		"MAX_ROWS",
		"MAX_SIZE",
		"MAX_UPDATES_PER_HOUR",
		"MAX_USER_CONNECTIONS",
		"MEDIUM",
		"MEDIUMBLOB",
		"MEDIUMINT",
		"MEDIUMTEXT",
		"MEMORY",
		"MERGE",
		"MESSAGE_TEXT",
		"MICROSECOND",
		"MIDDLEINT",
		"MIGRATE",
		"MINUTE",
		"MINUTE_MICROSECOND",
		"MINUTE_SECOND",
		"MIN_ROWS",
		"MOD",
		"MODE",
		"MODIFIES",
		"MODIFY",
		"MONTH",
		"MULTILINESTRING",
		"MULTIPOINT",
		"MULTIPOLYGON",
		"MUTEX",
		"MYSQL_ERRNO",
		"NAME",
		"NAMES",
		"NATIONAL",
		"NATURAL",
		"NCHAR",
		"NDB",
		"NDBCLUSTER",
		"NEW",
		"NEXT",
		"NO",
		"NODEGROUP",
		"NONE",
		"NOT",
		"NO_WAIT",
		"NO_WRITE_TO_BINLOG",
		"NULL",
		"NUMERIC",
		"NVARCHAR",
		"OFFSET",
		"OLD_PASSWORD",
		"ON",
		"ONE",
		"ONE_SHOT",
		"OPEN",
		"OPTIMIZE",
		"OPTION",
		"OPTIONALLY",
		"OPTIONS",
		"OR",
		"ORDER",
		"OUT",
		"OUTER",
		"OUTFILE",
		"OWNER",
		"PACK_KEYS",
		"PAGE",
		"PARSER",
		"PARTIAL",
		"PARTITION",
		"PARTITIONING",
		"PARTITIONS",
		"PASSWORD",
		"PHASE",
		"PLUGIN",
		"PLUGINS",
		"POINT",
		"POLYGON",
		"PORT",
		"PRECISION",
		"PREPARE",
		"PRESERVE",
		"PREV",
		"PRIMARY",
		"PRIVILEGES",
		"PROCEDURE",
		"PROCESSLIST",
		"PROFILE",
		"PROFILES",
		"PROXY",
		"PURGE",
		"QUARTER",
		"QUERY",
		"QUICK",
		"RANGE",
		"READ",
		"READS",
		"READ_ONLY",
		"READ_WRITE",
		"REAL",
		"REBUILD",
		"RECOVER",
		"REDOFILE",
		"REDO_BUFFER_SIZE",
		"REDUNDANT",
		"REFERENCES",
		"REGEXP",
		"RELAY",
		"RELAYLOG",
		"RELAY_LOG_FILE",
		"RELAY_LOG_POS",
		"RELAY_THREAD",
		"RELEASE",
		"RELOAD",
		"REMOVE",
		"RENAME",
		"REORGANIZE",
		"REPAIR",
		"REPEAT",
		"REPEATABLE",
		"REPLACE",
		"REPLICATION",
		"REQUIRE",
		"RESET",
		"RESIGNAL",
		"RESTORE",
		"RESTRICT",
		"RESUME",
		"RETURN",
		"RETURNS",
		"REVOKE",
		"RIGHT",
		"RLIKE",
		"ROLLBACK",
		"ROLLUP",
		"ROUTINE",
		"ROW",
		"ROWS",
		"ROW_FORMAT",
		"RTREE",
		"SAVEPOINT",
		"SCHEDULE",
		"SCHEMA",
		"SCHEMAS",
		"SCHEMA_NAME",
		"SECOND",
		"SECOND_MICROSECOND",
		"SECURITY",
		"SELECT",
		"SENSITIVE",
		"SEPARATOR",
		"SERIAL",
		"SERIALIZABLE",
		"SERVER",
		"SESSION",
		"SET",
		"SHARE",
		"SHOW",
		"SHUTDOWN",
		"SIGNAL",
		"SIGNED",
		"SIMPLE",
		"SLAVE",
		"SLOW",
		"SMALLINT",
		"SNAPSHOT",
		"SOCKET",
		"SOME",
		"SONAME",
		"SOUNDS",
		"SOURCE",
		"SPATIAL",
		"SPECIFIC",
		"SQL",
		"SQLEXCEPTION",
		"SQLSTATE",
		"SQLWARNING",
		"SQL_BIG_RESULT",
		"SQL_BUFFER_RESULT",
		"SQL_CACHE",
		"SQL_CALC_FOUND_ROWS",
		"SQL_NO_CACHE",
		"SQL_SMALL_RESULT",
		"SQL_THREAD",
		"SQL_TSI_DAY",
		"SQL_TSI_FRAC_SECOND",
		"SQL_TSI_HOUR",
		"SQL_TSI_MINUTE",
		"SQL_TSI_MONTH",
		"SQL_TSI_QUARTER",
		"SQL_TSI_SECOND",
		"SQL_TSI_WEEK",
		"SQL_TSI_YEAR",
		"SSL",
		"START",
		"STARTING",
		"STARTS",
		"STATUS",
		"STOP",
		"STORAGE",
		"STRAIGHT_JOIN",
		"STRING",
		"SUBCLASS_ORIGIN",
		"SUBJECT",
		"SUBPARTITION",
		"SUBPARTITIONS",
		"SUPER",
		"SUSPEND",
		"SWAPS",
		"SWITCHES",
		"TABLE",
		"TABLES",
		"TABLESPACE",
		"TABLE_CHECKSUM",
		"TABLE_NAME",
		"TEMPORARY",
		"TEMPTABLE",
		"TERMINATED",
		"TEXT",
		"THAN",
		"THEN",
		"TIME",
		"TIMESTAMP",
		"TIMESTAMPADD",
		"TIMESTAMPDIFF",
		"TINYBLOB",
		"TINYINT",
		"TINYTEXT",
		"TO",
		"TRAILING",
		"TRANSACTION",
		"TRIGGER",
		"TRIGGERS",
		"TRUE",
		"TRUNCATE",
		"TYPE",
		"TYPES",
		"UNCOMMITTED",
		"UNDEFINED",
		"UNDO",
		"UNDOFILE",
		"UNDO_BUFFER_SIZE",
		"UNICODE",
		"UNINSTALL",
		"UNION",
		"UNIQUE",
		"UNKNOWN",
		"UNLOCK",
		"UNSIGNED",
		"UNTIL",
		"UPDATE",
		"UPGRADE",
		"USAGE",
		"USE",
		"USER",
		"USER_RESOURCES",
		"USE_FRM",
		"USING",
		"UTC_DATE",
		"UTC_TIME",
		"UTC_TIMESTAMP",
		"VALUE",
		"VALUES",
		"VARBINARY",
		"VARCHAR",
		"VARCHARACTER",
		"VARIABLES",
		"VARYING",
		"VIEW",
		"WAIT",
		"WARNINGS",
		"WEEK",
		"WHEN",
		"WHERE",
		"WHILE",
		"WITH",
		"WORK",
		"WRAPPER",
		"WRITE",
		"X509",
		"XA",
		"XML",
		"XOR",
		"YEAR",
		"YEAR_MONTH",
		"ZEROFILL",
	);

	private $numberFunctions = array(
		'ABS',
		'ACOS',
		'ASIN',
		'ATAN2',
		'ATAN',
		'CEIL',
		'CEILING',
		'CONV',
		'COS',
		'COT',
		'CRC32',
		'DEGREES',
		'EXP',
		'FLOOR',
		'LN',
		'LOG10',
		'LOG2',
		'LOG',
		'MOD',
		'PI',
		'POW',
		'POWER',
		'RADIANS',
		'RAND',
		'ROUND',
		'SIGN',
		'SIN',
		'SQRT',
		'TAN',
		'TRUNCATE',
	);

	private $charFunctions = array(
		'ASCII_SYM',
		'BIN',
		'BIT_LENGTH',
		'CHAR_LENGTH',
		'CHAR',
		'CONCAT_WS',
		'CONCAT',
		'ELT',
		'EXPORT_SET',
		'FIELD',
		'FIND_IN_SET',
		'FORMAT',
		'FROM_BASE64',
		'HEX',
		'INSERT',
		'INSTR',
		'LEFT',
		'LENGTH',
		'LOAD_FILE',
		'LOCATE',
		'LOWER',
		'LPAD',
		'LTRIM',
		'MAKE_SET',
		'MID',
		'OCT',
		'ORD',
		'QUOTE',
		'REPEAT',
		'REPLACE',
		'REVERSE',
		'RIGHT',
		'RPAD',
		'RTRIM',
		'SOUNDEX',
		'SPACE',
		'STRCMP',
		'SUBSTRING_INDEX',
		'SUBSTRING',
		'TO_BASE64',
		'TRIM',
		'UNHEX',
		'UPPER',
		'WEIGHT_STRING',
	);

	private $timeFunctions = array(
		'ADDDATE',
		'ADDTIME',
		'CONVERT_TZ',
		'CURDATE',
		'CURTIME',
		'DATE_ADD',
		'DATE_FORMAT',
		'DATE_SUB',
		'DATE_SYM',
		'DATEDIFF',
		'DAYNAME',
		'DAYOFMONTH',
		'DAYOFWEEK',
		'DAYOFYEAR',
		'EXTRACT',
		'FROM_DAYS',
		'FROM_UNIXTIME',
		'GET_FORMAT',
		'HOUR',
		'LAST_DAY ',
		'MAKEDATE',
		'MAKETIME ',
		'MICROSECOND',
		'MINUTE',
		'MONTH',
		'MONTHNAME',
		'NOW',
		'PERIOD_ADD',
		'PERIOD_DIFF',
		'QUARTER',
		'SEC_TO_TIME',
		'SECOND',
		'STR_TO_DATE',
		'SUBTIME',
		'SYSDATE',
		'TIME_FORMAT',
		'TIME_TO_SEC',
		'TIME_SYM',
		'TIMEDIFF',
		'TIMESTAMP',
		'TIMESTAMPADD',
		'TIMESTAMPDIFF',
		'TO_DAYS',
		'TO_SECONDS',
		'UNIX_TIMESTAMP',
		'UTC_DATE',
		'UTC_TIME',
		'UTC_TIMESTAMP',
		'WEEK',
		'WEEKDAY',
		'WEEKOFYEAR',
		'YEAR',
		'YEARWEEK',
	);

	private $otherFunctions = array(
		'MAKE_SET', 'LOAD_FILE',
		'IF', 'IFNULL',
		'AES_ENCRYPT', 'AES_DECRYPT',
		'DECODE', 'ENCODE',
		'DES_DECRYPT', 'DES_ENCRYPT',
		'ENCRYPT', 'MD5',
		'OLD_PASSWORD', 'PASSWORD',
		'BENCHMARK', 'CHARSET', 'COERCIBILITY', 'COLLATION', 'CONNECTION_ID',
		'CURRENT_USER', 'DATABASE', 'SCHEMA', 'USER', 'SESSION_USER', 'SYSTEM_USER',
		'VERSION_SYM',
		'FOUND_ROWS', 'LAST_INSERT_ID', 'DEFAULT',
		'GET_LOCK', 'RELEASE_LOCK', 'IS_FREE_LOCK', 'IS_USED_LOCK', 'MASTER_POS_WAIT',
		'INET_ATON', 'INET_NTOA',
		'NAME_CONST',
		'SLEEP',
		'UUID',
		'VALUES',
	);

	private $groupFunctions = array(
		'AVG', 'COUNT', 'MAX_SYM', 'MIN_SYM', 'SUM',
		'BIT_AND', 'BIT_OR', 'BIT_XOR',
		'GROUP_CONCAT',
		'STD', 'STDDEV', 'STDDEV_POP', 'STDDEV_SAMP',
		'VAR_POP', 'VAR_SAMP', 'VARIANCE',
	);


	/**
	 * @param wfWAFSQLiLexer $lexer
	 * @param string $subject
	 * @param int $flags
	 */
	public function __construct($lexer, $subject = null, $flags = 0) {
		parent::__construct($lexer);
		$this->setSubject($subject);
		$this->setFlags($flags);
	}

	protected function _init() {
		$this->portableCommentVersions = array();
		$this->index = -1;
	}

	/**
	 * @param int $num
	 * @return bool
	 */
	public function hasMoreThanNumTokens($num) {
		$this->_init();

		$savePoint = $this->index;
		for ($i = 0; $i <= $num; $i++) {
			if (!$this->nextToken()) {
				$this->index = $savePoint;
				return false;
			}
		}
		$this->index = $savePoint;
		return true;
	}

	/**
	 * @return bool
	 */
	public function evaluate() {
		try {
			$this->parse();
			return true;
		} catch (wfWAFParserSyntaxError $e) {
			return false;
		}
	}

	public function parse() {
		$this->_init();
		if (
			$this->parseSelectStatement()
			|| $this->parseInsertStatement()
			|| $this->parseUpdateStatement()
//			|| $this->parseDeleteStatement()
//			|| $this->parseReplaceStatement()
		) {
			$token = $this->nextToken();
			if ($token && !$this->isTokenOfType($token, wfWAFSQLiLexer::SEMICOLON)) {
				$this->triggerSyntaxError($this->currentToken());
			}
		} else {
			$this->triggerSyntaxError($this->expectNextToken());
		}
	}

	/**
	 * @param int $index
	 * @return bool
	 */
	protected function getToken($index) {
		if (array_key_exists($index, $this->tokens)) {
			return $this->tokens[$index];
		}
		while ($token = $this->getLexer()->nextToken()) {
			if (!$this->isCommentToken($token)) {
				$this->tokens[$index] = $token;
				return $this->tokens[$index];
			}
		}
		return false;
	}


	/**
	 * @param wfWAFLexerToken $token
	 * @return bool
	 */
	public function isCommentToken($token) {
		if ($this->isTokenOfType($token, wfWAFSQLiLexer::MYSQL_PORTABLE_COMMENT_START)) {
			$this->portableCommentVersions[(int) preg_replace('/[^\d]/', '', $token->getValue())] = 1;
		}

		return $this->isTokenOfType($token, array(
			wfWAFSQLiLexer::SINGLE_LINE_COMMENT,
			wfWAFSQLiLexer::MULTI_LINE_COMMENT,
			wfWAFSQLiLexer::MYSQL_PORTABLE_COMMENT_START,
			wfWAFSQLiLexer::MYSQL_PORTABLE_COMMENT_END,
		));
	}

	public function hasMultiplePortableCommentVersions() {
		return count($this->portableCommentVersions) > 1;
	}

	/**
	 * Expects the next token to be an identifier with the supplied case-insensitive value
	 *
	 * @param $keyword
	 * @return wfWAFLexerToken
	 * @throws wfWAFParserSyntaxError
	 */
	protected function expectNextIdentifierEquals($keyword) {
		$nextToken = $this->expectNextToken();
		$this->expectTokenTypeEquals($nextToken, wfWAFSQLiLexer::UNQUOTED_IDENTIFIER);
		if ($nextToken->getLowerCaseValue() !== wfWAFUtils::strtolower($keyword)) {
			$this->triggerSyntaxError($nextToken);
		}
		return $nextToken;
	}

	private function parseSelectStatement() {
		$startIndex = $this->index;
		$hasSelect = false;
		while ($this->parseSelectExpression()) {
			$hasSelect = true;
			$savePoint = $this->index;
			if ($this->isIdentifierWithValue($this->nextToken(), 'union')) {
				$hasSelect = false;
				if (!$this->isIdentifierWithValue($this->nextToken(), 'all')) {
					$this->index--;
				}
				continue;
			}
			$this->index = $savePoint;
			break;
		}
		if ($hasSelect) {
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	private function parseSelectExpression() {
		$savePoint = $this->index;
		if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS) &&
			$this->parseSelectExpression() &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)
		) {
			return true;
		}
		$this->index = $savePoint;

		if ($this->parseSelect()) {
			if ($this->parseFrom()) {
				$this->parseWhere();
				$this->parseProcedure();
				$this->parseGroupBy();
				$this->parseHaving();
			}
			$this->parseOrderBy();
			$this->parseLimit();

			return true;
		}
		return false;
	}

	/**
	 * @throws wfWAFParserSyntaxError
	 */
	private function parseSelect() {
		$startPoint = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'select')) {
			$optionalSelectParamsRegex = '/ALL|DISTINCT(?:ROW)?|HIGH_PRIORITY|MAX_STATEMENT_TIME|STRAIGHT_JOIN|SQL_SMALL_RESULT|SQL_BIG_RESULT|SQL_BUFFER_RESULT|SQL_CACHE|SQL_NO_CACHE|SQL_CALC_FOUND_ROWS/i';
			while (true) {
				$savePoint = $this->index;
				$token = $this->nextToken();
				if ($token) {
					$value = $token->getLowerCaseValue();
					if (preg_match($optionalSelectParamsRegex, $value)) {
						if ($value == 'max_statement_time') {
							$this->expectTokenTypeEquals($this->expectNextToken(), wfWAFSQLiLexer::EQUALS_SYMBOL);
							$this->expectTokenTypeInArray($this->expectNextToken(), array(
								wfWAFSQLiLexer::INTEGER_LITERAL,
								wfWAFSQLiLexer::BINARY_NUMBER_LITERAL,
								wfWAFSQLiLexer::HEX_NUMBER_LITERAL,
								wfWAFSQLiLexer::BINARY_NUMBER_LITERAL,
							));
						}
						continue;
					}
				}
				$this->index = $savePoint;
				break;
			}
			return $this->parseSelectList();
		}
		$this->index = $startPoint;
		return false;
	}

	/**
	 * @throws wfWAFParserSyntaxError
	 */
	private function parseSelectList() {
		$startPoint = $this->index;
		$hasSelects = false;
		if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::ASTERISK)) {
			$hasSelects = true;
			if (!$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
				// Just SELECT * [FROM ...]
				$this->index--;
				return true;
			}
		} else {
			$this->index = $startPoint;
		}
		while ($this->parseDisplayedColumn()) {
			$hasSelects = true;
			if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
				continue;
			}
			$this->index--;
			break;
		}
		if ($hasSelects) {
			return true;
		}
		$this->index = $startPoint;
		return false;
	}

	private function parseDisplayedColumn() {
		/*
		( table_spec DOT ASTERISK )
		|
		( column_spec (alias)? )
		|
		( bit_expr (alias)? )
		*/
		$savePoint = $this->index;
		if ($this->parseTableSpec() &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::DOT) &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::ASTERISK)
		) {
			return true;
		}
		$this->index = $savePoint;

		$savePoint = $this->index;
		if ($this->parseExpression()) {
			$this->parseAlias();
			return true;
		}
		$this->index = $savePoint;

		$savePoint = $this->index;
		if ($this->parseColumnSpec()) {
			$this->parseAlias();
			return true;
		}
		$this->index = $savePoint;

		return false;
	}

	/**
	 * @return bool
	 */
	private function parseExpressionList() {
		$startIndex = $this->index;
		if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS)) {
			$hasExpressions = false;
			while ($this->parseExpression()) {
				$hasExpressions = true;
				if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
					continue;
				}
				$this->index--;
				break;
			}
			if ($hasExpressions && $this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)) {
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	private function parseExpression() {
		// Combines these:
		// exp_factor3 ( AND_SYM exp_factor3 )* ;
		// expression:	exp_factor1 ( OR_SYM exp_factor1 )* ;
		// exp_factor1:	exp_factor2 ( XOR exp_factor2 )* ;
		// exp_factor2:	exp_factor3 ( AND_SYM exp_factor3 )* ;

		$savePoint = $this->index;
		$hasExpression = false;
		while ($this->parseExpressionFactor3()) {
			$hasExpression = true;
			$savePoint2 = $this->index;
			$token = $this->nextToken();
			if ($this->isOrToken($token) || $this->isAndToken($token) || $this->isIdentifierWithValue($token, 'xor')) {
				continue;
			}
			$this->index = $savePoint2;
			break;
		}
		if ($hasExpression) {
			return true;
		}
		$this->index = $savePoint;
		return false;
	}

	private function parseExpressionFactor3() {
		// (NOT_SYM)? exp_factor4 ;
		$savePoint = $this->index;
		if (!$this->isNotSymbolToken($this->nextToken())) {
			$this->index--;
		}
		if ($this->parseExpressionFactor4()) {
			return true;
		}
		$this->index = $savePoint;
		return false;
	}

	private function parseExpressionFactor4() {
		// bool_primary ( IS_SYM (NOT_SYM)? (boolean_literal|NULL_SYM) )? ;
		$savePoint = $this->index;
		if ($this->parseBoolPrimary()) {
			$savePoint = $this->index;
			if ($this->isIdentifierWithValue($this->nextToken(), 'is')) {
				if (!$this->isNotSymbolToken($this->nextToken())) {
					$this->index--;
				}
				if ($this->isIdentifierWithValue($this->nextToken(), array(
					'true', 'false', 'null',
				))
				) {
					return true;
				}
			}
			$this->index = $savePoint;
			return true;
		}
		$this->index = $savePoint;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseBoolPrimary() {
		$startIndex = $this->index;
		$token = $this->nextToken();
		if ($token) {
			$hasNot = false;
			if ($this->isNotSymbolToken($token)) {
				$hasNot = true;
				$token = $this->nextToken();
			}
			$val = $token->getLowerCaseValue();
			if ($token->getType() === wfWAFSQLiLexer::UNQUOTED_IDENTIFIER) {
				if ($val === 'exists' && $this->parseSubquery()) {
					return true;
				} else if ($hasNot) {
					$this->index = $startIndex;
					return false;
				}
			}
			if (!$hasNot) {
				$this->index = $startIndex;
			}
		}

		if ($this->parsePredicate()) {
			$savePoint = $this->index;
			$opToken = $this->nextToken();
			if ($opToken) {
				switch ($opToken->getType()) {
					case wfWAFSQLiLexer::EQUALS_SYMBOL:
					case wfWAFSQLiLexer::LESS_THAN:
					case wfWAFSQLiLexer::GREATER_THAN:
					case wfWAFSQLiLexer::LESS_THAN_EQUAL_TO:
					case wfWAFSQLiLexer::GREATER_THAN_EQUAL_TO:
					case wfWAFSQLiLexer::NOT_EQUALS:
					case wfWAFSQLiLexer::SET_VAR:
						$savePoint2 = $this->index;
						if ($this->isIdentifierWithValue($this->nextToken(), array(
								'any', 'all'
							)) &&
							$this->parseSubquery()
						) {
							return true;
						}
						$this->index = $savePoint2;

						$savePoint2 = $this->index;
						if ($this->testForSubquery() && $this->parseSubquery()) {
							return true;
						}
						$this->index = $savePoint2;

						if ($this->parsePredicate()) {
							return true;
						}
						$this->index = $startIndex;
						return false;
				}
			}
			$this->index = $savePoint;
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	private function parsePredicate() {
		$startIndex = $this->index;
		if ($this->parseBitExpression()) {
			$savePoint = $this->index;
			$token = $this->nextToken();
			if ($token) {
				if ($hasNot = $this->isNotSymbolToken($token)) {
					$token = $this->nextToken();
					if (!$token) {
						$this->index = $startIndex;
						return false;
					}
				}
				$val = $token->getLowerCaseValue();

				if ($token->getType() === wfWAFSQLiLexer::UNQUOTED_IDENTIFIER) {
					switch ($val) {
						case 'in':
							if ($this->parseSubquery() || $this->parseExpressionList()) {
								return true;
							}
							break;

						case 'between':
							if ($this->parseBitExpression() && $this->isIdentifierWithValue($this->nextToken(), 'and') &&
								$this->parsePredicate()
							) {
								return true;
							}
							break;

						case 'sounds':
							if ($this->isIdentifierWithValue($this->nextToken(), 'like') &&
								$this->parseBitExpression()
							) {
								return true;
							}
							break;

						case 'like':
						case 'rlike':
							if ($this->parseSimpleExpression()) {
								// We've got a LIKE statement at this point
								$savePoint = $this->index;
								if ($this->isIdentifierWithValue($this->nextToken(), 'escape') &&
									$this->parseSimpleExpression()
								) {
									return true;
								}
								$this->index = $savePoint;
								return true;
							}
							break;

						case 'regexp':
							if ($this->parseBitExpression()) {
								return true;
							}
							break;

						default:
							if ($hasNot) {
								$this->index = $startIndex;
								return false;
							}
							break;
					}
				}
			}
			$this->index = $savePoint;
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseBitExpression() {
		// factor1 ( VERTBAR factor1 )? ;
		$savePoint = $this->index;
		if ($this->parseBitExprFactor5()) {
			$savePoint = $this->index;
			$token = $this->nextToken();
			if (($this->isTokenOfType($token, array(
						wfWAFSQLiLexer::BIT_OR,
						wfWAFSQLiLexer::BIT_AND,
						wfWAFSQLiLexer::BIT_XOR,
						wfWAFSQLiLexer::BIT_LEFT_SHIFT,
						wfWAFSQLiLexer::BIT_RIGHT_SHIFT,
						wfWAFSQLiLexer::BIT_INVERSION,
						wfWAFSQLiLexer::PLUS,
						wfWAFSQLiLexer::MINUS,
						wfWAFSQLiLexer::ASTERISK,
						wfWAFSQLiLexer::DIVISION,
						wfWAFSQLiLexer::MOD,
					))
					||
					$this->isIdentifierWithValue($token, array(
						'div', 'mod'
					))) &&
				$this->parseBitExpression()
			) {
				return true;
			}
			$this->index = $savePoint;
			return true;
		}
		$this->index = $savePoint;
		return false;
	}


	private function parseBitExprFactor5() {
		// factor6 ( (PLUS|MINUS) interval_expr )? ;
		$savePoint = $this->index;
		if ($this->parseBitExprFactor6()) {
			$savePoint = $this->index;
			if ($this->isTokenOfType($this->nextToken(), array(
					wfWAFSQLiLexer::PLUS,
					wfWAFSQLiLexer::MINUS,
				)) && $this->parseIntervalExpression()
			) {
				return true;
			}
			$this->index = $savePoint;
			return true;
		}
		$this->index = $savePoint;
		return false;
	}

	private function parseBitExprFactor6() {
		// (PLUS | MINUS | NEGATION | BINARY) simple_expr
		// | simple_expr ;

		$startPoint = $this->index;
		$savePoint = $this->index;
		while (
			($token = $this->nextToken()) &&
			(
				$this->isTokenOfType($token, array(
					wfWAFSQLiLexer::PLUS,
					wfWAFSQLiLexer::MINUS,
				)) ||
				($this->isTokenOfType($token, wfWAFSQLiLexer::BIT_INVERSION)) ||
				($this->isIdentifierWithValue($token, 'BINARY'))
			)
		) {
			$savePoint = $this->index;
		}
		$this->index = $savePoint;

		if ($this->parseSimpleExpression()) {
			return true;
		}
		$this->index = $startPoint;
		return false;

	}

	/**
	 * literal_value
	 * | column_spec
	 * | function_call
	 * | USER_VAR
	 * | expression_list
	 * | (ROW_SYM expression_list)
	 * | subquery
	 * | EXISTS subquery
	 * | {identifier expr}
	 * | match_against_statement
	 * | case_when_statement
	 * | interval_expr
	 *
	 * @return bool
	 */
	private function parseSimpleExpression() {
		$startPoint = $this->index;
		$simple = ($parseLiteral = $this->parseLiteral()) ||
			($parseMatchAgainst = $this->parseMatchAgainst()) ||
			($parseFunctionCall = $this->parseFunctionCall()) ||
			($parseVariable = $this->parseVariable()) ||
			($parseExpressionList = $this->parseExpressionList()) ||
			($parseSubquery = $this->parseSubquery()) ||
			($parseExistsSubquery = $this->parseExistsSubquery()) ||
			($parseCaseWhen = $this->parseCaseWhen()) ||
			($parseODBCExpression = $this->parseODBCExpression()) ||
			($parseIntervalExpression = $this->parseIntervalExpression()) ||
			($parseColumnSpec = $this->parseColumnSpec());

		if ($simple) {
			$token = $this->nextToken();
			if ($token && $token->getLowerCaseValue() == 'collate') {
				$savePoint = $this->index;
				if ($this->parseCollationName()) {
					return true;
				}
				$this->index = $savePoint;
			} else {
				$this->index--;
			}
			return true;
		}
		$this->index = $startPoint;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseLiteral() {
		$startIndex = $this->index;
		$savePoint = $this->index;
		while ($this->isTokenOfType($this->nextToken(), array(
			wfWAFSQLiLexer::PLUS,
			wfWAFSQLiLexer::MINUS,
		))) {
			$savePoint = $this->index;
		}
		$this->index = $savePoint;

		// Check if this is a Character Set Introducer
		$nextToken = $this->nextToken();
		$hasCharacterSetIntroducer = $this->isTokenOfType($nextToken, wfWAFSQLiLexer::UNQUOTED_IDENTIFIER) &&
			substr($nextToken->getValue(), 0, 1) === '_';
		if (!$hasCharacterSetIntroducer) {
			$this->index--;
		}

		$validLiteral = false;
		$nextToken = $this->nextToken();
		if ($nextToken) {
			switch ($nextToken->getType()) {
				case wfWAFSQLiLexer::INTEGER_LITERAL:
				case wfWAFSQLiLexer::BINARY_NUMBER_LITERAL:
				case wfWAFSQLiLexer::HEX_NUMBER_LITERAL:
				case wfWAFSQLiLexer::REAL_NUMBER_LITERAL:
					$validLiteral = true;
					break;
				// Allow concatenation: 'test' 'test' is valid
				case wfWAFSQLiLexer::DOUBLE_STRING_LITERAL:
				case wfWAFSQLiLexer::SINGLE_STRING_LITERAL:
					$savePoint = $this->index;
					while ($this->isTokenOfType($this->nextToken(), array(
						wfWAFSQLiLexer::DOUBLE_STRING_LITERAL,
						wfWAFSQLiLexer::SINGLE_STRING_LITERAL
					))) {
						$savePoint = $this->index;
					}
					$this->index = $savePoint;
					$validLiteral = true;
					break;

				case wfWAFSQLiLexer::UNQUOTED_IDENTIFIER:
					if ($nextToken->getLowerCaseValue() === 'null') {
						$validLiteral = true;
					}
					break;
			}
		}


		if ($validLiteral) {
			if ($hasCharacterSetIntroducer) {
				// Check for and parse collation
				$savePoint = $this->index;
				$hasCollation = $this->isIdentifierWithValue($this->nextToken(), 'collation') &&
					$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::UNQUOTED_IDENTIFIER);
				if (!$hasCollation) {
					$this->index = $savePoint;
				}
			}

			return true;
		}

		$this->index = $startIndex;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseColumnSpec() {
		$savePoint = $this->index;
		if ($this->parseTableSpec()) {
			$savePoint = $this->index;
			if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::DOT)) {
				$nextToken = $this->nextToken();
				if ($nextToken && ($nextToken->getType() == wfWAFSQLiLexer::UNQUOTED_IDENTIFIER ||
						$nextToken->getType() == wfWAFSQLiLexer::QUOTED_IDENTIFIER)
				) {
					return true;
				}
				$this->index = $savePoint;
				return false;
			}

			$this->index = $savePoint;
			return true;
		}
		$this->index = $savePoint;
		return false;
	}

	/**
	 * CAST_SYM LPAREN expression AS_SYM cast_data_type RPAREN  )
	 * | (  CONVERT_SYM LPAREN expression COMMA cast_data_type RPAREN  )
	 * | (  CONVERT_SYM LPAREN expression USING_SYM transcoding_name RPAREN  )
	 * | (  group_functions LPAREN ( ASTERISK | ALL | DISTINCT )? bit_expr RPAREN  )
	 *
	 * @return bool
	 */
	private function parseFunctionCall() {
		$startPoint = $this->index;
		$functionToken = $this->nextToken();
		if ($functionToken && $functionToken->getType() === wfWAFSQLiLexer::UNQUOTED_IDENTIFIER) {
			if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS)) {
				switch ($functionToken->getLowerCaseValue()) {
					case 'cast':
						if ($this->parseExpression() &&
							$this->isIdentifierWithValue($this->nextToken(), 'as') &&
							$this->parseCastDataType() &&
							$this->parseOptionalCharacterSet() &&
							$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)
						) {
							return true;
						}
						break;

					case 'convert':
						if ($this->parseExpression()) {
							$savePoint = $this->index;
							if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA) &&
								$this->parseCastDataType() &&
								$this->parseOptionalCharacterSet() &&
								$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)
							) {
								return true;
							}
							$this->index = $savePoint;
							$savePoint = $this->index;
							if ($this->isIdentifierWithValue($this->nextToken(), 'using') &&
								$this->parseTranscodingName() &&
								$this->parseOptionalCharacterSet() &&
								$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)
							) {
								return true;
							}
							$this->index = $savePoint;
						}
						break;

					case 'trim':
						if (!$this->isIdentifierWithValue($this->nextToken(), array(
							'leading',
							'both',
							'trailing',
						))) {
							$this->index--;
						}

						while ($this->parseExpression()) {
							$nextToken = $this->nextToken();
							if (
								$this->isTokenOfType($nextToken, wfWAFSQLiLexer::COMMA) ||
								$this->isIdentifierWithValue($nextToken, array(
									'from',
									'for',
									'in',
								))
							) {
								continue;
							}
							$this->index--;
							break;
						}

						if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)) {
							return true;
						}
						break;

					case 'weight_string':
						if ($this->parseExpression()) {
							$savePoint = $this->index;
							if (!(
								$this->isIdentifierWithValue($this->nextToken(), 'as') &&
								$this->parseCastDataType() &&
								$this->parseOptionalCharacterSet()
							)) {
								$this->index = $savePoint;
							}

							if ($this->isIdentifierWithValue($this->nextToken(), 'level')) {
								while ($this->parseExpression()) {
									$nextToken = $this->nextToken();
									if (
										$this->isTokenOfType($nextToken, wfWAFSQLiLexer::COMMA) ||
										$this->isTokenOfType($nextToken, wfWAFSQLiLexer::MINUS)
									) {
										continue;
									}
									$this->index--;
									break;
								}
								while ($this->isIdentifierWithValue($this->nextToken(), array(
									'asc',
									'desc',
									'reverse',
								))) {
									continue;
								}
								$this->index--;
							} else {
								$this->index--;
							}
						}

						if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)) {
							return true;
						}
						break;

					default:
						$savePoint = $this->index;
						if (in_array($functionToken->getUpperCaseValue(), $this->groupFunctions)) {
							$token = $this->nextToken();
							if (!$this->isIdentifierWithValue($token, array(
									'all', 'distinct',
								)) && !$this->isTokenOfType($token, wfWAFSQLiLexer::ASTERISK)
							) {
								$this->index--;
							}
							$this->parseBitExpression();
							if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)) {
								return true;
							}
						}
						$this->index = $savePoint;

						while ($this->parseExpression()) {
							$nextToken = $this->nextToken();
							if (
								$this->isTokenOfType($nextToken, wfWAFSQLiLexer::COMMA) ||
								$this->isIdentifierWithValue($nextToken, array(
									'from',
									'for',
									'in',
								))
							) {
								continue;
							}
							$this->index--;
							break;
						}

						if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)) {
							return true;
						}
						break;
				}
			}
		}
		$this->index = $startPoint;
		return false;
	}

	/**
	 * BINARY (INTEGER_NUM)?
	 * | CHAR (INTEGER_NUM)?
	 * | DATE_SYM
	 * | DATETIME
	 * | DECIMAL_SYM ( INTEGER_NUM (COMMA INTEGER_NUM)? )?
	 * | SIGNED_SYM (INTEGER_SYM)?
	 * | TIME_SYM
	 * | UNSIGNED_SYM (INTEGER_SYM)?
	 *
	 * @return bool
	 */
	private function parseCastDataType() {
		$startPoint = $this->index;
		$token = $this->nextToken();
		if ($this->isKeywordToken($token)) {
			switch ($token->getLowerCaseValue()) {
				case 'binary':
				case 'char':
				case 'nchar':
				case 'varchar':
				case 'character':
					$savePoint = $this->index;
					if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS) &&
						$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::INTEGER_LITERAL) &&
						$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)
					) {
						return true;
					}
					$this->index = $savePoint;
					return true;

				case 'date':
				case 'datetime':
				case 'time':
					return true;

				case 'signed':
				case 'unsigned':
					if (!$this->isIdentifierWithValue($this->nextToken(), array(
						'int',
						'integer',
					))) {
						$this->index--;
					}
					return true;

				case 'decimal':
					$savePoint = $this->index;
					if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS) &&
						$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::INTEGER_LITERAL)) {

						$savePoint2 = $this->index;
						if (!($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA) &&
							$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::INTEGER_LITERAL)
						)) {
							$this->index = $savePoint2;
						}
						if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)) {
							return true;
						}
					}

					$this->index = $savePoint;
					return true;
			}
		}
		$this->index = $startPoint;
		return false;
	}

	private function parseTranscodingName() {
		$savePoint = $this->index;
		$token = $this->nextToken();
		if ($token && $token->getType() === wfWAFSQLiLexer::UNQUOTED_IDENTIFIER) {
			return true;
		}
		$this->index = $savePoint;
		return false;
	}

	private function parseOptionalCharacterSet() {
		$savePoint = $this->index;
		if (!(
			$this->nextToken()->getLowerCaseValue() === 'character' &&
			$this->nextToken()->getLowerCaseValue() === 'set' &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::UNQUOTED_IDENTIFIER)
		)) {
			$this->index = $savePoint;
		}
		return true;
	}

	private function parseVariable() {
		$nextToken = $this->nextToken();
		if ($nextToken && $nextToken->getType() === wfWAFSQLiLexer::VARIABLE) {
			return true;
		}
		$this->index--;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseSubquery() {
		$startIndex = $this->index;
		if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS) &&
			$this->parseSelectStatement() &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)
		) {
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	private function testForSubquery() {
		$startIndex = $this->index;
		$nextToken = $this->nextToken();
		if ($nextToken && $nextToken->getType() === wfWAFSQLiLexer::OPEN_PARENTHESIS) {
			$selectToken = $this->nextToken();
			if ($this->isIdentifierWithValue($selectToken, 'select')) {
				$this->index = $startIndex;
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 *
	 *
	 * @return bool
	 */
	private function parseExistsSubquery() {
		$startIndex = $this->index;
		$existsToken = $this->nextToken();
		if ($this->isIdentifierWithValue($existsToken, 'exists')) {
			if ($this->parseSubquery()) {
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * MATCH (column_spec (COMMA column_spec)* ) AGAINST (expression (search_modifier)? )
	 *
	 * @return bool
	 */
	private function parseMatchAgainst() {
		$startIndex = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'match')) {
			$savePoint = $this->index;
			if (!$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS)) {
				$this->index = $savePoint;
			}
			$hasColumns = false;
			while ($this->parseColumnSpec()) {
				$hasColumns = true;
				$savePoint = $this->index;
				if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
					continue;
				}
				$this->index = $savePoint;
				break;
			}
			$savePoint = $this->index;
			if (!$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)) {
				$this->index = $savePoint;
			}
			if ($hasColumns && $this->isIdentifierWithValue($this->nextToken(), 'against') &&
				$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS) &&
				$this->parseExpression() &&
				($this->parseSearchModifier() || true) &&
				$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)
			) {
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * Used in match/against
	 *
	 * @link https://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html
	 * @return bool
	 */
	private function parseSearchModifier() {
		$startIndex = $this->index;

		$startToken = $this->nextToken();
		if ($this->isIdentifierWithValue($startToken, 'in')) {
			$next = $this->nextToken();
			if ($this->isIdentifierWithValue($next, 'natural') &&
				$this->isIdentifierWithValue($this->nextToken(), 'language') &&
				$this->isIdentifierWithValue($this->nextToken(), 'mode')
			) {
				$saveIndex = $this->index;
				if ($this->isIdentifierWithValue($this->nextToken(), 'with') &&
					$this->isIdentifierWithValue($this->nextToken(), 'query') &&
					$this->isIdentifierWithValue($this->nextToken(), 'expansion')
				) {
					return true;
				}
				$this->index = $saveIndex;
				return true;

			} else if ($this->isIdentifierWithValue($next, 'boolean') &&
				$this->isIdentifierWithValue($this->nextToken(), 'mode')
			) {
				return true;
			}
		} else if ($this->isIdentifierWithValue($startToken, 'with')) {
			if ($this->isIdentifierWithValue($this->nextToken(), 'query') &&
				$this->isIdentifierWithValue($this->nextToken(), 'expansion')
			) {
				return true;
			}
		}

		$this->index = $startIndex;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseCaseWhen() {
		$startIndex = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'case')) {
			$hasWhen = false;
			while (true) {
				if (!$this->isIdentifierWithValue($this->nextToken(), 'when')) {
					$this->index--;
					break;
				}
				if ($this->parseExpression()) {
					if ($this->isIdentifierWithValue($this->nextToken(), 'then') && $this->parseBitExpression()) {
						$hasWhen = true;
						continue;
					}
					$this->index--;
				}
				$this->index--;
				break;
			}
			if ($hasWhen) {
				$endToken = $this->nextToken();
				if ($this->isIdentifierWithValue($endToken, 'else')) {
					if (!$this->parseBitExpression()) {
						$this->index = $startIndex;
						return false;
					}
					$endToken = $this->nextToken();
				}
				if ($this->isIdentifierWithValue($endToken, 'end')) {
					return true;
				}
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseODBCExpression() {
		$startIndex = $this->index;
		if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_BRACKET) && $this->isIdentifier($this->nextToken()) && $this->parseExpression() && $this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_BRACKET)) {
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseIntervalExpression() {
		$startIndex = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'interval') && $this->parseExpression()) {
			$intervalUnitToken = $this->nextToken();
			if ($intervalUnitToken && in_array($intervalUnitToken->getType(), $this->intervalUnits)) {
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * @return bool
	 */
	public function parseCollationName() {
		$startIndex = $this->index;
		$token = $this->nextToken();
		if ($token && $token->getType() === wfWAFSQLiLexer::UNQUOTED_IDENTIFIER) {
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseFrom() {
		$startIndex = $this->index;
		$token = $this->nextToken();
		if ($this->isIdentifierWithValue($token, 'from')) {
			return $this->parseTableReferences();
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * @link http://dev.mysql.com/doc/refman/5.6/en/join.html
	 * @return bool
	 */
	private function parseTableReferences() {
		$startPoint = $this->index;
		$hasReferences = false;
		while ($this->parseEscapedTableReference()) {
			$hasReferences = true;
			$savePoint = $this->index;
			$token = $this->nextToken();
			if ($this->isTokenOfType($token, wfWAFSQLiLexer::COMMA)) {
				continue;
			}
			$this->index = $savePoint;
			break;
		}
		if ($hasReferences) {
			return true;
		}
		$this->index = $startPoint;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseEscapedTableReference() {
		$startPoint = $this->index;
		if ($this->parseTableReference() ||
			(
				$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_BRACKET) &&
				$this->isIdentifierWithValue($this->nextToken(), 'oj') &&
				$this->parseTableReference() &&
				$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_BRACKET)
			)
		) {
			return true;
		}

		$this->index = $startPoint;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseTableReference() {
		$savePoint = $this->index;
		$hasTables = false;
		if ($this->parseTableFactor()) {
			$hasTables = true;
			while ($this->parseJoinTable()) {

			}
		}
		if ($hasTables) {
			return true;
		}
		$this->index = $savePoint;
		return false;
	}

	/**
	 * table_factor:
	 *   tbl_name [PARTITION (partition_names)] [[AS] alias] [index_hint_list]
	 *   | table_subquery [AS] alias
	 *   | ( table_references )
	 */
	private function parseTableFactor() {
		$savePoint = $this->index;
		if ($this->parseTableSpec()) {
			$savePoint2 = $this->index;
			if (!$this->parsePartitionClause()) {
				$this->index = $savePoint2;
			}

			$this->parseAlias();
			$this->parseIndexHintList();

			return true;
		}
		$this->index = $savePoint;

		$savePoint = $this->index;
		if ($this->parseSubquery() && $this->parseAlias()) {
			return true;
		}
		$this->index = $savePoint;

		$savePoint = $this->index;
		if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS) &&
			$this->parseTableReferences() &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)
		) {
			return true;
		}
		$this->index = $savePoint;
		return false;
	}

	/**
	 * PARTITION (partition_names)
	 *
	 * @return bool
	 */
	private function parsePartitionClause() {
		$startIndex = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'partition') &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS) &&
			$this->parsePartitionNames() &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)
		) {
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parsePartitionNames() {
		$startPoint = $this->index;
		$hasPartition = false;
		while ($this->parsePartitionName()) {
			$hasPartition = true;
			$savePoint = $this->index;
			if (!$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
				$this->index = $savePoint;
				break;
			}
		}
		if ($hasPartition) {
			return true;
		}
		$this->index = $startPoint;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parsePartitionName() {
		$startPoint = $this->index;
		$token = $this->nextToken();
		if ($this->isValidNonReservedWordIdentifier($token)) {
			return true;
		}
		$this->index = $startPoint;
		return false;
	}

	/**
	 * join_table:
	 *   table_reference [INNER | CROSS] JOIN table_factor [join_condition]
	 *   | table_reference STRAIGHT_JOIN table_factor
	 *   | table_reference STRAIGHT_JOIN table_factor ON conditional_expr
	 *   | table_reference {LEFT|RIGHT} [OUTER] JOIN table_reference join_condition
	 *   | table_reference NATURAL [{LEFT|RIGHT} [OUTER]] JOIN table_factor
	 *
	 * @return bool
	 */
	private function parseJoinTable() {
		$savePoint = $this->index;
		if (!$this->isIdentifierWithValue($this->nextToken(), array(
			'inner', 'cross',
		))
		) {
			$this->index = $savePoint;
		}
		if ($this->isIdentifierWithValue($this->nextToken(), 'join') && $this->parseTableFactor()) {
			$this->parseJoinCondition();
			return true;
		}
		$this->index = $savePoint;

		$savePoint = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'straight_join') &&
			$this->parseTableFactor()
		) {
			$savePoint = $this->index;
			if (!($this->isIdentifierWithValue($this->nextToken(), 'on') && $this->parseExpression())) {
				$this->index = $savePoint;
			}
			return true;
		}
		$this->index = $savePoint;

		$savePoint = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), array(
			'left', 'right',
		))
		) {
			$savePoint2 = $this->index;
			if (!$this->isIdentifierWithValue($this->nextToken(), array(
				'outer',
			))
			) {
				$this->index = $savePoint2;
			}
		} else {
			$this->index = $savePoint;
		}
		if ($this->isIdentifierWithValue($this->nextToken(), 'join') &&
			$this->parseTableReference() &&
			$this->parseJoinCondition()
		) {
			return true;
		}
		$this->index = $savePoint;

		$savePoint = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), array(
			'natural',
		))
		) {
			if ($this->isIdentifierWithValue($this->nextToken(), array(
				'left', 'right',
			))
			) {
				$savePoint2 = $this->index;
				if (!$this->isIdentifierWithValue($this->nextToken(), array(
					'outer',
				))
				) {
					$this->index = $savePoint2;
				}
			} else {
				$this->index = $savePoint;
			}
			if ($this->isIdentifierWithValue($this->nextToken(), 'join') &&
				$this->parseTableFactor()
			) {
				return true;
			}

		}
		$this->index = $savePoint;
		return false;
	}

	/**
	 * (ON expression) | (USING_SYM column_list)
	 *
	 * @return bool
	 */
	private function parseJoinCondition() {
		$savePoint = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'on') && $this->parseExpression()) {
			return true;
		}
		$this->index = $savePoint;

		$savePoint = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'using') && $this->parseColumnList()) {
			return true;
		}
		$this->index = $savePoint;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseTableSpec() {
		$savePoint = $this->index;
		if ($this->isTokenOfType($this->nextToken(), array(
				wfWAFSQLiLexer::UNQUOTED_IDENTIFIER,
				wfWAFSQLiLexer::QUOTED_IDENTIFIER,
			)) &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::DOT) &&
			$this->isTokenOfType($this->nextToken(), array(
				wfWAFSQLiLexer::UNQUOTED_IDENTIFIER,
				wfWAFSQLiLexer::QUOTED_IDENTIFIER,
			))
		) {
			return true;
		}
		$this->index = $savePoint;
		if ($this->isValidNonReservedWordIdentifier($this->nextToken())) {
			return true;
		}
		$this->index = $savePoint;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseAlias() {
		$savePoint = $this->index;
		$token = $this->nextToken();
		if ($this->isIdentifierWithValue($token, 'as')) {
			$token = $this->nextToken();
		}
		if ($this->isValidNonReservedWordIdentifier($token)) {
			return true;
		}
		$this->index = $savePoint;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseIndexHintList() {
		$startPoint = $this->index;
		$hasHints = false;
		while ($this->parseIndexHint()) {
			$hasHints = true;
			$savePoint = $this->index;
			if (!$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
				$this->index = $savePoint;
				break;
			}
		}
		if ($hasHints) {
			return true;
		}
		$this->index = $startPoint;
		return false;
	}

	/**
	 * @return bool
	 */
	private function parseIndexHint() {
		// USE_SYM    index_options LPAREN (index_list)? RPAREN
		$savePoint = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'use') &&
			$this->parseIndexOptions() &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS)
		) {
			$this->parseIndexList();
			if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)) {
				return true;
			}
		}
		$this->index = $savePoint;

		// IGNORE_SYM index_options LPAREN index_list RPAREN
		$savePoint = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'ignore') &&
			$this->parseIndexOptions() &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS) &&
			$this->parseIndexList() &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)
		) {
			return true;
		}
		$this->index = $savePoint;

		// FORCE_SYM  index_options LPAREN index_list RPAREN
		$savePoint = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'force') &&
			$this->parseIndexOptions() &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS) &&
			$this->parseIndexList() &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)
		) {
			return true;
		}
		$this->index = $savePoint;

		return false;
	}

	/**
	 * (INDEX_SYM | KEY_SYM) (  FOR_SYM ((JOIN_SYM) | (ORDER_SYM BY_SYM) | (GROUP_SYM BY_SYM))  )?
	 *
	 * @return bool
	 */
	private function parseIndexOptions() {
		$savePoint = $this->index;
		$token = $this->nextToken();
		if ($this->isIdentifierWithValue($token, 'index') ||
			$this->isIdentifierWithValue($token, 'key')
		) {
			$savePoint = $this->index;
			if ($this->isIdentifierWithValue($this->nextToken(), 'for')) {

				$savePoint = $this->index;
				if ($this->isIdentifierWithValue($this->nextToken(), 'join')) {
					return true;
				}
				$this->index = $savePoint;

				$savePoint = $this->index;
				if ($this->isIdentifierWithValue($this->nextToken(), 'order') &&
					$this->isIdentifierWithValue($this->nextToken(), 'by')
				) {
					return true;
				}
				$this->index = $savePoint;

				$savePoint = $this->index;
				if ($this->isIdentifierWithValue($this->nextToken(), 'group') &&
					$this->isIdentifierWithValue($this->nextToken(), 'by')
				) {
					return true;
				}
				$this->index = $savePoint;

				return true;
			}
			$this->index = $savePoint;
			return true;
		}
		$this->index = $savePoint;
		return false;
	}

	private function parseIndexList() {
		$startPoint = $this->index;
		$hasIndex = false;
		while ($this->parseIndexName()) {
			$hasIndex = true;
			$savePoint = $this->index;
			if (!$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
				$this->index = $savePoint;
				break;
			}
		}
		if ($hasIndex) {
			return true;
		}
		$this->index = $startPoint;
		return false;
	}

	private function parseIndexName() {
		$startPoint = $this->index;
		$token = $this->nextToken();
		if ($this->isValidNonReservedWordIdentifier($token)) {
			return true;
		}
		$this->index = $startPoint;
		return false;
	}

	/**
	 * LPAREN column_spec (COMMA column_spec)* RPAREN
	 *
	 * @return bool
	 */
	private function parseColumnList() {
		$startPoint = $this->index;
		if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS)) {
			$hasColumn = false;
			while ($this->parseColumnSpec()) {
				$hasColumn = true;
				$savePoint = $this->index;
				if (!$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
					$this->index = $savePoint;
					break;
				}
			}
			if ($hasColumn && $this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)) {
				return true;
			}
		}
		$this->index = $startPoint;
		return false;
	}

	private function parseWhere() {
		$startIndex = $this->index;
		$token = $this->nextToken();
		if ($this->isIdentifierWithValue($token, 'where')) {
			if ($this->parseExpression()) {
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	private function parseProcedure() {
		$startIndex = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'PROCEDURE') &&
			$this->isIdentifierWithValue($this->nextToken(), 'ANALYSE') &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS)
		) {
			$savePoint = $this->index;
			if ($this->parseExpression()) {
				$savePoint = $this->index;
				if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA) &&
					$this->parseExpression() &&
					$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)
				) {
					return true;
				}
				$this->index = $savePoint;
				if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)) {
					return true;
				}
			}
			$this->index = $savePoint;
			if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)) {
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * GROUP_SYM BY_SYM groupby_item (COMMA groupby_item)* (WITH ROLLUP_SYM)?
	 *
	 * @return bool
	 */
	private function parseGroupBy() {
		$startIndex = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'group') &&
			$this->isIdentifierWithValue($this->nextToken(), 'by')
		) {
			$hasItems = false;
			while ($this->parseGroupByItem()) {
				$hasItems = true;
				if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
					continue;
				}
				$this->index--;
				break;
			}
			if ($hasItems) {
				$savePoint = $this->index;
				if (!($this->isIdentifierWithValue($this->nextToken(), 'with') &&
					$this->isIdentifierWithValue($this->nextToken(), 'rollup'))
				) {
					$this->index = $savePoint;
				}
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * column_spec | INTEGER_NUM | bit_expr ;
	 *
	 * @return bool
	 */
	private function parseGroupByItem() {
		$startIndex = $this->index;
		if ($this->parseBitExpression()) {
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * HAVING expression
	 *
	 * @return bool
	 */
	private function parseHaving() {
		$startIndex = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'having')
			&& $this->parseExpression()
		) {
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * ORDER_SYM BY_SYM orderby_item (COMMA orderby_item)*
	 *
	 * @return bool
	 */
	private function parseOrderBy() {
		$startIndex = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'order') &&
			$this->isIdentifierWithValue($this->nextToken(), 'by')
		) {
			$hasItems = false;
			while ($this->parseOrderByItem()) {
				$hasItems = true;
				if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
					continue;
				}
				$this->index--;
				break;
			}
			if ($hasItems) {
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * groupby_item (ASC | DESC)? ;
	 *
	 * @return bool
	 */
	private function parseOrderByItem() {
		$startIndex = $this->index;
		if ($this->parseGroupByItem()) {
			$savePoint = $this->index;
			if (!$this->isIdentifierWithValue($this->nextToken(), array(
				'asc', 'desc',
			))
			) {
				$this->index = $savePoint;
			}
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	private function parseLimit() {
		// LIMIT ((offset COMMA)? row_count) | (row_count OFFSET_SYM offset)
		$startIndex = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'limit') &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::INTEGER_LITERAL)
		) {
			$savePoint = $this->index;
			if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA) &&
				$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::INTEGER_LITERAL)
			) {
				return true;
			}
			$this->index = $savePoint;

			$savePoint = $this->index;
			if ($this->isIdentifierWithValue($this->nextToken(), 'offset') &&
				$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::INTEGER_LITERAL)
			) {
				return true;
			}
			$this->index = $savePoint;
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * @link http://dev.mysql.com/doc/refman/5.6/en/insert.html
	 * @return bool
	 */
	private function parseInsertStatement() {
		$startIndex = $this->index;
		if ($this->parseInsertStatement1() ||
			$this->parseInsertStatement2() ||
			$this->parseInsertStatement3()
		) {
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * insert_header
	 * (column_list)?
	 * value_list_clause
	 * ( insert_subfix )?
	 *
	 * @return bool
	 */
	private function parseInsertStatement1() {
		$startIndex = $this->index;
		if ($this->parseInsertHeader()) {
			$this->parseColumnList();
			if ($this->parseValueListClause()) {
				$this->parseInsertSubfix();
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * insert_header
	 * set_columns_cluase
	 * ( insert_subfix )?
	 *
	 * @return bool
	 */
	private function parseInsertStatement2() {
		$startIndex = $this->index;
		if ($this->parseInsertHeader() && $this->parseSetColumnsClause()) {
			$this->parseInsertSubfix();
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * insert_header
	 * (column_list)?
	 * select_expression
	 * ( insert_subfix )?
	 *
	 * @return bool
	 */
	private function parseInsertStatement3() {
		$startIndex = $this->index;
		if ($this->parseInsertHeader()) {
			$this->parseColumnList();
			if ($this->parseSelectStatement()) {
				$this->parseInsertSubfix();
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * INSERT (LOW_PRIORITY | HIGH_PRIORITY)? (IGNORE_SYM)?
	 *   (INTO)? table_spec
	 *   (partition_clause)?
	 *
	 * @return bool
	 */
	private function parseInsertHeader() {
		$startIndex = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'insert')) {
			$savePoint = $this->index;
			// (LOW_PRIORITY | HIGH_PRIORITY)?
			if (!$this->isIdentifierWithValue($this->nextToken(), array(
				'LOW_PRIORITY', 'HIGH_PRIORITY'
			))
			) {
				$this->index = $savePoint;
			}

			// (IGNORE_SYM)?
			$savePoint = $this->index;
			if (!$this->isIdentifierWithValue($this->nextToken(), array(
				'IGNORE'
			))
			) {
				$this->index = $savePoint;
			}

			// (INTO)?
			$savePoint = $this->index;
			if (!$this->isIdentifierWithValue($this->nextToken(), array(
				'into'
			))
			) {
				$this->index = $savePoint;
			}

			// table_spec
			if ($this->parseTableSpec()) {
				$savePoint = $this->index;
				// (partition_clause)?
				if (!$this->parsePartitionClause()) {
					$this->index = $savePoint;
				}
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * (VALUES | VALUE_SYM) column_value_list (COMMA column_value_list)*;
	 *
	 * @return bool
	 */
	private function parseValueListClause() {
		$startIndex = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), array(
			'value', 'values',
		))
		) {
			$hasValues = false;
			while ($this->parseColumnValueList()) {
				$hasValues = true;
				$savePoint = $this->index;
				if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
					$hasValues = false;
					continue;
				}
				$this->index = $savePoint;
				break;
			}
			if ($hasValues) {
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * LPAREN (bit_expr|DEFAULT) (COMMA (bit_expr|DEFAULT) )* RPAREN ;
	 *
	 * @return bool
	 */
	private function parseColumnValueList() {
		$startIndex = $this->index;
		if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::OPEN_PARENTHESIS)) {
			$hasValues = false;
			while (true) {
				$savePoint = $this->index;
				if (!$this->parseBitExpression() && !$this->isIdentifierWithValue($this->nextToken(), 'DEFAULT')) {
					$this->index = $savePoint;
					break;
				}
				$hasValues = true;
				$savePoint = $this->index;
				if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
					$hasValues = false;
					continue;
				}
				$this->index = $savePoint;
				break;
			}
			if ($hasValues && $this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::CLOSE_PARENTHESIS)) {
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	private function parseInsertSubfix() {
		// ON DUPLICATE_SYM KEY_SYM UPDATE column_spec EQ_SYM expression (COMMA column_spec EQ_SYM expression)*
		$startIndex = $this->index;
		if (
			$this->isIdentifierWithValue($this->nextToken(), 'on') &&
			$this->isIdentifierWithValue($this->nextToken(), 'duplicate') &&
			$this->isIdentifierWithValue($this->nextToken(), 'key') &&
			$this->isIdentifierWithValue($this->nextToken(), 'update')
		) {
			$hasValues = false;
			while (true) {
				$savePoint = $this->index;
				if ($this->parseColumnSpec() &&
					$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::EQUALS_SYMBOL) &&
					$this->parseExpression()
				) {
					$hasValues = true;
					$savePoint = $this->index;
					if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
						$hasValues = false;
						continue;
					}
					$this->index = $savePoint;
					break;
				}
				$this->index = $savePoint;
				break;
			}
			if ($hasValues) {
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * SET_SYM set_column_cluase ( COMMA set_column_cluase )*;
	 *
	 * @return bool
	 */
	private function parseSetColumnsClause() {
		$startIndex = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'set')) {
			$hasValues = true;
			while ($this->parseSetColumnClause()) {
				$hasValues = true;
				$savePoint = $this->index;
				if ($this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::COMMA)) {
					$hasValues = false;
					continue;
				}
				$this->index = $savePoint;
				break;
			}
			if ($hasValues) {
				return true;
			}
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * column_spec EQ_SYM (expression|DEFAULT) ;
	 *
	 * @return bool
	 */
	private function parseSetColumnClause() {
		$startIndex = $this->index;
		if ($this->parseColumnSpec() &&
			$this->isTokenOfType($this->nextToken(), wfWAFSQLiLexer::EQUALS_SYMBOL) &&
			($this->parseExpression() || $this->isIdentifierWithValue($this->nextToken(), 'default'))
		) {
			return true;
		}
		$this->index = $startIndex;
		return false;
	}

	/**
	 * UPDATE (LOW_PRIORITY)? (IGNORE_SYM)? table_reference
	 *   set_columns_cluase
	 *   (where_clause)?
	 *   (orderby_clause)?
	 *   (limit_clause)?
	 * |
	 * UPDATE (LOW_PRIORITY)? (IGNORE_SYM)? table_references
	 *   set_columns_cluase
	 *   (where_clause)?
	 *
	 * @return bool
	 */
	private function parseUpdateStatement() {
		$startIndex = $this->index;
		if ($this->isIdentifierWithValue($this->nextToken(), 'update')) {
			$savePoint = $this->index;
			if (!$this->isIdentifierWithValue($this->nextToken(), 'LOW_PRIORITY')) {
				$this->index = $savePoint;
			}

			$savePoint = $this->index;
			if (!$this->isIdentifierWithValue($this->nextToken(), 'ignore')) {
				$this->index = $savePoint;
			}

			if ($this->parseTableReferences() && $this->parseSetColumnsClause()) {
				$this->parseWhere();
				$this->parseOrderBy();
				$this->parseLimit();
				return true;
			}
		}
		$this->index = $startIndex;
		return false;

	}

	/**
	 * @param wfWAFLexerToken $token
	 * @return bool
	 */
	private function isIdentifier($token) {
		return $token && ($token->getType() === wfWAFSQLiLexer::QUOTED_IDENTIFIER || $token->getType() === wfWAFSQLiLexer::UNQUOTED_IDENTIFIER);
	}

	/**
	 * @param wfWAFLexerToken $token
	 * @param string|array $value
	 * @return bool
	 */
	private function isIdentifierWithValue($token, $value) {
		return $token && $token->getType() === wfWAFSQLiLexer::UNQUOTED_IDENTIFIER &&
		(is_array($value) ? in_array($token->getLowerCaseValue(), array_map('wfWAFUtils::strtolower', $value)) :
			$token->getLowerCaseValue() === wfWAFUtils::strtolower($value));
	}

	/**
	 * @param wfWAFLexerToken $token
	 * @param mixed $type
	 * @return bool
	 */
	protected function isTokenOfType($token, $type) {
		if (is_array($type)) {
			return $token && in_array($token->getType(), $type);
		}
		return $token && $token->getType() === $type;
	}

	/**
	 * @param wfWAFLexerToken $token
	 * @return bool
	 */
	private function isNotSymbolToken($token) {
		return $token &&
		(
			($token->getType() === wfWAFSQLiLexer::UNQUOTED_IDENTIFIER && $token->getLowerCaseValue() === 'not') ||
			($token->getType() === wfWAFSQLiLexer::EXPR_NOT)
		);
	}

	/**
	 * @param wfWAFLexerToken $token
	 * @return bool
	 */
	private function isKeywordToken($token) {
		return $token && $token->getType() === wfWAFSQLiLexer::UNQUOTED_IDENTIFIER &&
		in_array($token->getUpperCaseValue(), $this->keywords);
	}

	/**
	 * @param wfWAFLexerToken $token
	 * @return bool
	 */
	private function isReservedWordToken($token) {
		return $token && $token->getType() === wfWAFSQLiLexer::UNQUOTED_IDENTIFIER &&
		in_array($token->getUpperCaseValue(), $this->reservedWords);
	}

	/**
	 * @param wfWAFLexerToken $token
	 * @return bool
	 */
	private function isValidNonKeywordIdentifier($token) {
		return $token && (
			$token->getType() === wfWAFSQLiLexer::QUOTED_IDENTIFIER ||
			($token->getType() === wfWAFSQLiLexer::UNQUOTED_IDENTIFIER && !$this->isKeywordToken($token))
		);
	}

	/**
	 * @param wfWAFLexerToken $token
	 * @return bool
	 */
	private function isValidNonReservedWordIdentifier($token) {
		return $token && (
			$token->getType() === wfWAFSQLiLexer::QUOTED_IDENTIFIER ||
			($token->getType() === wfWAFSQLiLexer::UNQUOTED_IDENTIFIER && !$this->isReservedWordToken($token))
		);
	}

	/**
	 * @param wfWAFLexerToken $token
	 * @return bool
	 */
	private function isOrToken($token) {
		return $token && ($this->isIdentifierWithValue($token, 'or') || $this->isTokenOfType($token, wfWAFSQLiLexer::EXPR_OR));
	}

	/**
	 * @param wfWAFLexerToken $token
	 * @return bool
	 */
	private function isAndToken($token) {
		return $token && ($this->isIdentifierWithValue($token, 'and') || $this->isTokenOfType($token, wfWAFSQLiLexer::EXPR_AND));
	}

	/**
	 * @return string
	 */
	public function getSubject() {
		return $this->subject;
	}

	/**
	 * @param string $subject
	 */
	public function setSubject($subject) {
		$this->subject = $subject;
		$this->setTokens(array());
		$this->lexer->setSQL($this->subject);
	}

	/**
	 * @return int
	 */
	public function getFlags() {
		return $this->flags;
	}

	/**
	 * @param int $flags
	 */
	public function setFlags($flags) {
		$this->flags = $flags;
		$this->lexer->setFlags($this->flags);
	}
}


class wfWAFSQLiLexer implements wfWAFLexerInterface {

	const FLAG_TOKENIZE_MYSQL_PORTABLE_COMMENTS = 0x1;

	const UNQUOTED_IDENTIFIER = 'UNQUOTED_IDENTIFIER';
	const VARIABLE = 'VARIABLE';
	const QUOTED_IDENTIFIER = 'QUOTED_IDENTIFIER';
	const DOUBLE_STRING_LITERAL = 'DOUBLE_STRING_LITERAL';
	const SINGLE_STRING_LITERAL = 'SINGLE_STRING_LITERAL';
	const INTEGER_LITERAL = 'INTEGER_LITERAL';
	const REAL_NUMBER_LITERAL = 'REAL_NUMBER_LITERAL';
	const BINARY_NUMBER_LITERAL = 'BINARY_NUMBER_LITERAL';
	const HEX_NUMBER_LITERAL = 'HEX_NUMBER_LITERAL';
	const DOT = 'DOT';
	const OPEN_PARENTHESIS = 'OPEN_PARENTHESIS';
	const CLOSE_PARENTHESIS = 'CLOSE_PARENTHESIS';
	const OPEN_BRACKET = 'OPEN_BRACKET';
	const CLOSE_BRACKET = 'CLOSE_BRACKET';
	const COMMA = 'COMMA';
	const EXPR_OR = 'EXPR_OR';
	const EXPR_AND = 'EXPR_AND';
	const EXPR_NOT = 'EXPR_NOT';
	const BIT_AND = 'BIT_AND';
	const BIT_LEFT_SHIFT = 'BIT_LEFT_SHIFT';
	const BIT_RIGHT_SHIFT = 'BIT_RIGHT_SHIFT';
	const BIT_XOR = 'BIT_XOR';
	const BIT_INVERSION = 'BIT_INVERSION';
	const BIT_OR = 'BIT_OR';
	const PLUS = 'PLUS';
	const MINUS = 'MINUS';
	const ASTERISK = 'ASTERISK';
	const DIVISION = 'DIVISION';
	const MOD = 'MOD';
	const ARROW = 'ARROW';
	const EQUALS_SYMBOL = 'EQUALS_SYMBOL';
	const NOT_EQUALS = 'NOT_EQUALS';
	const LESS_THAN = 'LESS_THAN';
	const GREATER_THAN = 'GREATER_THAN';
	const LESS_THAN_EQUAL_TO = 'LESS_THAN_EQUAL_TO';
	const GREATER_THAN_EQUAL_TO = 'GREATER_THAN_EQUAL_TO';
	const SET_VAR = 'SET_VAR';
	const RIGHT_BRACKET = 'RIGHT_BRACKET';
	const LEFT_BRACKET = 'LEFT_BRACKET';
	const SEMICOLON = 'SEMICOLON';
	const COLON = 'COLON';
	const MYSQL_PORTABLE_COMMENT_START = 'MYSQL_PORTABLE_COMMENT_START';
	const MYSQL_PORTABLE_COMMENT_END = 'MYSQL_PORTABLE_COMMENT_END';
	const SINGLE_LINE_COMMENT = 'SINGLE_LINE_COMMENT';
	const MULTI_LINE_COMMENT = 'MULTI_LINE_COMMENT';
	/**
	 * @var int
	 */
	private $flags;
	private $tokenMatchers;
	private $hasPortableCommentStart = false;

	public static function getLexerTokenMatchers() {
		static $tokenMatchers;
		if ($tokenMatchers === null) {
			$tokenMatchers = array(
				new wfWAFLexerTokenMatcher(self::REAL_NUMBER_LITERAL, '/^(?:[0-9]+\\.[0-9]+|[0-9]+\\.|\\.[0-9]+|[Ee][\\+\\-][0-9]+)/'),
				new wfWAFLexerTokenMatcher(self::BINARY_NUMBER_LITERAL, '/^(?:0b[01]+|[bB]\'[01]+\')/', true),
				new wfWAFLexerTokenMatcher(self::HEX_NUMBER_LITERAL, '/^(?:0x[0-9a-fA-F]+|[xX]\'[0-9a-fA-F]+\')/', true),
				new wfWAFLexerTokenMatcher(self::INTEGER_LITERAL, '/^[0-9]+/', true),
				new wfWAFLexerTokenMatcher(self::VARIABLE, '/^(?:@(?:`(?:[^`]*(?:``[^`]*)*)`|
"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|
\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'|
[a-zA-Z_\\$\\.]+|
@[a-zA-Z_\\$][a-zA-Z_\\$0-9]{0,256}){0,1})
/Asx'),
				new wfWAFLexerTokenMatcher(self::QUOTED_IDENTIFIER, '/^`(?:[^`]*(?:``[^`]*)*)`/As'),
				new wfWAFLexerTokenMatcher(self::DOUBLE_STRING_LITERAL, '/^(?:[nN]|_[0-9a-zA-Z\\$_]{0,256})?"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"/As'),
				new wfWAFLexerTokenMatcher(self::SINGLE_STRING_LITERAL, '/^(?:[nN]|_[0-9a-zA-Z\\$_]{0,256})?\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As'),
				// U+0080 .. U+FFFF
				new wfWAFLexerTokenMatcher(self::UNQUOTED_IDENTIFIER, '/^[0-9a-zA-Z\\$_\\x{0080}-\\x{FFFF}]{1,256}/u'),
				new wfWAFLexerTokenMatcher(self::MYSQL_PORTABLE_COMMENT_START, '/^\\/\\*\\![0-9]{0,5}/s'),
				new wfWAFLexerTokenMatcher(self::MYSQL_PORTABLE_COMMENT_END, '/^\\*\\//s'),
				new wfWAFLexerTokenMatcher(self::SINGLE_LINE_COMMENT, '/^(?:#[^\n]*|--(?:[ \t\r][^\n]*|[\n]))/'),
				new wfWAFLexerTokenMatcher(self::MULTI_LINE_COMMENT, '/^\\/\\*.*?\\*\\//s'),
				new wfWAFLexerTokenMatcher(self::DOT, '/^\\./'),
				new wfWAFLexerTokenMatcher(self::OPEN_PARENTHESIS, '/^\\(/'),
				new wfWAFLexerTokenMatcher(self::CLOSE_PARENTHESIS, '/^\\)/'),
				new wfWAFLexerTokenMatcher(self::OPEN_BRACKET, '/^\\{/'),
				new wfWAFLexerTokenMatcher(self::CLOSE_BRACKET, '/^\\}/'),
				new wfWAFLexerTokenMatcher(self::COMMA, '/^,/'),
				new wfWAFLexerTokenMatcher(self::EXPR_OR, '/^\\|\\|/'),
				new wfWAFLexerTokenMatcher(self::EXPR_AND, '/^&&/'),
				new wfWAFLexerTokenMatcher(self::BIT_LEFT_SHIFT, '/^\\<\\</'),
				new wfWAFLexerTokenMatcher(self::BIT_RIGHT_SHIFT, '/^\\>\\>/'),
				new wfWAFLexerTokenMatcher(self::EQUALS_SYMBOL, '/^(?:\\=|\\<\\=\\>)/'),
				new wfWAFLexerTokenMatcher(self::ARROW, '/^\\=\\>/'),
				new wfWAFLexerTokenMatcher(self::LESS_THAN_EQUAL_TO, '/^\\<\\=/'),
				new wfWAFLexerTokenMatcher(self::GREATER_THAN_EQUAL_TO, '/^\\>\\=/'),
				new wfWAFLexerTokenMatcher(self::NOT_EQUALS, '/^(?:\\<\\>|(?:\\!|\\~|\\^)\\=)/'),
				new wfWAFLexerTokenMatcher(self::LESS_THAN, '/^\\</'),
				new wfWAFLexerTokenMatcher(self::GREATER_THAN, '/^\\>/'),
				new wfWAFLexerTokenMatcher(self::SET_VAR, '/^:\\=/'),
				new wfWAFLexerTokenMatcher(self::BIT_XOR, '/^\\^/'),
				new wfWAFLexerTokenMatcher(self::BIT_INVERSION, '/^\\~/'),
				new wfWAFLexerTokenMatcher(self::BIT_OR, '/^\\|/'),
				new wfWAFLexerTokenMatcher(self::PLUS, '/^\\+/'),
				new wfWAFLexerTokenMatcher(self::MINUS, '/^\\-/'),
				new wfWAFLexerTokenMatcher(self::ASTERISK, '/^\\*/'),
				new wfWAFLexerTokenMatcher(self::DIVISION, '/^\\//'),
				new wfWAFLexerTokenMatcher(self::MOD, '/^%/'),
				new wfWAFLexerTokenMatcher(self::EXPR_NOT, '/^\\!/'),
				new wfWAFLexerTokenMatcher(self::BIT_AND, '/^&/'),
				new wfWAFLexerTokenMatcher(self::RIGHT_BRACKET, '/^\\]/'),
				new wfWAFLexerTokenMatcher(self::LEFT_BRACKET, '/^\\[/'),
				new wfWAFLexerTokenMatcher(self::SEMICOLON, '/^;/'),
				new wfWAFLexerTokenMatcher(self::COLON, '/^:/'),
			);
		}
		return $tokenMatchers;
	}

	/**
	 * @var string
	 */
	private $sql;

	/**
	 * @var wfWAFStringScanner
	 */
	private $scanner;

	/**
	 * wfWAFRuleLexer constructor.
	 * @param $sql
	 * @param int $flags
	 */
	public function __construct($sql = null, $flags = 0) {
		$this->scanner = new wfWAFStringScanner();
		$this->tokenMatchers = self::getLexerTokenMatchers();
		$this->setSQL($sql);
		$this->setFlags($flags);
	}

	/**
	 * @return array
	 * @throws wfWAFParserSyntaxError
	 */
	public function tokenize() {
		$tokens = array();
		while ($token = $this->nextToken()) {
			$tokens[] = $token;
		}
		return $tokens;
	}

	/**
	 * @return bool|wfWAFLexerToken
	 * @throws wfWAFParserSyntaxError
	 */
	public function nextToken() {
		if (!$this->scanner->eos()) {
			/** @var wfWAFLexerTokenMatcher $tokenMatcher */
			foreach ($this->tokenMatchers as $tokenMatcher) {
				$this->scanner->skip('/^\s+/s');
				if ($this->scanner->eos()) {
					return false;
				}

				if (($this->flags & self::FLAG_TOKENIZE_MYSQL_PORTABLE_COMMENTS) === 0 &&
					($tokenMatcher->getTokenID() === self::MYSQL_PORTABLE_COMMENT_START ||
						$tokenMatcher->getTokenID() === self::MYSQL_PORTABLE_COMMENT_END)
				) {
					continue;
				}
				if (!$this->hasPortableCommentStart && $tokenMatcher->getTokenID() === self::MYSQL_PORTABLE_COMMENT_END) {
					continue;
				}

				if ($tokenMatcher->useMaximalMunch() && ($match = $this->scanner->check($tokenMatcher->getMatch())) !== null) {
					$biggestToken = $this->createToken($tokenMatcher->getTokenID(), $match);
					/** @var wfWAFLexerTokenMatcher $tokenMatcher2 */
					foreach ($this->tokenMatchers as $tokenMatcher2) {
						if ($tokenMatcher === $tokenMatcher2) {
							continue;
						}
						if (($match2 = $this->scanner->check($tokenMatcher2->getMatch())) !== null) {
							$biggestToken2 = $this->createToken($tokenMatcher2->getTokenID(), $match2);
							if (wfWAFUtils::strlen($biggestToken2->getValue()) > wfWAFUtils::strlen($biggestToken->getValue())) {
								$biggestToken = $biggestToken2;
							}
						}
					}
					$this->scanner->advancePointer(wfWAFUtils::strlen($biggestToken->getValue()));
					return $biggestToken;

				} else if (($match = $this->scanner->scan($tokenMatcher->getMatch())) !== null) {
					$token = $this->createToken($tokenMatcher->getTokenID(), $match);
					if ($tokenMatcher->getTokenID() === self::MYSQL_PORTABLE_COMMENT_START) {
						$this->hasPortableCommentStart = true;
					} else if ($tokenMatcher->getTokenID() === self::MYSQL_PORTABLE_COMMENT_END) {
						$this->hasPortableCommentStart = false;
					}
					return $token;
				}
			}
			$char = $this->scanner->scanChar();
			$e = new wfWAFParserSyntaxError(sprintf('Invalid character "%s" (\x%02x) found on line %d, column %d',
				$char, ord($char), $this->scanner->getLine(), $this->scanner->getColumn()));
			$e->setParseLine($this->scanner->getLine());
			$e->setParseColumn($this->scanner->getColumn());
			throw $e;
		}
		return false;
	}

	public function hasMoreTokens() {

	}

	/**
	 * @param $type
	 * @param $value
	 * @return wfWAFLexerToken
	 */
	protected function createToken($type, $value) {
		return new wfWAFLexerToken($type, $value, $this->scanner->getLine(), $this->scanner->getColumn());
	}

	/**
	 * @return string
	 */
	public function getSQL() {
		return $this->sql;
	}

	/**
	 * @param string $sql
	 */
	public function setSQL($sql) {
		if (is_string($sql)) {
			$this->scanner->setString($sql);
		}
		$this->sql = $sql;
	}

	/**
	 * @return int
	 */
	public function getFlags() {
		return $this->flags;
	}

	/**
	 * @param int $flags
	 */
	public function setFlags($flags) {
		$this->flags = $flags;
	}
}

class wfWAFLexerTokenMatcher {
	/**
	 * @var mixed
	 */
	private $tokenID;
	/**
	 * @var string
	 */
	private $match;
	/**
	 * @var bool
	 */
	private $useMaximalMunch;


	/**
	 * @param mixed $tokenID
	 * @param string $match
	 * @param bool $useMaximalMunch
	 */
	public function __construct($tokenID, $match, $useMaximalMunch = false) {
		$this->tokenID = $tokenID;
		$this->match = $match;
		$this->useMaximalMunch = $useMaximalMunch;
	}

	/**
	 * @return bool
	 */
	public function useMaximalMunch() {
		return $this->useMaximalMunch;
	}

	/**
	 * @return mixed
	 */
	public function getTokenID() {
		return $this->tokenID;
	}

	/**
	 * @param mixed $tokenID
	 */
	public function setTokenID($tokenID) {
		$this->tokenID = $tokenID;
	}

	/**
	 * @return string
	 */
	public function getMatch() {
		return $this->match;
	}

	/**
	 * @param string $match
	 */
	public function setMatch($match) {
		$this->match = $match;
	}
}
}
