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


namespace Unity.Burst.Editor
{
    internal struct SearchCriteria
    {
        internal string filter;
        internal bool isCaseSensitive;
        internal bool isWholeWords;
        internal bool isRegex;

        internal SearchCriteria(string keyword, bool caseSensitive, bool wholeWord, bool regex)
        {
            filter = keyword;
            isCaseSensitive = caseSensitive;
            isWholeWords = wholeWord;
            isRegex = regex;
        }

        internal bool Equals(SearchCriteria obj) =>
            filter == obj.filter && isCaseSensitive == obj.isCaseSensitive && isWholeWords == obj.isWholeWords && isRegex == obj.isRegex;

        public override bool Equals(object obj) =>
            obj is SearchCriteria other && Equals(other);

        public override int GetHashCode() => base.GetHashCode();
    }

    internal static class BurstStringSearch
    {
        /// <summary>
        /// Gets index of line end in given string, both absolute and relative to start of line.
        /// </summary>
        /// <param name="str">String to search in.</param>
        /// <param name="line">Line to get end index of.</param>
        /// <returns>(absolute line end index of string, line end index relative to line start).</returns>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Argument must be greater than 0 and less than or equal to number of lines in
        /// <paramref name="str" />.
        /// </exception>
        internal static (int total, int relative) GetEndIndexOfPlainLine (string str, int line)
        {
            var lastIdx = -1;
            var newIdx = -1;

            for (var i = 0; i <= line; i++)
            {
                lastIdx = newIdx;
                newIdx = str.IndexOf('\n', lastIdx + 1);

                if (newIdx == -1 && i < line)
                {
                    throw new ArgumentOutOfRangeException(nameof(line),
                        "Argument must be greater than 0 and less than or equal to number of lines in str.");
                }
            }
            lastIdx++;
            return newIdx != -1 ? (newIdx, newIdx - lastIdx) : (str.Length - 1, str.Length - 1 - lastIdx);
        }

        /// <summary>
        /// Gets index of line end in given string, both absolute and relative to start of line.
        /// Adjusts the index so color tags are not included in relative index.
        /// </summary>
        /// <param name="str">String to search in.</param>
        /// <param name="line">Line to find end of in string.</param>
        /// <returns>(absolute line end index of string, line end index relative to line start adjusted for color tags).</returns>
        internal static (int total, int relative) GetEndIndexOfColoredLine(string str, int line)
        {
            var (total, relative) = GetEndIndexOfPlainLine(str, line);
            return RemoveColorTagFromIdx(str, total, relative);
        }

        /// <summary>
        /// Adjusts index of color tags on line.
        /// </summary>
        /// <remarks>Assumes that <see cref="tidx"/> is index of something not a color tag.</remarks>
        /// <param name="str">String containing the indexes.</param>
        /// <param name="tidx">Total index of line end.</param>
        /// <param name="ridx">Relative index of line end.</param>
        /// <returns>(<see cref="tidx"/>, <see cref="ridx"/>) adjusted for color tags on line.</returns>
        private static (int total, int relative) RemoveColorTagFromIdx(string str, int tidx, int ridx)
        {
            var lineStartIdx = tidx - ridx;
            var colorTagFiller = 0;

            var tmp = str.LastIndexOf("</color", tidx);
            var lastWasStart = true;
            var colorTagStart = str.LastIndexOf("<color=", tidx);

            if (tmp > colorTagStart)
            {
                // color tag end was closest
                lastWasStart = false;
                colorTagStart = tmp;
            }

            while (colorTagStart != -1 && colorTagStart >= lineStartIdx)
            {
                var colorTagEnd = str.IndexOf('>', colorTagStart);
                // +1 as the index is zero based.
                colorTagFiller += colorTagEnd - colorTagStart + 1;

                if (lastWasStart)
                {
                    colorTagStart = str.LastIndexOf("</color", colorTagStart);
                    lastWasStart = false;
                }
                else
                {
                    colorTagStart = str.LastIndexOf("<color=", colorTagStart);
                    lastWasStart = true;
                }
            }
            return (tidx - colorTagFiller, ridx - colorTagFiller);
        }

