// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.

namespace System.Data.Entity.Core.Common.EntitySql
{
    using System.Collections.Generic;
    using System.Data.Entity.Core.Common.EntitySql.AST;
    using System.Data.Entity.Resources;
    using System.Data.Entity.Utilities;
    using System.Diagnostics;
    using System.IO;
    using System.Text.RegularExpressions;

    // <summary>
    // Represents Cql scanner and helper functions.
    // </summary>
    internal sealed partial class CqlLexer
    {
        private static readonly StringComparer _stringComparer = StringComparer.OrdinalIgnoreCase;
        private static Dictionary<string, short> _keywords;
        private static HashSet<string> _invalidAliasNames;
        private static HashSet<string> _invalidInlineFunctionNames;
        private static Dictionary<string, short> _operators;
        private static Dictionary<string, short> _punctuators;
        private static HashSet<string> _canonicalFunctionNames;
        private static Regex _reDateTimeValue;
        private static Regex _reTimeValue;
        private static Regex _reDateTimeOffsetValue;

        private const string _datetimeValueRegularExpression =
            @"^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}([ ])+[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2}(\.[0-9]{1,7})?)?$";

        private const string _timeValueRegularExpression = @"^[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2}(\.[0-9]{1,7})?)?$";

        private const string _datetimeOffsetValueRegularExpression =
            @"^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}([ ])+[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2}(\.[0-9]{1,7})?)?([ ])*[\+-][0-9]{1,2}:[0-9]{1,2}$";

        private int _iPos;
        private int _lineNumber;
        private ParserOptions _parserOptions;
        private readonly string _query;

        // <summary>
        // set for DOT expressions
        // </summary>
        private bool _symbolAsIdentifierState;

        // <summary>
        // set for AS expressions
        // </summary>
        private bool _symbolAsAliasIdentifierState;

        // <summary>
        // set for function definitions
        // </summary>
        private bool _symbolAsInlineFunctionNameState;

        // Defines the set of characters to be interpreted as mandatory line breaks
        // according to UNICODE 5.0, section 5.8 Newline Guidelines.These are 'mandatory'
        // line breaks. We do not handle other 'line breaking opportunities'as defined by
        // UNICODE 5.0 since they are intended for presentation. The mandatory line break
        // defines breaking opportunities that must not be ignored. For all practical purposes
        // the interpretation of mandatory breaks determines the end of one line and consequently
        // the start of the next line of query text.
        // NOTE that CR and CRLF is treated as a composite 'character' and was obviously and intentionaly
        // omitted in the character set bellow.
        private static readonly Char[] _newLineCharacters =
            {
                '\u000A', // LF - line feed
                '\u0085', // NEL - next line
                '\u000B', // VT - vertical tab
                '\u2028', // LS - line separator
                '\u2029' // PS - paragraph separator
            };

        // <summary>
        // Intializes scanner
        // </summary>
        // <param name="query"> input query </param>
        // <param name="parserOptions"> parser options </param>
        internal CqlLexer(string query, ParserOptions parserOptions)
            : this()
        {
            DebugCheck.NotNull(query);
            DebugCheck.NotNull(parserOptions);

            _query = query;
            _parserOptions = parserOptions;
            yy_reader = new StringReader(_query);
        }

        // <summary>
        // Creates a new token.
        // </summary>
        // <param name="tokenId"> tokenid </param>
        // <param name="tokenvalue"> ast node </param>
        internal static Token NewToken(short tokenId, Node tokenvalue)
        {
            return new Token(tokenId, tokenvalue);
        }

        // <summary>
        // Creates a new token representing a terminal.
        // </summary>
        // <param name="tokenId"> tokenid </param>
        // <param name="termToken"> lexical value </param>
        internal static Token NewToken(short tokenId, TerminalToken termToken)
        {
            return new Token(tokenId, termToken);
        }

        // <summary>
        // Represents a token to be used in parser stack.
        // </summary>
        internal class Token
        {
            private readonly short _tokenId;
            private readonly object _tokenValue;

            internal Token(short tokenId, Node tokenValue)
            {
                _tokenId = tokenId;
                _tokenValue = tokenValue;
            }

            internal Token(short tokenId, TerminalToken terminal)
            {
                _tokenId = tokenId;
                _tokenValue = terminal;
            }

            internal short TokenId
            {
                get { return _tokenId; }
            }

            internal object Value
            {
                get { return _tokenValue; }
            }
        }

        // <summary>
        // Represents a terminal token
        // </summary>
        internal class TerminalToken
        {
            private readonly string _token;
            private readonly int _iPos;

            internal TerminalToken(string token, int iPos)
            {
                _token = token;
                _iPos = iPos;
            }

