/*
 * Author: Nikolay Dvurechensky
 * Site: https://www.dvurechensky.pro/
 * Gmail: dvurechenskysoft@gmail.com
 * Last Updated: 12 мая 2025 06:51:53
 * Version: 1.0.7
 */

using FileSearch.Logic.Model.EncodingDetection;
using FileSearch.Logic.Model.Engine;
using System.Text;

namespace FileSearch.Logic.Model.CriterionSchemas
{
    internal class ContentCriterion : CriterionBase, ICriterion
    {
        private const int BufferSize = 32 * 1024; // 32KB

        private readonly string _text;
        private readonly char[][] _textInChars;
        private readonly bool _ignoreCase;
        private readonly bool _matchFullWords;
        private readonly IEncodingFactory _encodingFactory;

        public ContentCriterion(string text, bool ignoreCase, bool matchFullWords, IEncodingFactory encodingFactory)
        {
            if (text == null) throw new ArgumentNullException("text");
            if (encodingFactory == null) throw new ArgumentNullException("encodingFactory");

            _text = text;
            _ignoreCase = ignoreCase;
            _matchFullWords = matchFullWords;
            _encodingFactory = encodingFactory;

            _textInChars = StringToCharArrays(text, ignoreCase);
        }

        public string Name { get { return "File content"; } }

        public CriterionWeight Weight { get { return CriterionWeight.Heavy; } }

        public bool DirectorySupport { get { return false; } }

        public bool FileSupport { get { return true; } }

        public bool IsMatch(FileSystemInfo fileSystemInfo, ICriterionContext context)
        {
            var fileInfo = (FileInfo)fileSystemInfo;

            var buffer = new byte[BufferSize];
            var textLength = _text.Length;

            Encoding[] encodings = new Encoding[1];

            // Проверить несколько кодировок
            for (int encodingIndex = 0; encodingIndex < encodings.Length; encodingIndex++)
            {
                // Кодирование текущего цикла. Первая попытка будет NULL.
                Encoding encoding = encodings[encodingIndex];
                bool characterShouldBeNonWord = false;
                char characterBefore = '\0';

                using (var stream = fileInfo.OpenRead())
                {
                    int length;
                    int foundMatchingSymbols = 0;
                    while ((length = stream.Read(buffer, 0, BufferSize)) > 0)
                    {
                        // Если кодировка еще не определена, определите ее сейчас.
                        if (encoding == null)
                        {
                            encodings = _encodingFactory.DetectEncoding(buffer);
                            encoding = encodings[encodingIndex];
                        }

                        var currentString = encoding.GetString(buffer, 0, length);
                        var currentStringLength = currentString.Length; // Кэш

                        bool startAtBegin = foundMatchingSymbols > 0;
                        var charIndex = 0;

                        // Первый символ должен быть символом, отличным от слова, если предыдущие прочитанные байты заканчиваются соответствующей строкой.
                        if (_matchFullWords && characterShouldBeNonWord && currentStringLength > 0)
                        {
                            characterShouldBeNonWord = false;
                            if (!CharIsWordChar(currentString[0]))
                                return true;
                        }

                        // Проверьте, находится ли первый или следующий совпадающий символ в текущей строке.
                        if ((charIndex = currentString.IndexOfAny(_textInChars[foundMatchingSymbols], charIndex)) >= 0)
                        {
                            do
                            {
                                // Назначьте персонажа заранее.
                                if (charIndex > 0 && foundMatchingSymbols == 0 && _matchFullWords)
                                    characterBefore = currentString[charIndex - 1];

                                // Не первый символ _text, поэтому проверьте, находится ли второй символ в первой позиции.
                                if (startAtBegin)
                                {
                                    startAtBegin = false;
                                    if (charIndex > 0) // Буква должна быть на первой позиции! Если нет, начните заново с первого символа в _text.
                                    {
                                        foundMatchingSymbols = 0;
                                        continue;
                                    }
                                }

                                // Скопируйте переменную, чтобы она не была изменена в приведенном ниже цикле.
                                var current = charIndex;
                                // Постарайтесь сопоставить как можно больше символов.
                                while (++foundMatchingSymbols < textLength && currentStringLength > ++current && (currentString[current] == _text[foundMatchingSymbols] || _ignoreCase && _textInChars[foundMatchingSymbols].Any(c => c == currentString[current]))) ;
                                // Нашел!
                                if (foundMatchingSymbols == textLength && (!_matchFullWords || !CharIsWordChar(characterBefore)))
                                {
                                    if (_matchFullWords)
                                    {
                                        // Попытайтесь определить следующее чтение, заканчивается ли строка символом, отличным от слова.
                                        if (current >= currentStringLength - 1)
                                            characterShouldBeNonWord = true;
                                        // Проверьте, не является ли следующая буква словом char. Если нет, верните true. В противном случае убедитесь, что индекс равен +1, чтобы он был сброшен в следующем операторе IF.
                                        else if (!CharIsWordChar(currentString[++current]))
                                            return true;
                                    }
                                    else
                                        // Возвращает true, если не проверяется слово вместо части строки.
                                        return true;
                                }
                                // Сбросить счетчик совпадающих символов, если конец текущей строки не достигнут. Если да, продолжайте тестирование при следующем чтении.
                                if (currentStringLength != current)
                                    foundMatchingSymbols = 0;
                                // Проверьте, есть ли следующий соответствующий символ в текущей строке.
                            } while ((charIndex = currentString.IndexOfAny(_textInChars[foundMatchingSymbols], ++charIndex)) >= 0);

                            // Назначьте последний символ, чтобы в следующем раунде он знал предыдущий символ.
                            if (foundMatchingSymbols == 0 && _matchFullWords)
                                characterBefore = currentString[currentStringLength - 1];
                        }
                        else
                        {
                            foundMatchingSymbols = 0;
                            // Переназначьте предыдущий символ последнему символу в строке.
                            if (_matchFullWords) characterBefore = currentString[currentStringLength - 1];
                        }
                    }
                    if (_matchFullWords && characterShouldBeNonWord)
                        return true;
                }
            }
            return false;
        }

        private static bool CharIsWordChar(char c)
        {
            return char.IsLetterOrDigit(c) || c == '_';
        }

        private static char[][] StringToCharArrays(string input, bool ignoreCase)
        {
            var list = new List<char[]>();
            foreach (var c in input)
            {
                if (!ignoreCase || !char.IsLetter(c))
                    list.Add(new[] { c });
                else
                {
                    var u = char.ToUpperInvariant(c);
                    var l = char.ToLowerInvariant(c);
                    list.Add(u != l ? new[] { u, l } : new[] { c });
                }
            }
            return list.ToArray();
        }
    }
}
