using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using LiteDB;

namespace Dacryptero.Search
{
    public static class SingleFieldSearcher
    {
        public static (double score, ResultFound? found) Search(
            FoundType found, string name,
            BsonValue value, string word
        )
        {
            switch (value.Type)
            {
                case BsonType.Int32:
                    return Search(found, name, 
                        value.AsInt32.ToString(), AST.FieldComparer.Equality, word
                    );
                case BsonType.Int64:
                    return Search(found, name, 
                        value.AsInt64.ToString(), AST.FieldComparer.Equality, word
                    );
                case BsonType.Boolean:
                    return Search(found, name, 
                        value.AsBoolean.ToString(), AST.FieldComparer.Equality, word
                    );
                case BsonType.DateTime:
                    return Search(found, name, 
                        value.AsDateTime.ToString("s"), AST.FieldComparer.StartWith, word
                    );
                case BsonType.Double:
                    return Search(found, name, 
                        value.AsDouble.ToString(), AST.FieldComparer.Equality, word
                    );
                case BsonType.String:
                    return Search(found, name, value.AsString, AST.FieldComparer.Contains, word);
                default: return (0, null);

            }
        }

        public static (double score, ResultFound? found) Search(
            FoundType found, string name,
            BsonValue value, AST.FieldOptionBase field
        )
        {
            switch (value.Type)
            {
                case BsonType.Int32:
                    if (field.LongValue is null)
                        return (0, null);
                    else return Search(found, name, value.AsInt32, field.Comparer, field.LongValue.Value);
                case BsonType.Int64:
                    if (field.LongValue is null)
                        return (0, null);
                    else return Search(found, name, value.AsInt64, field.Comparer, field.LongValue.Value);
                case BsonType.Boolean:
                    if (field.BoolValue is null)
                        return (0, null);
                    else return Search(found, name, value.AsBoolean, field.Comparer, field.BoolValue.Value);
                case BsonType.DateTime:
                    if (field.Comparer is AST.FieldComparer.StartWith 
                        or AST.FieldComparer.EndsWith
                        or AST.FieldComparer.Contains
                        && field.Value is not null)
                        return Search(found, name, value.AsDateTime.ToString("s"), field.Comparer, field.Value);
                    if (field.DateTimeValue is null)
                        return (0, null);
                    else return Search(found, name, value.AsDateTime, field.Comparer, field.DateTimeValue.Value);
                case BsonType.Double:
                    if (field.DoubleValue is null)
                        return (0, null);
                    else return Search(found, name, value.AsDouble, field.Comparer, field.DoubleValue.Value);
                case BsonType.String:
                    if (field.Value is null)
                        return (0, null);
                    else return Search(found, name, value.AsString, field.Comparer, field.Value);
                default: return (0, null);
            }
        }

        private static (double score, ResultFound? found1) Search(
            FoundType found, string name, long dbValue, AST.FieldComparer comparer, long queryValue
        )
        {
            var success = comparer switch
            {
                AST.FieldComparer.Lower => dbValue < queryValue,
                AST.FieldComparer.LowerOrEqual => dbValue <= queryValue,
                AST.FieldComparer.Higher => dbValue > queryValue,
                AST.FieldComparer.HigherOrEqual => dbValue >= queryValue,
                AST.FieldComparer.Equality => dbValue == queryValue,
                _ => false,
            };
            if (!success)
                return (0, null);
            var res = new ResultFound(found, name);
            res.Preview.Add(new ResultFound.FoundPreview(
                ResultFound.FoundPreviewType.Term,
                dbValue.ToString()
            ));
            return (1, res);
        }

        private static (double score, ResultFound? found1) Search(
            FoundType found, string name, bool dbValue, AST.FieldComparer comparer, bool queryValue
        )
        {
            var success = comparer switch
            {
                AST.FieldComparer.Lower => !dbValue && queryValue,
                AST.FieldComparer.LowerOrEqual => !dbValue || dbValue == queryValue,
                AST.FieldComparer.Higher => dbValue && !queryValue,
                AST.FieldComparer.HigherOrEqual => dbValue == queryValue || !queryValue,
                AST.FieldComparer.Equality => dbValue == queryValue,
                _ => false,
            };
            if (!success)
                return (0, null);
            var res = new ResultFound(found, name);
            res.Preview.Add(new ResultFound.FoundPreview(
                ResultFound.FoundPreviewType.Term,
                dbValue.ToString()
            ));
            return (1, res);
        }
        