            internal int IPos
            {
                get { return _iPos; }
            }

            internal string Token
            {
                get { return _token; }
            }
        }

        internal static class yy_translate
        {
            internal static char translate(char c)

            #region TRANSLATE

            {
                if (Char.IsWhiteSpace(c)
                    || Char.IsControl(c))
                {
                    if (IsNewLine(c))
                    {
                        return '\n';
                    }
                    return ' ';
                }

                if (c < 0x007F)
                {
                    return c;
                }

                if (Char.IsLetter(c)
                    || Char.IsSymbol(c)
                    || Char.IsNumber(c))
                {
                    return 'a';
                }

                //
                // otherwise pass dummy 'marker' char so as we can continue 'extracting' tokens.
                // 
                return '`';
            }

            #endregion
        }

        // <summary>
        // Returns current lexeme
        // </summary>
        internal string YYText
        {
            get { return yytext(); }
        }

        // <summary>
        // Returns current input position
        // </summary>
        internal int IPos
        {
            get { return _iPos; }
        }

        // <summary>
        // Advances input position.
        // </summary>
        // <returns> updated input position </returns>
        internal int AdvanceIPos()
        {
            _iPos += YYText.Length;
            return _iPos;
        }

        // <summary>
        // returns true if given term is a eSQL keyword
        // </summary>
        internal static bool IsReservedKeyword(string term)
        {
            return InternalKeywordDictionary.ContainsKey(term);
        }

        // <summary>
        // Map lexical symbol to a keyword or an identifier.
        // </summary>
        // <param name="symbol"> lexeme </param>
        // <returns> Token </returns>
        internal Token MapIdentifierOrKeyword(string symbol)
        {
            /*
            The purpose of this method is to separate symbols into keywords and identifiers.
            This separation then leads parser into applying different productions 
            to the same eSQL expression. For example if 'key' symbol is mapped to a keyword then
            the expression 'KEY(x)' will satisfy 'keyExpr ::= KEY parenExpr', else if 'key' is mapped
            to an identifier then the expression satisfies
            'methodExpr :: = identifier L_PAREN optAllOrDistinct exprList R_PAREN optWithRelationship'

            Escaped symbols are always assumed to be identifiers.

            For unescaped symbols the naive implementation would check the symbol against 
            the collection of keywords and map the symbol to a keyword in case of match, 
            otherwise map to an identifier.
            This would result in a strong restriction on unescaped identifiers - they must not
            match keywords.

            In the long run this strategy has a potential of invalidating user queries with addition 
            of new keywords to the language. This is an undesired effect and the current implementation
            tries to mitigate it.

            The general mitigation pattern is to separate the collection of keywords and the collection of
            invalid aliases (identifiers), making invalid identifiers a subset of keywords.
            This allows in certain language constructs using unescaped references 'common' identifiers 
            that may be defined in the query or in the model (such as Key in Customer.Key). 
            Although it adds usability for common cases, it does not solve the general problem:
            select c.id as Key from Customers as c -- works
            select Key from (select c.id from Customers as c) as Key -- does not work for the first occurence of Key
                                                                     -- it is mapped to a keyword which results in 
                                                                     -- invalid syntax
            select [Key] from (select c.id from Customers as c) as Key -- works again

            The first two major places in syntax where restrictions are relaxed:
            1. DOT expressions where a symbol before DOT or after DOT is expected to be an identifier.
            2. AS expressions where a symbol after AS is expected to be an identifier.
            In both places identifiers are checked against the invalid aliases collection instead of
            the keywords collection. If an unescaped identifier appears outside of these two places 
            (like the Key in the second query above) it must be escaped or it must not match a keyword.

            The third special case is related to method expressions (function calls). Normally method identifier
            in a method expression must not match a keyword or must be escaped, except the two cases: LEFT and RIGHT.
            LEFT and RIGHT are canonical functions and their usage in a method expression is not ambiguos with 
            LEFT OUTER JOIN and RIGHT OUT JOIN constructs.
            Note that if method identifier is a DOT expression (multipart identifier) such as 'MyNameSpace.Key.Ref(x)' 
            then every part of the identifier follows the relaxed check described for DOT expressions (see above).
            This would help with LEFT and RIGHT functions, 'Edm.Left(x)' would work without the third specialcase,
            but most common use of these function is likely to be without 'Edm.'

            The fourth special case is function names in query inline definition section. These names are checked
            against both
            - the invalid aliases collection and
            - the collection invalid inline function names.
            The second collection contains certain keywords that are not in the first collection and that may be followed
            by the L_PAREN, which makes them look like method expression. The reason for this stronger restriction is to
            disallow the following kind of ambiguos queries:
            Function Key(c Customer) AS (Key(c))
            select Key(cust) from Customsers as cust
            */

            Token token;

            // Handle the escaped identifiers coming from HandleEscapedIdentifiers()
            if (IsEscapedIdentifier(symbol, out token))
            {
                Debug.Assert(token != null, "IsEscapedIdentifier must not return null token");
                return token;
            }

            // Handle keywords
            if (IsKeyword(symbol, out token))
            {
                Debug.Assert(token != null, "IsKeyword must not return null token");
                return token;
            }

            // Handle unescaped identifiers
            return MapUnescapedIdentifier(symbol);
        }