        /// <summary>
        /// Finds the zero indexed line number of given <see cref="matchIdx"/>.
        /// </summary>
        /// <param name="str">String to search in.</param>
        /// <param name="matchIdx">Index to find line number of.</param>
        /// <returns>Line number of given index in string.</returns>
        internal static int FindLineNr(string str, int matchIdx)
        {
            var lineNr = 0;
            var idxn = str.IndexOf('\n');

            while (idxn != -1 && idxn < matchIdx)
            {
                lineNr++;
                idxn = str.IndexOf('\n', idxn + 1);
            }

            return lineNr;
        }

        /// <summary>
        /// Finds first match of <see cref="criteria"/> in given string.
        /// </summary>
        /// <param name="str">String to search in.</param>
        /// <param name="criteria">Search options.</param>
        /// <param name="regx">Used when <see cref="criteria"/> specifies regex search.</param>
        /// <param name="startIdx">Index to start the search at.</param>
        /// <returns>(start index of match, length of match)</returns>
        internal static (int idx, int length) FindMatch(string str, SearchCriteria criteria, Regex regx, int startIdx = 0)
        {
            var idx = -1;
            var len = 0;

            if (criteria.isRegex)
            {
                // regex will have the appropriate options in it if isCaseSensitive or/and isWholeWords is true.
                var res = regx.Match(str, startIdx);

                if (res.Success) (idx, len) = (res.Index, res.Length);
            }
            else if (criteria.isWholeWords)
            {
                (idx, len) = (IndexOfWholeWord(str, startIdx, criteria.filter, criteria.isCaseSensitive
                    ? StringComparison.InvariantCulture
                    : StringComparison.InvariantCultureIgnoreCase), criteria.filter.Length);
            }
            else
            {
                unsafe
                {
                    fixed (char* source = str)
                    {
                        fixed (char* target = criteria.filter)
                        {
                            (idx, len) = (
                                IndexOfCustom(source, str.Length, target, criteria.filter.Length, startIdx, criteria.isCaseSensitive)
                                , criteria.filter.Length);
                        }
                    }
                }
            }

            return (idx, len);
        }

        internal static List<(int idx, int length)> FindAllMatches(string str, SearchCriteria criteria, Regex regx,
            int startIdx = 0)
        {
            var retVal = new List<(int, int)>();

            if (criteria.isRegex)
            {
                var res = regx.Matches(str, startIdx);

                foreach (Match match in res)
                {
                    retVal.Add((match.Index, match.Length));
                }
            }
            else if (criteria.isWholeWords)
            {
                retVal.AddRange(IndexOfWholeWordAll(str, startIdx, criteria.filter,
                    criteria.isCaseSensitive
                        ? StringComparison.InvariantCulture
                        : StringComparison.CurrentCultureIgnoreCase));
            }
            else
            {
                unsafe
                {
                    fixed (char* source = str)
                    {
                        fixed (char* target = criteria.filter)
                        {
                            retVal.AddRange(FindAllIndices(source, str.Length, target, criteria.filter.Length, startIdx, criteria.isCaseSensitive));
                        }
                    }
                }
            }

            return retVal;
        }

        private static char ToUpper(char c) => c - 97U > 25U ? c : (char)(c - 32U);

        private static unsafe int ScanForFilterInsensitive(char* str, char* filter, int flen, int i)
        {
            int j = 0;
            while (j < flen && ToUpper(str[i + j]) == ToUpper(filter[j]))
            {
                j++;
            }
            return j;
        }

        private static unsafe int ScanForFilter(char* str, char* filter, int flen, int i)
        {
            int j = 0;
            while (j < flen && str[i + j] == filter[j])
            {
                j++;
            }
            return j;
        }