        private static (double score, ResultFound? found1) Search(
            FoundType found, string name, DateTime dbValue, AST.FieldComparer comparer, DateTime queryValue
        )
        {
            var success = comparer switch
            {
                AST.FieldComparer.Lower => dbValue < queryValue,
                AST.FieldComparer.LowerOrEqual => dbValue <= queryValue,
                AST.FieldComparer.Higher => dbValue > queryValue,
                AST.FieldComparer.HigherOrEqual => dbValue >= queryValue,
                AST.FieldComparer.Equality => dbValue == queryValue,
                _ => false,
            };
            if (!success)
                return (0, null);
            var res = new ResultFound(found, name);
            res.Preview.Add(new ResultFound.FoundPreview(
                ResultFound.FoundPreviewType.Term,
                dbValue.ToString("s")
            ));
            return (1, res);
        }

        private static (double score, ResultFound? found1) Search(
            FoundType found, string name, double dbValue, AST.FieldComparer comparer, double queryValue
        )
        {
            var success = comparer switch
            {
                AST.FieldComparer.Lower => dbValue < queryValue,
                AST.FieldComparer.LowerOrEqual => dbValue <= queryValue,
                AST.FieldComparer.Higher => dbValue > queryValue,
                AST.FieldComparer.HigherOrEqual => dbValue >= queryValue,
                AST.FieldComparer.Equality => dbValue == queryValue,
                _ => false,
            };
            if (!success)
                return (0, null);
            var res = new ResultFound(found, name);
            res.Preview.Add(new ResultFound.FoundPreview(
                ResultFound.FoundPreviewType.Term,
                dbValue.ToString()
            ));
            return (1, res);
        }