        #region MapIdentifierOrKeyword implementation details

        private bool IsEscapedIdentifier(string symbol, out Token identifierToken)
        {
            if (symbol.Length > 1
                && symbol[0] == '[')
            {
                if (symbol[symbol.Length - 1] == ']')
                {
                    var name = symbol.Substring(1, symbol.Length - 2);
                    var id = new Identifier(name, true, _query, _iPos);
                    id.ErrCtx.ErrorContextInfo = EntityRes.CtxEscapedIdentifier;
                    identifierToken = NewToken(CqlParser.ESCAPED_IDENTIFIER, id);
                    return true;
                }
                else
                {
                    var errorMessage = Strings.InvalidEscapedIdentifier(symbol);
                    throw EntitySqlException.Create(_query, errorMessage, _iPos, null, false, null);
                }
            }
            else
            {
                identifierToken = null;
                return false;
            }
        }

        private bool IsKeyword(string symbol, out Token terminalToken)
        {
            var lookAheadChar = GetLookAheadChar();

            if (!IsInSymbolAsIdentifierState(lookAheadChar)
                &&
                !IsCanonicalFunctionCall(symbol, lookAheadChar)
                &&
                InternalKeywordDictionary.ContainsKey(symbol))
            {
                ResetSymbolAsIdentifierState(true);

                var keywordID = InternalKeywordDictionary[symbol];

                if (keywordID == CqlParser.AS)
                {
                    // Treat the symbol following AS keyword as an identifier.
                    // Note that this state will be turned off by a punctuator, so in case of function definitions:
                    // FUNCTION identifier(...) AS (generalExpr) 
                    // the generalExpr will not be affected by the state.
                    _symbolAsAliasIdentifierState = true;
                }
                else if (keywordID == CqlParser.FUNCTION)
                {
                    // Treat the symbol following FUNCTION keyword as an identifier.
                    // Inline function names in definition section have stronger restrictions than normal identifiers
                    _symbolAsInlineFunctionNameState = true;
                }

                terminalToken = NewToken(keywordID, new TerminalToken(symbol, _iPos));
                return true;
            }
            else
            {
                terminalToken = null;
                return false;
            }
        }

        // <summary>
        // Returns true when current symbol looks like a caninical function name in a function call.
        // Method only treats canonical functions with names ovelapping eSQL keywords.
        // This check allows calling these canonical functions without escaping their names.
        // Check lookAheadChar for a left paren to see if looks like a function call, check symbol against the list of
        // canonical functions with names overlapping keywords.
        // </summary>
        private bool IsCanonicalFunctionCall(string symbol, Char lookAheadChar)
        {
            return lookAheadChar == '(' && InternalCanonicalFunctionNames.Contains(symbol);
        }

        private Token MapUnescapedIdentifier(string symbol)
        {
            // Validate before calling ResetSymbolAsIdentifierState(...) because it will reset _symbolAsInlineFunctionNameState
            var invalidIdentifier = InternalInvalidAliasNames.Contains(symbol);
            if (_symbolAsInlineFunctionNameState)
            {
                invalidIdentifier |= InternalInvalidInlineFunctionNames.Contains(symbol);
            }

            ResetSymbolAsIdentifierState(true);

            if (invalidIdentifier)
            {
                var errorMessage = Strings.InvalidAliasName(symbol);
                throw EntitySqlException.Create(_query, errorMessage, _iPos, null, false, null);
            }
            else
            {
                var id = new Identifier(symbol, false, _query, _iPos);
                id.ErrCtx.ErrorContextInfo = EntityRes.CtxIdentifier;
                return NewToken(CqlParser.IDENTIFIER, id);
            }
        }

        // <summary>
        // Skip insignificant whitespace to reach the first potentially significant char.
        // </summary>
        private Char GetLookAheadChar()
        {
            yy_mark_end();
            var lookAheadChar = yy_advance();
            while (lookAheadChar != YY_EOF
                   && (Char.IsWhiteSpace(lookAheadChar) || IsNewLine(lookAheadChar)))
            {
                lookAheadChar = yy_advance();
            }
            yy_to_mark();
            return lookAheadChar;
        }

