using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace FtxDemo.Data {
    public class FtxInterceptor : IDbCommandInterceptor {
        private const string PREFIX_STRING = "$:";
        private static readonly DbType[] REWRITE_PARAM_TYPES = { DbType.String, DbType.AnsiString, DbType.StringFixedLength, DbType.AnsiStringFixedLength };
        private static readonly string[] OPERATORS_BINARY = { "and", "or" };
        private static readonly string[] OPERATORS_UNARY = { "not" };

        #region Ignored states

        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { }
        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { }
        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { }
        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { }

        #endregion

        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) {
            RewriteQuery(command);
        }

        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) {
            RewriteQuery(command);
        }

        static void RewriteQuery(DbCommand cmd) {
            for (int i = 0; i < cmd.Parameters.Count; i++) {
                var parameter = cmd.Parameters[i];

                if (REWRITE_PARAM_TYPES.Contains(parameter.DbType)) {
                    if (parameter.Value == DBNull.Value) continue;  // ignore null values

                    var value = parameter.Value as string;

                    if (value.IndexOf(PREFIX_STRING, StringComparison.Ordinal) >= 0) {
                        // Process parameter value
                        value = value.Substring(PREFIX_STRING.Length);  // remove prefix
                        value = value.Substring(1, value.Length - 2);   // remove % escaping by linq translator from string.Contains to sql LIKE
                        parameter.Value = value;
                        parameter.DbType = DbType.String;
                        parameter.Size = value.Length;

                        // Replace LIKE with CONTAINS
                        var patternString = $@"\[(\w*)\].\[(\w*)\]\s*LIKE\s*@{parameter.ParameterName}\s?(?:ESCAPE N?'~')";
                        var replaceString = $@"CONTAINS([$1].[$2], @{parameter.ParameterName})";
                        cmd.CommandText = Regex.Replace(cmd.CommandText, patternString, replaceString);
                    }
                }
            }
        }

        public static string CreateQuery(string s, bool addPrefix = true) {
            if (s == null) throw new ArgumentNullException(nameof(s));
            if (string.IsNullOrWhiteSpace(s)) throw new ArgumentException("Value cannot be empty or whitespace only string.", nameof(s));

            // Generate tokens from string
            var tokens = TokenizeQuery(s);

            // Generate query from tokens
            var q = ParseQueryTokens(tokens);

            // Add intereceptor prefix when requested
            if (addPrefix) q = PREFIX_STRING + q;

            return q;
        }

        private static IEnumerable<string> TokenizeQuery(string s) {
            if (s == null) throw new ArgumentNullException(nameof(s));
            if (string.IsNullOrWhiteSpace(s)) throw new ArgumentException("Value cannot be empty or whitespace only string.", nameof(s));

            s = s.ToLower();

            var lastToken = string.Empty;
            var isInQuote = false;

            for (int i = 0; i < s.Length; i++) {
                var c = s[i];
                if (Char.IsWhiteSpace(c)) {
                    // Character is whitespace
                    if (isInQuote) {
                        // Add space when in quote
                        lastToken += " ";
                    }
                    else if (!String.IsNullOrWhiteSpace(lastToken)) {
                        // End of token - add it
                        yield return lastToken;
                        lastToken = string.Empty;
                    }
                }
                else if (c == '"') {
                    if (isInQuote) {
                        // End of quote - close quote and add token
                        if (!String.IsNullOrWhiteSpace(lastToken)) yield return $"\"{lastToken}\"";
                        lastToken = string.Empty;
                        isInQuote = false;
                    }
                    else {
                        // Open quote
                        isInQuote = true;
                    }
                }
                else if (c == '(' || c == ')') {
                    // Bracket
                    if (isInQuote) {
                        lastToken += c;
                    }
                    else {
                        yield return c.ToString();
                    }
                }
                else {
                    // Other char
                    lastToken += c;
                }
            }

            // Add last token if not empty
            if (!String.IsNullOrWhiteSpace(lastToken)) {
                if (isInQuote) lastToken = $"\"{lastToken}\"";
                yield return lastToken;
            }
        }

        private static string ParseQueryTokens(IEnumerable<string> tokenList) {
            var tokens = tokenList.ToArray();
            var qsb = new StringBuilder();
            var lastTokenIsOperator = false;

            for (int i = 0; i < tokens.Length; i++) {
                var token = tokens[i];

                if (OPERATORS_BINARY.Contains(token)) {
                    // Token is binary operator
                    if (lastTokenIsOperator) continue;  // Ignore two consequent operators
                    if (i + 1 == tokens.Length) continue; // Ignore operator at end of query

                    qsb.Append(token.ToUpper());
                    qsb.Append(" ");
                    lastTokenIsOperator = true;
                }
                else if (OPERATORS_UNARY.Contains(token)) {
                    // Token is unary operator
                    if (i + 1 == tokens.Length) continue; // Ignore operator at end of query

                    if (!lastTokenIsOperator && i > 0) qsb.Append("AND ");
                    qsb.Append(token.ToUpper());
                    qsb.Append(" ");
                    lastTokenIsOperator = true;
                }
                else if (token.StartsWith("\"", StringComparison.Ordinal)) {
                    // Token is phrase
                    qsb.Append(token);
                    qsb.Append(" ");
                    lastTokenIsOperator = false;
                }
                else {
                    // Token is something else
                    if (!lastTokenIsOperator && i > 0) qsb.Append("AND ");

                    qsb.Append($"FORMSOF(INFLECTIONAL,{token}) ");
                    lastTokenIsOperator = false;
                }
            }

            return qsb.ToString();
        }

    }
}