        private static unsafe List<(int, int)> FindAllIndices(char* str, int len, char* filter, int flen, int startIdx, bool caseSensitive)
        {
            var retVal = new List<(int,int)>();
            if (len < flen) { return retVal; }

            int stop = len - flen;
            if (caseSensitive)
            {
                for (int i = startIdx; i < stop; i++)
                {
                    if (ScanForFilter(str, filter, flen, i) == flen)
                    {
                        retVal.Add((i, flen));
                        i += flen - 1;
                    }
                }
            }
            else
            {
                for (int i = startIdx; i < stop; i++)
                {
                    if (ScanForFilterInsensitive(str, filter, flen, i) == flen)
                    {
                        retVal.Add((i, flen));
                        i += flen-1;
                    }
                }
            }
            return retVal;
        }


        /// <summary>
        /// Finds index of first occurence of <see cref="filter"/> in <see cref="str"/>.
        /// </summary>
        /// <param name="str">String to search through</param>
        /// <param name="len">Length of <see cref="str"/></param>
        /// <param name="filter">Needle to find</param>
        /// <param name="flen">Lenght of <see cref="filter"/></param>
        /// <param name="startIdx">Index to start search from</param>
        /// <param name="caseSensitive">Whether search ignore casing</param>
        /// <returns>index of first match or -1</returns>
        private static unsafe int IndexOfCustom(char* str, int len, char* filter, int flen, int startIdx, bool caseSensitive)
        {
            if (len < flen) { return -1; }

            int stop = len - flen;
            if (caseSensitive)
            {
                for (int i = startIdx; i < stop; i++)
                {
                    if (ScanForFilter(str, filter, flen, i) == flen)
                    {
                        return i;
                    }
                }
            }
            else
            {
                for (int i = startIdx; i < stop; i++)
                {
                    if (ScanForFilterInsensitive(str, filter, flen, i) == flen)
                    {
                        return i;
                    }
                }
            }

            return -1;
        }

        /// <summary>
        /// Finds index of <see cref="filter"/> matching for whole words.
        /// </summary>
        /// <param name="str">String to search in.</param>
        /// <param name="startIdx">Index to start search from.</param>
        /// <param name="filter">Key to search for.</param>
        /// <param name="opt">Options for string comparison.</param>
        /// <returns>Index of match or -1.</returns>
        private static int IndexOfWholeWord(string str, int startIdx, string filter, StringComparison opt)
        {
            const string wholeWordMatch = @"\w";

            var j = startIdx;
            var filterLen = filter.Length;
            var strLen = str.Length;
            while (j < strLen && (j = str.IndexOf(filter, j, opt)) >= 0)
            {
                var noPrior = true;
                if (j != 0)
                {
                    var frontBorder = str[j - 1];
                    noPrior = !Regex.IsMatch(frontBorder.ToString(), wholeWordMatch);
                }

                var noAfter = true;
                if (j + filterLen != strLen)
                {
                    var endBorder = str[j + filterLen];
                    noAfter = !Regex.IsMatch(endBorder.ToString(), wholeWordMatch);
                }

                if (noPrior && noAfter) return j;

                j++;
            }
            return -1;
        }


        private static List<(int idx, int len)> IndexOfWholeWordAll(string str, int startIdx, string filter, StringComparison opt)
        {
            const string wholeWordMatch = @"\w";
            var retVal = new List<(int, int)>();

            var j = startIdx;
            var filterLen = filter.Length;
            var strLen = str.Length;
            while (j < strLen && (j = str.IndexOf(filter, j, opt)) >= 0)
            {
                var noPrior = true;
                if (j != 0)
                {
                    var frontBorder = str[j - 1];
                    noPrior = !Regex.IsMatch(frontBorder.ToString(), wholeWordMatch);
                }

                var noAfter = true;
                if (j + filterLen != strLen)
                {
                    var endBorder = str[j + filterLen];
                    noAfter = !Regex.IsMatch(endBorder.ToString(), wholeWordMatch);
                }

                if (noPrior && noAfter)
                {
                    retVal.Add((j, filterLen));
                    j += filterLen - 1;
                }

                j++;
            }
            return retVal;
        }
    }
}