        private bool IsInSymbolAsIdentifierState(char lookAheadChar)
        {
            return _symbolAsIdentifierState ||
                   _symbolAsAliasIdentifierState ||
                   _symbolAsInlineFunctionNameState ||
                   lookAheadChar == '.' /*treat symbols followed by DOT as identifiers*/;
        }

        // <summary>
        // Resets "symbol as identifier" state.
        // </summary>
        // <param name="significant"> see function callers for more info </param>
        private void ResetSymbolAsIdentifierState(bool significant)
        {
            _symbolAsIdentifierState = false;

            // Do not reset the following states if going over {NONNEWLINE_SPACE} or {NEWLINE} or {LINE_COMMENT}
            if (significant)
            {
                _symbolAsAliasIdentifierState = false;
                _symbolAsInlineFunctionNameState = false;
            }
        }

        #endregion

        // <summary>
        // Maps operator to respective token
        // </summary>
        // <param name="oper"> operator lexeme </param>
        // <returns> Token </returns>
        internal Token MapOperator(string oper)
        {
            if (InternalOperatorDictionary.ContainsKey(oper))
            {
                return NewToken(InternalOperatorDictionary[oper], new TerminalToken(oper, _iPos));
            }
            else
            {
                var errorMessage = Strings.InvalidOperatorSymbol;
                throw EntitySqlException.Create(_query, errorMessage, _iPos, null, false, null);
            }
        }

        // <summary>
        // Maps punctuator to respective token
        // </summary>
        // <param name="punct"> punctuator </param>
        // <returns> Token </returns>
        internal Token MapPunctuator(string punct)
        {
            if (InternalPunctuatorDictionary.ContainsKey(punct))
            {
                ResetSymbolAsIdentifierState(true);

                if (punct.Equals(".", StringComparison.OrdinalIgnoreCase))
                {
                    _symbolAsIdentifierState = true;
                }

                return NewToken(InternalPunctuatorDictionary[punct], new TerminalToken(punct, _iPos));
            }
            else
            {
                var errorMessage = Strings.InvalidPunctuatorSymbol;
                throw EntitySqlException.Create(_query, errorMessage, _iPos, null, false, null);
            }
        }

        // <summary>
        // Maps double quoted string to a literal or an idendifier
        // </summary>
        // <returns> Token </returns>
        internal Token MapDoubleQuotedString(string symbol)
        {
            // If there is a mode that makes eSQL parser to follow the SQL-92 rules regarding quotation mark
            // delimiting identifiers then this method may decide to map to identifiers.
            // In this case identifiers delimited by double quotation marks can be either eSQL reserved keywords
            // or can contain characters not usually allowed by the eSQL syntax rules for identifiers, 
            // so identifiers mapped here should be treated as escaped identifiers.
            return NewLiteralToken(symbol, LiteralKind.String);
        }

        // <summary>
        // Creates literal token
        // </summary>
        // <param name="literal"> literal </param>
        // <param name="literalKind"> literal kind </param>
        // <returns> Literal Token </returns>
        internal Token NewLiteralToken(string literal, LiteralKind literalKind)
        {
            DebugCheck.NotEmpty(literal);
            Debug.Assert(literalKind != LiteralKind.Null, "literalKind must not be LiteralKind.Null");

            var literalValue = literal;
            switch (literalKind)
            {
                case LiteralKind.Binary:
                    literalValue = GetLiteralSingleQuotePayload(literal);
                    if (!IsValidBinaryValue(literalValue))
                    {
                        var errorMessage = Strings.InvalidLiteralFormat("binary", literalValue);
                        throw EntitySqlException.Create(_query, errorMessage, _iPos, null, false, null);
                    }
                    break;

                case LiteralKind.String:
                    if ('N' == literal[0])
                    {
                        literalKind = LiteralKind.UnicodeString;
                    }
                    break;

                case LiteralKind.DateTime:
                    literalValue = GetLiteralSingleQuotePayload(literal);
                    if (!IsValidDateTimeValue(literalValue))
                    {
                        var errorMessage = Strings.InvalidLiteralFormat("datetime", literalValue);
                        throw EntitySqlException.Create(_query, errorMessage, _iPos, null, false, null);
                    }
                    break;

                case LiteralKind.Time:
                    literalValue = GetLiteralSingleQuotePayload(literal);
                    if (!IsValidTimeValue(literalValue))
                    {
                        var errorMessage = Strings.InvalidLiteralFormat("time", literalValue);
                        throw EntitySqlException.Create(_query, errorMessage, _iPos, null, false, null);
                    }
                    break;
                case LiteralKind.DateTimeOffset:
                    literalValue = GetLiteralSingleQuotePayload(literal);
                    if (!IsValidDateTimeOffsetValue(literalValue))
                    {
                        var errorMessage = Strings.InvalidLiteralFormat("datetimeoffset", literalValue);
                        throw EntitySqlException.Create(_query, errorMessage, _iPos, null, false, null);
                    }
                    break;

                case LiteralKind.Guid:
                    literalValue = GetLiteralSingleQuotePayload(literal);
                    if (!IsValidGuidValue(literalValue))
                    {
                        var errorMessage = Strings.InvalidLiteralFormat("guid", literalValue);
                        throw EntitySqlException.Create(_query, errorMessage, _iPos, null, false, null);
                    }
                    break;
            }

            return NewToken(CqlParser.LITERAL, new Literal(literalValue, literalKind, _query, _iPos));
        }