        private static (double score, ResultFound? found1) Search(
            FoundType found, string name, string dbValue, AST.FieldComparer comparer, string queryValue
        )
        {
            var dbValueLower = dbValue.ToLowerInvariant();
            queryValue = queryValue.ToLowerInvariant();
            var res = new ResultFound(found, name);
            switch (comparer)
            {
                case AST.FieldComparer.Lower:
                    if (dbValueLower.CompareTo(queryValue) < 0)
                    {
                        res.Preview.AddRange(SlicePreviewAfter(dbValue, 0, dbValue.Length));
                        return (1, res);
                    }
                    else return (0, null);
                case AST.FieldComparer.LowerOrEqual:
                    if (dbValueLower.CompareTo(queryValue) <= 0)
                    {
                        res.Preview.AddRange(SlicePreviewAfter(dbValue, 0, dbValue.Length));
                        return (1, res);
                    }
                    else return (0, null);
                case AST.FieldComparer.Higher:
                    if (dbValueLower.CompareTo(queryValue) > 0)
                    {
                        res.Preview.AddRange(SlicePreviewAfter(dbValue, 0, dbValue.Length));
                        return (1, res);
                    }
                    else return (0, null);
                case AST.FieldComparer.HigherOrEqual:
                    if (dbValueLower.CompareTo(queryValue) >= 0)
                    {
                        res.Preview.AddRange(SlicePreviewAfter(dbValue, 0, dbValue.Length));
                        return (1, res);
                    }
                    else return (0, null);
                case AST.FieldComparer.Equality:
                    if (dbValueLower == queryValue)
                    {
                        res.Preview.AddRange(SlicePreviewAfter(dbValue, 0, dbValue.Length));
                        return (1, res);
                    }
                    else return (0, null);
                case AST.FieldComparer.StartWith:
                    if (dbValueLower.StartsWith(queryValue))
                    {
                        res.Preview.Add(new ResultFound.FoundPreview(
                            ResultFound.FoundPreviewType.Term,
                            dbValue.Substring(0, queryValue.Length)
                        ));
                        res.Preview.AddRange(SlicePreviewAfter(dbValue, queryValue.Length, dbValue.Length));
                        return (1, res);
                    }
                    else return (0, null);
                case AST.FieldComparer.EndsWith:
                    if (dbValueLower.EndsWith(queryValue))
                    {
                        res.Preview.AddRange(SlicePreviewBefore(dbValue, dbValue.Length - queryValue.Length, 0));
                        res.Preview.Add(new ResultFound.FoundPreview(
                            ResultFound.FoundPreviewType.Term,
                            dbValue.Substring(dbValue.Length - queryValue.Length, queryValue.Length)
                        ));
                        return (1, res);
                    }
                    else return (0, null);
                case AST.FieldComparer.Contains:
                {
                    var index = dbValueLower.IndexOf(queryValue);
                    if (index >= 0)
                    {
                        if (index > 0)
                            res.Preview.AddRange(SlicePreviewBefore(
                                dbValue, index, 0
                            ));
                        res.Preview.Add(new ResultFound.FoundPreview(
                            ResultFound.FoundPreviewType.Term,
                            dbValue.Substring(index, queryValue.Length)
                        ));
                        if (index + queryValue.Length < dbValue.Length)
                            res.Preview.AddRange(SlicePreviewAfter(
                                dbValue, index + queryValue.Length, dbValue.Length
                            ));
                        return (1, res);
                    }
                    else return (0, null);
                }
                case AST.FieldComparer.Regex:
                {
                    try
                    {
                        var regex = new Regex(queryValue, RegexOptions.Compiled);
                        var match = regex.Match(queryValue);
                        if (match.Success)
                        {
                            if (match.Index > 0)
                                res.Preview.AddRange(SlicePreviewBefore(
                                    dbValue, match.Index, 0
                                ));
                            res.Preview.Add(new ResultFound.FoundPreview(
                                ResultFound.FoundPreviewType.Term,
                                match.Value
                            ));
                            if (match.Index + match.Value.Length < dbValue.Length)
                                res.Preview.AddRange(SlicePreviewAfter(
                                    dbValue, match.Index + match.Value.Length, dbValue.Length
                                ));
                            return (1, res);
                        }
                        else return (0, null);
                    }
                    catch (Exception e)
                    {
                        Serilog.Log.Debug(e, "Invalid regex query {query}", queryValue);
                        return (0, null);
                    }
                }
                default:
#if DEBUG
                    throw new NotImplementedException($"Unknown case {comparer}");
#else
                    return (0, null);
#endif
            }
        }

        const int MinimumContext = 15;

        const int MaximumContext = 50;

        public static IEnumerable<ResultFound.FoundPreview> SlicePreviewAfter(string text, int start, int maximum)
        {
            int pos = start + MinimumContext;
            for (; pos < maximum; ++pos)
                if (!char.IsLetterOrDigit(text[pos]))
                    break;
            pos = Math.Min(Math.Min(start + MaximumContext, maximum), pos);
            yield return new ResultFound.FoundPreview(
                ResultFound.FoundPreviewType.Context,
                text.Substring(start, pos - start)
            );
            if (pos < maximum)
                yield return new ResultFound.FoundPreview(ResultFound.FoundPreviewType.Ellipsis, null);
        }

        public static IEnumerable<ResultFound.FoundPreview> SlicePreviewBefore(string text, int start, int minimum)
        {
            int pos = start - MinimumContext;
            for (; pos >= minimum; --pos)
                if (!char.IsLetterOrDigit(text[pos]))
                    break;
            pos = Math.Max(Math.Max(start - MaximumContext, minimum), pos);
            if (pos > minimum)
                yield return new ResultFound.FoundPreview(ResultFound.FoundPreviewType.Ellipsis, null);
            yield return new ResultFound.FoundPreview(
                ResultFound.FoundPreviewType.Context,
                text.Substring(pos, start - pos)
            );
        }
    }
}