        // <summary>
        // Creates parameter token
        // </summary>
        // <param name="param"> param </param>
        // <returns> Parameter Token </returns>
        internal Token NewParameterToken(string param)
        {
            return NewToken(CqlParser.PARAMETER, new QueryParameter(param, _query, _iPos));
        }

        // <summary>
        // handles escaped identifiers
        // ch will always be translated i.e. normalized.
        // </summary>
        internal Token HandleEscapedIdentifiers()
        {
            var ch = YYText[0];
            while (ch != YY_EOF)
            {
                if (ch == ']')
                {
                    yy_mark_end();
                    ch = yy_advance();
                    if (ch != ']')
                    {
                        yy_to_mark();
                        ResetSymbolAsIdentifierState(true);
                        return MapIdentifierOrKeyword(YYText.Replace("]]", "]"));
                    }
                }
                ch = yy_advance();
            }
            Debug.Assert(ch == YY_EOF, "ch == YY_EOF");
            var errorMessage = Strings.InvalidEscapedIdentifierUnbalanced(YYText);
            throw EntitySqlException.Create(_query, errorMessage, _iPos, null, false, null);
        }

        internal static bool IsLetterOrDigitOrUnderscore(string symbol, out bool isIdentifierASCII)
        {
            isIdentifierASCII = true;
            for (var i = 0; i < symbol.Length; i++)
            {
                isIdentifierASCII = isIdentifierASCII && symbol[i] < 0x80;
                if (!isIdentifierASCII
                    && !IsLetter(symbol[i])
                    && !IsDigit(symbol[i])
                    && (symbol[i] != '_'))
                {
                    return false;
                }
            }
            return true;
        }

        private static bool IsLetter(char c)
        {
            return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
        }

        private static bool IsDigit(char c)
        {
            return (c >= '0' && c <= '9');
        }

        private static bool isHexDigit(char c)
        {
            return (IsDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'));
        }

        // <summary>
        // Returns true if given char is a new line character defined by
        // UNICODE 5.0, section 5.8 Newline Guidelines.
        // These are 'mandatory' line breaks. NOTE that CRLF is treated as a
        // composite 'character' and was intentionaly omitted in the character set bellow.
        // </summary>
        internal static bool IsNewLine(Char c)
        {
            for (var i = 0; i < _newLineCharacters.Length; i++)
            {
                if (c == _newLineCharacters[i])
                {
                    return true;
                }
            }
            return false;
        }

        // <summary>
        // extracts single quoted literal 'payload'. literal MUST BE normalized.
        // </summary>
        private static string GetLiteralSingleQuotePayload(string literal)
        {
            Debug.Assert(-1 != literal.IndexOf('\''), "quoted literal value must have single quotes");
            Debug.Assert(-1 != literal.LastIndexOf('\''), "quoted literal value must have single quotes");
            Debug.Assert(literal.IndexOf('\'') != literal.LastIndexOf('\''), "quoted literal value must have 2 single quotes");
            Debug.Assert(literal.Split(new[] { '\'' }).Length == 3, "quoted literal value must have 2 single quotes");

            // NOTE: this is not a precondition validation. This validation is for security purposes based on the 
            // paranoid assumption that all input is evil. we should not see this exception under normal 
            // conditions.
            if ((literal.Split(new[] { '\'' }).Length != 3)
                || (-1 == literal.IndexOf('\''))
                || (-1 == literal.LastIndexOf('\'')))
            {
                var message = Strings.MalformedSingleQuotePayload;
                throw new EntitySqlException(message);
            }

            var startIndex = literal.IndexOf('\'');

            var literalPayload = literal.Substring(startIndex + 1, literal.Length - (startIndex + 2));

            Debug.Assert(literalPayload.IndexOf('\'') == -1, "quoted literal payload must not have single quotes");
            Debug.Assert(literalPayload.LastIndexOf('\'') == -1, "quoted literal payload must not have single quotes");

            // NOTE: this is not a precondition validation. This validation is for security purposes based on the 
            // paranoid assumption that all input is evil. we should not see this exception under normal 
            // conditions.
            if (literalPayload.Split(new[] { '\'' }).Length != 1)
            {
                var message = Strings.MalformedSingleQuotePayload;
                throw new EntitySqlException(message);
            }

            return literalPayload;
        }

        // <summary>
        // returns true if guid literal value format is valid
        // </summary>
        private static bool IsValidGuidValue(string guidValue)
        {
            var startIndex = 0;
            var endIndex = guidValue.Length - 1;
            if ((endIndex - startIndex) + 1 != 36)
            {
                return false;
            }

            var i = 0;
            var bValid = true;
            while (bValid && i < 36)
            {
                if ((i == 8)
                    || (i == 13)
                    || (i == 18)
                    || (i == 23))
                {
                    bValid = (guidValue[startIndex + i] == '-');
                }
                else
                {
                    bValid = isHexDigit(guidValue[startIndex + i]);
                }
                i++;
            }
            return bValid;
        }

        // <summary>
        // returns true if binary literal value format is valid
        // </summary>
        private static bool IsValidBinaryValue(string binaryValue)
        {
            DebugCheck.NotNull(binaryValue);

            if (String.IsNullOrEmpty(binaryValue))
            {
                return true;
            }

            var i = 0;
            var bValid = binaryValue.Length > 0;
            while (bValid && i < binaryValue.Length)
            {
                bValid = isHexDigit(binaryValue[i++]);
            }

            return bValid;
        }

        // <summary>
        // Returns true if datetime literal value format is valid
        // allowed format is: dddd-d?d-d?d{space}+d?d:d?d(:d?d(.d?d?d)?)?
        // where d is any decimal digit.
        // </summary>
        private static bool IsValidDateTimeValue(string datetimeValue)
        {
            if (null == _reDateTimeValue)
            {
                _reDateTimeValue = new Regex(_datetimeValueRegularExpression, RegexOptions.Singleline | RegexOptions.CultureInvariant);
            }
            return _reDateTimeValue.IsMatch(datetimeValue);
        }

        // <summary>
        // Returns true if time literal value format is valid
        // allowed format is: +d?d:d?d(:d?d(.d?d?d)?)?
        // where d is any decimal digit.
        // </summary>
        private static bool IsValidTimeValue(string timeValue)
        {
            if (null == _reTimeValue)
            {
                _reTimeValue = new Regex(_timeValueRegularExpression, RegexOptions.Singleline | RegexOptions.CultureInvariant);
            }
            return _reTimeValue.IsMatch(timeValue);
        }

        // <summary>
        // Returns true if datetimeoffset literal value format is valid
        // allowed format is: dddd-d?d-d?d{space}+d?d:d?d(:d?d(.d?d?d)?)?([+-]d?d:d?d)?
        // where d is any decimal digit.
        // </summary>
        private static bool IsValidDateTimeOffsetValue(string datetimeOffsetValue)
        {
            if (null == _reDateTimeOffsetValue)
            {
                _reDateTimeOffsetValue = new Regex(
                    _datetimeOffsetValueRegularExpression, RegexOptions.Singleline | RegexOptions.CultureInvariant);
            }
            return _reDateTimeOffsetValue.IsMatch(datetimeOffsetValue);
        }

        private static Dictionary<string, short> InternalKeywordDictionary
        {
            get
            {
                if (null == _keywords)
                {
                    #region Initializes eSQL keywords

                    var keywords = new Dictionary<string, short>(60, _stringComparer);
                    keywords.Add("all", CqlParser.ALL);
                    keywords.Add("and", CqlParser.AND);
                    keywords.Add("anyelement", CqlParser.ANYELEMENT);
                    keywords.Add("apply", CqlParser.APPLY);
                    keywords.Add("as", CqlParser.AS);
                    keywords.Add("asc", CqlParser.ASC);
                    keywords.Add("between", CqlParser.BETWEEN);
                    keywords.Add("by", CqlParser.BY);
                    keywords.Add("case", CqlParser.CASE);
                    keywords.Add("cast", CqlParser.CAST);
                    keywords.Add("collate", CqlParser.COLLATE);
                    keywords.Add("collection", CqlParser.COLLECTION);
                    keywords.Add("createref", CqlParser.CREATEREF);
                    keywords.Add("cross", CqlParser.CROSS);
                    keywords.Add("deref", CqlParser.DEREF);
                    keywords.Add("desc", CqlParser.DESC);
                    keywords.Add("distinct", CqlParser.DISTINCT);
                    keywords.Add("element", CqlParser.ELEMENT);
                    keywords.Add("else", CqlParser.ELSE);
                    keywords.Add("end", CqlParser.END);
                    keywords.Add("escape", CqlParser.ESCAPE);
                    keywords.Add("except", CqlParser.EXCEPT);
                    keywords.Add("exists", CqlParser.EXISTS);
                    keywords.Add("false", CqlParser.LITERAL);
                    keywords.Add("flatten", CqlParser.FLATTEN);
                    keywords.Add("from", CqlParser.FROM);
                    keywords.Add("full", CqlParser.FULL);
                    keywords.Add("function", CqlParser.FUNCTION);
                    keywords.Add("group", CqlParser.GROUP);
                    keywords.Add("grouppartition", CqlParser.GROUPPARTITION);
                    keywords.Add("having", CqlParser.HAVING);
                    keywords.Add("in", CqlParser.IN);
                    keywords.Add("inner", CqlParser.INNER);
                    keywords.Add("intersect", CqlParser.INTERSECT);
                    keywords.Add("is", CqlParser.IS);
                    keywords.Add("join", CqlParser.JOIN);
                    keywords.Add("key", CqlParser.KEY);
                    keywords.Add("left", CqlParser.LEFT);
                    keywords.Add("like", CqlParser.LIKE);
                    keywords.Add("limit", CqlParser.LIMIT);
                    keywords.Add("multiset", CqlParser.MULTISET);
                    keywords.Add("navigate", CqlParser.NAVIGATE);
                    keywords.Add("not", CqlParser.NOT);
                    keywords.Add("null", CqlParser.NULL);
                    keywords.Add("of", CqlParser.OF);
                    keywords.Add("oftype", CqlParser.OFTYPE);
                    keywords.Add("on", CqlParser.ON);
                    keywords.Add("only", CqlParser.ONLY);
                    keywords.Add("or", CqlParser.OR);
                    keywords.Add("order", CqlParser.ORDER);
                    keywords.Add("outer", CqlParser.OUTER);
                    keywords.Add("overlaps", CqlParser.OVERLAPS);
                    keywords.Add("ref", CqlParser.REF);
                    keywords.Add("relationship", CqlParser.RELATIONSHIP);
                    keywords.Add("right", CqlParser.RIGHT);
                    keywords.Add("row", CqlParser.ROW);
                    keywords.Add("select", CqlParser.SELECT);
                    keywords.Add("set", CqlParser.SET);
                    keywords.Add("skip", CqlParser.SKIP);
                    keywords.Add("then", CqlParser.THEN);
                    keywords.Add("top", CqlParser.TOP);
                    keywords.Add("treat", CqlParser.TREAT);
                    keywords.Add("true", CqlParser.LITERAL);
                    keywords.Add("union", CqlParser.UNION);
                    keywords.Add("using", CqlParser.USING);
                    keywords.Add("value", CqlParser.VALUE);
                    keywords.Add("when", CqlParser.WHEN);
                    keywords.Add("where", CqlParser.WHERE);
                    keywords.Add("with", CqlParser.WITH);
                    _keywords = keywords;

                    #endregion
                }
                return _keywords;
            }
        }

        private static HashSet<string> InternalInvalidAliasNames
        {
            get
            {
                if (null == _invalidAliasNames)
                {
                    #region Initializes invalid aliases

                    var invalidAliasName = new HashSet<string>(_stringComparer);
                    invalidAliasName.Add("all");
                    invalidAliasName.Add("and");
                    invalidAliasName.Add("apply");
                    invalidAliasName.Add("as");
                    invalidAliasName.Add("asc");
                    invalidAliasName.Add("between");
                    invalidAliasName.Add("by");
                    invalidAliasName.Add("case");
                    invalidAliasName.Add("cast");
                    invalidAliasName.Add("collate");
                    invalidAliasName.Add("createref");
                    invalidAliasName.Add("deref");
                    invalidAliasName.Add("desc");
                    invalidAliasName.Add("distinct");
                    invalidAliasName.Add("element");
                    invalidAliasName.Add("else");
                    invalidAliasName.Add("end");
                    invalidAliasName.Add("escape");
                    invalidAliasName.Add("except");
                    invalidAliasName.Add("exists");
                    invalidAliasName.Add("flatten");
                    invalidAliasName.Add("from");
                    invalidAliasName.Add("group");
                    invalidAliasName.Add("having");
                    invalidAliasName.Add("in");
                    invalidAliasName.Add("inner");
                    invalidAliasName.Add("intersect");
                    invalidAliasName.Add("is");
                    invalidAliasName.Add("join");
                    invalidAliasName.Add("like");
                    invalidAliasName.Add("multiset");
                    invalidAliasName.Add("navigate");
                    invalidAliasName.Add("not");
                    invalidAliasName.Add("null");
                    invalidAliasName.Add("of");
                    invalidAliasName.Add("oftype");
                    invalidAliasName.Add("on");
                    invalidAliasName.Add("only");
                    invalidAliasName.Add("or");
                    invalidAliasName.Add("overlaps");
                    invalidAliasName.Add("ref");
                    invalidAliasName.Add("relationship");
                    invalidAliasName.Add("select");
                    invalidAliasName.Add("set");
                    invalidAliasName.Add("then");
                    invalidAliasName.Add("treat");
                    invalidAliasName.Add("union");
                    invalidAliasName.Add("using");
                    invalidAliasName.Add("when");
                    invalidAliasName.Add("where");
                    invalidAliasName.Add("with");
                    _invalidAliasNames = invalidAliasName;

                    #endregion
                }
                return _invalidAliasNames;
            }
        }

        private static HashSet<string> InternalInvalidInlineFunctionNames
        {
            get
            {
                if (null == _invalidInlineFunctionNames)
                {
                    #region Initializes invalid inline function names

                    var invalidInlineFunctionNames = new HashSet<string>(_stringComparer);
                    invalidInlineFunctionNames.Add("anyelement");
                    invalidInlineFunctionNames.Add("element");
                    invalidInlineFunctionNames.Add("function");
                    invalidInlineFunctionNames.Add("grouppartition");
                    invalidInlineFunctionNames.Add("key");
                    invalidInlineFunctionNames.Add("ref");
                    invalidInlineFunctionNames.Add("row");
                    invalidInlineFunctionNames.Add("skip");
                    invalidInlineFunctionNames.Add("top");
                    invalidInlineFunctionNames.Add("value");
                    _invalidInlineFunctionNames = invalidInlineFunctionNames;

                    #endregion
                }
                return _invalidInlineFunctionNames;
            }
        }

        private static Dictionary<string, short> InternalOperatorDictionary
        {
            get
            {
                if (null == _operators)
                {
                    #region Initializes operator dictionary

                    var operators = new Dictionary<string, short>(16, _stringComparer);
                    operators.Add("==", CqlParser.OP_EQ);
                    operators.Add("!=", CqlParser.OP_NEQ);
                    operators.Add("<>", CqlParser.OP_NEQ);
                    operators.Add("<", CqlParser.OP_LT);
                    operators.Add("<=", CqlParser.OP_LE);
                    operators.Add(">", CqlParser.OP_GT);
                    operators.Add(">=", CqlParser.OP_GE);
                    operators.Add("&&", CqlParser.AND);
                    operators.Add("||", CqlParser.OR);
                    operators.Add("!", CqlParser.NOT);
                    operators.Add("+", CqlParser.PLUS);
                    operators.Add("-", CqlParser.MINUS);
                    operators.Add("*", CqlParser.STAR);
                    operators.Add("/", CqlParser.FSLASH);
                    operators.Add("%", CqlParser.PERCENT);
                    _operators = operators;

                    #endregion
                }
                return _operators;
            }
        }

        private static Dictionary<string, short> InternalPunctuatorDictionary
        {
            get
            {
                if (null == _punctuators)
                {
                    #region Initializes punctuators dictionary

                    var punctuators = new Dictionary<string, short>(16, _stringComparer);
                    punctuators.Add(",", CqlParser.COMMA);
                    punctuators.Add(":", CqlParser.COLON);
                    punctuators.Add(".", CqlParser.DOT);
                    punctuators.Add("?", CqlParser.QMARK);
                    punctuators.Add("(", CqlParser.L_PAREN);
                    punctuators.Add(")", CqlParser.R_PAREN);
                    punctuators.Add("[", CqlParser.L_BRACE);
                    punctuators.Add("]", CqlParser.R_BRACE);
                    punctuators.Add("{", CqlParser.L_CURLY);
                    punctuators.Add("}", CqlParser.R_CURLY);
                    punctuators.Add(";", CqlParser.SCOLON);
                    punctuators.Add("=", CqlParser.EQUAL);
                    _punctuators = punctuators;

                    #endregion
                }
                return _punctuators;
            }
        }

        private static HashSet<string> InternalCanonicalFunctionNames
        {
            get
            {
                if (null == _canonicalFunctionNames)
                {
                    var canonicalFunctionNames = new HashSet<string>(_stringComparer);
                    canonicalFunctionNames.Add("left");
                    canonicalFunctionNames.Add("right");
                    _canonicalFunctionNames = canonicalFunctionNames;
                }
                return _canonicalFunctionNames;
            }
        }
    }
}
