-- import addon read namespace from global env
local _G = _G
local Grichelde = _G.Grichelde or {}

local IsAddOnLoaded, assert, nilOrEmpty, pairs, ipairs, spairs, tContains, tFilter, tInsert, tConcat, tSize, tIsEmpty, find, sub, gsub, gmatch, getNextCharUtf8, isLetter, isUpper, isLower, toUpper, toLower, capitalize, bytes2Char, trim, length, lengthUtf8
    = Grichelde.F.IsAddOnLoaded, Grichelde.F.assert, Grichelde.F.nilOrEmpty, Grichelde.F.pairs, Grichelde.F.ipairs, Grichelde.F.spairs, Grichelde.F.tContains, Grichelde.F.tFilter, Grichelde.F.tInsert, Grichelde.F.tConcat, Grichelde.F.tSize, Grichelde.F.tIsEmpty,
      Grichelde.F.find, Grichelde.F.sub, Grichelde.F.gsub, Grichelde.F.gmatch, Grichelde.F.getNextCharUtf8, Grichelde.F.isLetter, Grichelde.F.isUpper, Grichelde.F.isLower, Grichelde.F.toUpper, Grichelde.F.toLower, Grichelde.F.capitalize, Grichelde.F.bytes2Char, Grichelde.F.trim, Grichelde.F.length, Grichelde.F.lengthUtf8

--- Splits a long text in longest possible chunks of <= 255 length, split at last available space
-- @param text string
-- @return array of chunks
function Grichelde:SplitText(text)
    local chunks = {}
    local leftGuillemet = bytes2Char(194, 171) .. " "
    local rightGuillemet = " " .. bytes2Char(194, 187)
    local chunkSize = Grichelde.INPUT_LIMIT - length(leftGuillemet) - length(rightGuillemet)

    local function preserveText(newText, chunk, blockText, posEnd)
        -- link found, block completed
        self:TracePrint("SplitText : Found preservable text up to %s", posEnd)
        local preserved = sub(newText, 1, posEnd)

        if ((length(chunk) > 0) and (length(chunk .. blockText) > chunkSize)) then
            -- block exceeds chunk, chunkify previous blocks
            self:DebugPrint("SplitText : add chunk:", chunk)
            tInsert(chunks, chunk .. rightGuillemet)
            chunk = leftGuillemet .. trim(blockText)
        else
            chunk = chunk .. blockText
        end

        if ((length(chunk) > 0) and (length(chunk .. preserved) > chunkSize)) then
            -- block exceeds chunk, chunkify previous blocks
            self:DebugPrint("SplitText : add chunk:", chunk)
            tInsert(chunks, chunk .. rightGuillemet)
            chunk = leftGuillemet .. trim(preserved)
        else
            chunk = chunk .. preserved
        end

        blockText = ""
        newText = sub(newText, posEnd + 1)

        return newText, chunk, blockText, posEnd
    end

    if (length(text or "") <= Grichelde.INPUT_LIMIT) then
        self:DebugPrint("SplitText : no chunk:", text)
        tInsert(chunks, text)
    else
        local lookAheads = { '|', '*', '<', '%', '{', '(', 'o' }

        local newText = text or ""
        local chunk, blockText = "", ""
        local currentChar
        local escape = 0

        -- must not enforce UTF-8 support here, as the positions are used
        while ((length(newText) > 0) and (escape < Grichelde.ENDLESS_LOOP_LIMIT)) do
            escape = escape + 1
            local previousChar = currentChar
            local first, textAhead = getNextCharUtf8(newText)
            currentChar = first
            self:DebugPrint("SplitText : currentChar, escape: %s, %s", currentChar, escape)
            self:TracePrint("SplitText : chunk:", chunk)
            self:TracePrint("SplitText : newText:", newText)

            -- as there is not OR in Luas pattern matching, search for all of the exclude patterns after another is
            -- cumbersome and inefficient -> look for each char consecutively if it matches the starting pattern only
            -- and if if matches do full pattern matching
            if (currentChar == ' ') then
                self:TracePrint("SplitText : block completed")

                if ((length(chunk) > 0) and (length(chunk .. blockText) > chunkSize)) then
                    -- block exceeds chunk, chunkify previous blocks
                    self:DebugPrint("SplitText : add chunk:", chunk)
                    tInsert(chunks, chunk .. rightGuillemet)
                    chunk = leftGuillemet .. trim(blockText)
                else
                    chunk = chunk .. blockText
                end

                blockText = currentChar
                newText = textAhead
            elseif (tContains(lookAheads, currentChar)) then
                -- lookahead-check for all preservable patterns (itemLinks, textures, emotes, ooc, etc.)

                -- link detection
                local linkPosEnd = self:CheckForLink(newText, currentChar)
                if (linkPosEnd ~= nil) then
                    -- link found, block completed
                    newText, chunk, blockText = preserveText(newText, chunk, blockText, linkPosEnd)
                else
                    -- substitution detection
                    local substPosEnd = self:CheckForSubstitutions(newText, currentChar)
                    if (substPosEnd ~= nil) then
                        -- substitution found, block completed
                        newText, chunk, blockText = preserveText(newText, chunk, blockText, substPosEnd)
                    else
                        -- raid target marker detection
                        local rtmPosEnd = self:CheckForRaidTargetMarkers(newText, currentChar)
                        if (rtmPosEnd ~= nil) then
                            -- raid target marker found, block completed
                            newText, chunk, blockText = preserveText(newText, chunk, blockText, rtmPosEnd)
                        else
                            blockText = blockText .. currentChar
                            newText = textAhead
                        end
                    end
                end
            else
                blockText = blockText .. currentChar
                newText = textAhead
            end
        end

        self:TracePrint("SplitText : main loop completed")
        if (length(chunk .. blockText) > 0) then
            -- catchup remaining text at the end
            if (length(chunk .. blockText) > chunkSize) then
                -- block exceeds chunk, chunkify previous blocks
                if (length(chunk) > 0) then
                    self:DebugPrint("SplitText : add chunk:", chunk)
                    tInsert(chunks, chunk .. rightGuillemet)
                    chunk = leftGuillemet .. trim(blockText)
                else
                    chunk = chunk .. blockText
                end
            else
                chunk = chunk .. blockText
            end

            self:DebugPrint("SplitText : last chunk:", chunk)
            -- sub(chunk, 1, 255) can result in broken UTF8 chars and error message
            tInsert(chunks, chunk)
        end
    end

    return chunks
end

--- Send text in chunks if length exceeds 255 bytes after replacement.
function Grichelde:SendChunkifiedChatMessage(message, ...)
    if (length(message) > 255) then
        local chunks = self:SplitText(message)
        self:DebugPrint("SendChunkifiedChatMessage : #chunks:", #chunks)

        for _, chunk in ipairs(chunks) do
            self.hooks["SendChatMessage"](chunk, ...);
        end
    else
        self.hooks["SendChatMessage"](message, ...);
    end
end

function Grichelde:ReplaceCharacters(text, replName, replTable, consolidate, replacedTexts)
    local function convertToCaseInsensitivePattern(pattern)
        local ciPattern = ""
        local ignored = {'^', '$', '(', ')', '.'}
        local quantifiers = {'*', '+', '-', '?'}
        local p, patRest = getNextCharUtf8(pattern)
        local escape = 0

        while ((p ~= nil) and (escape < Grichelde.ENDLESS_LOOP_LIMIT)) do
            escape = escape + 1
            if (tContains(ignored, p) or tContains(quantifiers, p)) then
                -- ignore
                ciPattern = ciPattern .. p
            elseif (p == "%") then
                -- ignore capture references
                p, patRest = getNextCharUtf8(patRest)
                if (p ~= nil) then
                    ciPattern = ciPattern .. "%" .. p
                end
            elseif (p == "[") then
                -- skip pattern sets
                ciPattern = ciPattern .. "["
                p, patRest = getNextCharUtf8(patRest)
                while ((p ~= nil) and (p ~= "]")) do
                    if (p == "%") then
                        -- ignore capture references
                        p, patRest = getNextCharUtf8(patRest)
                        if (p ~= nil) then
                            ciPattern = ciPattern .. "%" .. p
                        end
                    else
                        local upperP, lowerP = toUpper(p), toLower(p)
                        if (upperP ~= lowerP) then
                            ciPattern = ciPattern .. upperP .. lowerP
                        else
                            ciPattern = ciPattern .. p
                        end
                    end
                    p, patRest = getNextCharUtf8(patRest)
                end
                ciPattern = ciPattern .. "]"
            else
                ciPattern = ciPattern .. "[" ..  Grichelde.F.toUpper(p) .. Grichelde.F.toLower(p) .. "]"
            end

            p, patRest = getNextCharUtf8(patRest)
        end
        self:TracePrint("convertToCaseInsensitivePattern : %s => %s", pattern, ciPattern)
        return ciPattern
    end

    local function replaceCaptures(text, replaceText, captures)
        local replText = replaceText
        self:TracePrint("replaceCaptures : text: %s, #captures: %d", text, #captures)

        if (#captures > 0) then
            for i, cap in ipairs(captures) do
                --self:TracePrint("replaceCaptures : i: %d, cap: %s", i, cap)
                if (cap == nil) then
                    break
                else
                    local oldRepl = replText
                    replText = gsub(oldRepl, "%%" .. i, cap)
                    self:TracePrint("ReplaceCaptures : substitute capture %s: %s => %s", oldRepl, cap, replText)
                end
            end
        else
            self:TracePrint("ReplaceCaptures : no captures")
        end
        return replText
    end

    --- this is more complicated to get it right than it looks like
    local function applyCase(replRest, lastCase, nextCase)
        local repl = ""
        if (lastCase == nil) then
            -- lastCase was unknown, always take over nextCase
            if (nextCase == nil) then
                repl = repl .. replRest
            elseif (nextCase == true) then
                --repl = repl .. toUpper(replRest)
                repl = repl .. replRest
            else
                --repl = repl .. toLower(replRest)
                repl = repl .. replRest
            end
        elseif (lastCase == true) then
            -- lastCase was UPPER
            if (nextCase == nil) then
                repl = repl .. toUpper(replRest)
            elseif (nextCase == true) then
                repl = repl .. toUpper(replRest)
            else
                repl = repl .. replRest
            end
        else
            -- lastCase was lower
            if (nextCase == nil) then
                repl = repl .. replRest
            elseif (nextCase == true) then
                --repl = repl .. toLower(replRest)
                repl = repl .. replRest
            else
                --repl = repl .. toLower(replRest)
                repl = repl .. replRest
            end
        end
        return repl
    end

    local pos = 1
    local result = text
    local findText = result
    local searchText = replTable.searchText
    local replaceText = replTable.replaceText
    local matchWhen = replTable.matchWhen
    local doExactCase = replTable.exactCase
    local doConsolidate = replTable.consolidate
    local doStopOnMatch = replTable.stopOnMatch
    local stopOnMatch = false

    if doExactCase then
        self:DebugPrint("ReplaceCharacters : \"%s => %s\" (exact case)", searchText, replaceText)
    else
        self:DebugPrint("ReplaceCharacters : \"%s => %s\" (ignoreCase)", searchText, replaceText)
        searchText = convertToCaseInsensitivePattern(searchText)
    end

    local pos1, pos2, cap1, cap2, cap3, cap4, cap5, cap6, cap7, cap8, cap9 = find(findText, searchText, pos)
    self:TracePrint("ReplaceCharacters : pos1: %d, pos2: %d", pos1, pos2)
    self:TracePrint("ReplaceCharacters : cap1: %s, cap2: %s, cap3: %s, cap4: %s, cap5: %s, cap6: %s, cap7: %s, cap8: %s, cap9: %s", cap1, cap2, cap3, cap4, cap5, cap6, cap7, cap8, cap9)
    while (pos1 ~= nil) and (pos2 ~= nil) and (pos1 <= pos2) do
        -- continue from that position later
        pos = pos2 + 1

        if doStopOnMatch then
            stopOnMatch = true
        end

        local pre = sub(result, 1, pos1 - 1)
        local match = sub(result, pos1, pos2)
        local post = sub(result, pos2 + 1)
        local wordStart = sub(pre, -1, -1)
        local wordEnd = sub(post, 1, 1)

        -- additional checks for word boundaries
        local doesMatchWhen = false
        if (matchWhen == 2) then
            -- replace always
            doesMatchWhen = true
        elseif (matchWhen == 3) then
            -- replace only as a whole word
            if (nilOrEmpty(wordStart) or (find(wordStart,"[%s%p]") ~= nil)) and (nilOrEmpty(wordEnd) or (find(wordEnd, "[%s%p]") ~= nil)) then
                doesMatchWhen = true
            end
        elseif (matchWhen == 4) then
            -- replace only at start
            if (nilOrEmpty(wordStart) or (find(wordStart,"[%s%p]") ~= nil)) then
                doesMatchWhen = true
            end
        elseif (matchWhen == 5) then
            -- replace only at end
            if (nilOrEmpty(wordEnd) or (find(wordEnd, "[%s%p]") ~= nil)) then
                doesMatchWhen = true
            end
        elseif (matchWhen == 6) then
            -- replace only at start or end
            if (nilOrEmpty(wordStart) or (find(wordStart, "[%s%p]") ~= nil) or nilOrEmpty(wordEnd) or (find(wordEnd, "[%s%p]") ~= nil)) then
                doesMatchWhen = true
            end
        elseif (matchWhen == 7) then
            -- replace only in the middle
            if (not nilOrEmpty(wordStart) and (find(wordStart, "[%w]") ~= nil) and not nilOrEmpty(wordEnd) and (find(wordEnd, "[%w]") ~= nil)) then
                doesMatchWhen = true
            end
        end

        if (doesMatchWhen) then
            -- replace substitutions
            self:TracePrint("ReplaceCharacters : pre: %s, match: %s, post: %s, repl: %s", pre, match, post, replaceText)
            local caps = { cap1, cap2, cap3, cap4, cap5, cap6, cap7, cap8, cap9 }
            local replText = replaceCaptures(match, replaceText, caps)
            tInsert(replacedTexts[replName], replText)
            self:DebugPrint("ReplaceCharacters : pre: %s, match: %s, post: %s, repl: %s", pre, match, post, replText)

            if (not doExactCase) then
                local repl, lastCase = "", nil
                local c, matchRest = getNextCharUtf8(match)
                local r, replRest = "", replText

                while (c ~= nil) do
                    r, replRest = getNextCharUtf8(replRest)
                    r = r or ""
                    replRest = replRest or ""
                    self:TracePrint("ReplaceCharacters : c: %s, rest: %s", c, matchRest)
                    self:TracePrint("ReplaceCharacters : r: %s, rest: %s", r, replRest)

                    if (isUpper(c)) then
                        -- UPPER-CASE letter
                        self:TracePrint("ReplaceCharacters : characters: %s => %s", c, toUpper(r))
                        lastCase = true
                        repl = repl .. toUpper(r)
                    elseif (isLower(c)) then
                        -- lower_case letter
                        self:TracePrint("ReplaceCharacters : characters: %s => %s", c, toLower(r))
                        lastCase = false
                        repl = repl .. toLower(r)
                    else
                        -- no letter
                        self:TracePrint("ReplaceCharacters : characters: %s => %s", c, r)
                        lastCase = nil
                        repl = repl .. r
                    end

                    c, matchRest = getNextCharUtf8(matchRest)
                end

                self:TracePrint("ReplaceCharacters : remaining length %d", length(replRest))
                if (length(replRest) > 0) then
                    local nextLetter, _ = getNextCharUtf8(post)
                    nextLetter = nextLetter or ""

                    local nextCase = nil
                    if (isUpper(nextLetter)) then
                        nextCase = true
                    elseif (isLower(nextLetter)) then
                        nextCase = false
                    end

                    self:TracePrint("ReplaceCharacters : rest: %s, lastCase: %s, nextLetter: %s, nextCase: %s", replRest, lastCase, nextLetter, nextCase)

                    repl = repl .. applyCase(replRest, lastCase, nextCase)
                end

                replText = repl
            end

            -- actual replacement
            result = pre .. replText .. post
            self:TracePrint("ReplaceCharacters : result: %s", result)

            -- remember positions for consolidate
            if doConsolidate then
                tInsert(consolidate[replName], pos1)
            end

            -- update previous consolidate markers
            local diff = lengthUtf8(replText) - lengthUtf8(match)
            self:TracePrint("ReplaceCharacters : diff = %d - %d", lengthUtf8(replText), lengthUtf8(match))
            for key, posList in pairs(consolidate) do
                if (key ~= replName) then
                    for i, pc in ipairs(posList) do
                        if (pos1 < pc) then
                            consolidate[key][i] = consolidate[key][i] + diff
                        end
                    end
                end
            end

            -- replacement text can lengthen or shorten the result
            -- after replacement text and lowerText can have different sizes
            pos = pos + diff
        else
            self:DebugPrint("ReplaceCharacters : does not match when: %d", matchWhen)
        end

        findText = result
        -- update values for next iteration
        pos1, pos2, cap1, cap2, cap3, cap4, cap5, cap6, cap7, cap8, cap9 = find(findText, searchText, pos)
        self:TracePrint("ReplaceCharacters : pos1: %d, pos2: %d", pos1, pos2)
        self:TracePrint("ReplaceCharacters : cap1: %s, cap2: %s, cap3: %s, cap4: %s, cap5: %s, cap6: %s, cap7: %s, cap8: %s, cap9: %s", cap1, cap2, cap3, cap4, cap5, cap6, cap7, cap8, cap9)
    end

    if (text ~= result) then
        self:DebugPrint("ReplaceCharacters : replaced \"%s\" with \"%s\" %s", text, result, stopOnMatch and "(stop)" or "")
    end
    if (matchWhen > 1) and doConsolidate then
        self:DebugPrint("ReplaceCharacters : consolidate[" .. replName .. "] is:")
        self:DebugPrint(consolidate[replName])
    end

    return result, stopOnMatch
end

--- Replaces all character occurrences for which replacements have been defined in the options
-- @param text string
-- @param replacements table of mappings
-- @return string
function Grichelde:ReplaceAndConsolidate(text, replacements)
    local replacements = replacements or self.db.profile.replacements or {}
    self:TracePrint("ReplaceAndConsolidate : replacements")
    self:TracePrint(replacements)

    local result = text
    local consolidate = {}
    local replacedTexts = {}
    local stopOnMatch = nil

    -- replacements are done first
    for replName, replTable in spairs(replacements) do
        local searchText = replTable.searchText
        if (not nilOrEmpty(searchText) and (replTable.matchWhen > 1)) then
            consolidate[replName] = {}
            replacedTexts[replName] = {}

            local stop = false
            result, stop = self:ReplaceCharacters(result, replName, replTable, consolidate, replacedTexts)

            if stop then
                stopOnMatch = replName
                self:DebugPrint("ReplaceAndConsolidate : Stopping followup replacements after %s", replName)
                break
            end
        else
            -- empty mapping or never matched
            self:DebugPrint("ReplaceAndConsolidate : Skip replacement %s", replName)
        end
    end

    -- consolidation is done last
    for replName, replTable in spairs(replacements) do
        local before = result
        local search = replTable.searchText

        if (not nilOrEmpty(search) and (replTable.matchWhen > 1)) then
            local lowerResult = toLower(result)
            local offset = 0

            if (replTable.consolidate) then
                self:DebugPrint("consolidate[" .. replName .. "] is:")
                self:DebugPrint(consolidate[replName])
                self:DebugPrint("replacedTexts[" .. replName .. "] is:")
                self:DebugPrint(replacedTexts[replName])

                for i, pos1 in spairs(consolidate[replName]) do
                    local replText = replacedTexts[replName][i]
                    self:DebugPrint("ReplaceAndConsolidate : consolidating \"%s => %s\"", search, replText)

                    local pos2 = pos1 + length(replText) - 1
                    self:TracePrint("ReplaceAndConsolidate : pos1: %d, pos2: %d", pos1, pos2)
                    local match = toLower(replText)
                    local next = sub(lowerResult, pos2 + 1, pos2 + 1 + pos2 - pos1)
                    self:TracePrint("ReplaceAndConsolidate : match: %s, next: %s", match, next)

                    local _, p2 = find(next, "^" .. match)
                    self:TracePrint("ReplaceAndConsolidate : p2: %d", p2)
                    if (p2 ~= nil) then
                        result = sub(result, 1, pos2 + offset) .. sub(result, pos2 + 1 + p2 + offset)

                        -- consolidation will shorten the resulting text
                        offset = offset + length(result) - length(lowerResult)
                    end
                    self:DebugPrint("ReplaceAndConsolidate : result: %s", result)
                end
            end

            if (before ~= result) then
                self:DebugPrint("ReplaceAndConsolidate : consolidate \"%s\" with \"%s\"", before, result)
            end
        else
            self:DebugPrint("ReplaceAndConsolidate : Skip consolidation for %s", replName)
        end

        if (stopOnMatch ~= nil) and (stopOnMatch == replName) then
            break
        end
    end

    self:DebugPrint("ReplaceAndConsolidate : final text:", result)
    return result
end

--- looks for colored items, item links or textures
function Grichelde:CheckForLink(text, currentChar)
    if (currentChar == "|") then
        for _, pattern in ipairs(Grichelde.IGNORE_PATTERNS.LINKS) do
            local pos1, pos2 = find(text, "^" .. pattern)
            if (pos1 == 1) and (pos2 ~= nil) then
                local match = sub(text, pos1, pos2)
                self:DebugPrint("CheckForLink : Found link or texture pattern \"%s\" at (%d, %d)", pattern, pos1, pos2)
                return pos2
            end
        end
    end
    return nil
end

--- looks for emotes
function Grichelde:CheckForEmote(text, currentChar, replaceEmotes)
    if (currentChar == "*" or currentChar == "<") then
        for _, pattern in ipairs(Grichelde.IGNORE_PATTERNS.EMOTES) do
            local pos1, pos2 = find(text, "^" .. pattern)
            if (pos1 == 1) and (pos2 ~= nil) then
                local emote = sub(text, pos1, pos2)
                if (not replaceEmotes) then
                    self:DebugPrint("CheckForEmote : Found emote \"%s\" at (%d, %d), but preserved it", emote, pos1, pos2)
                    return pos2
                else
                    self:DebugPrint("CheckForEmote : Processing emote \"%s\" at (%d, %d)", emote, pos1, pos2)
                end
            end
        end
    end
    return nil
end

--- looks for %-substitutions
function Grichelde:CheckForSubstitutions(text, currentChar)
    local lowerText = toLower(text)
    if (currentChar == "%") then
        for _, pattern in ipairs(Grichelde.IGNORE_PATTERNS.SUBSTITUTES) do
            local pos1, pos2 = find(lowerText, "^" .. pattern)
            if (pos1 == 1) and (pos2 ~= nil) then
                self:DebugPrint("CheckForPreversableText : Found substitute pattern \"%s\" at (%d, %d)", pattern, pos1, pos2)
                return pos2
            end
        end
    end
    return nil
end

--- looks for general and localized raid target markers
function Grichelde:CheckForRaidTargetMarkers(text, currentChar)
    local lowerText = toLower(text)
    if (currentChar == "{") then
        -- rt1-9
        local pattern = Grichelde.IGNORE_PATTERNS.RAID_TARGETS[1]
        local pos1, pos2 = find(lowerText, "^" .. pattern)
        if (pos1 == 1) and (pos2 ~= nil) then
            self:DebugPrint("CheckForPreversableText : Found raid target marker \"%s\" at (%d, %d)", pattern, pos1, pos2)
            return pos2
        end

        for _, localizedRT in ipairs(Grichelde.IGNORE_PATTERNS.LOCALIZED_RAID_TARGETS) do
            local translation = toLower(self.L["IgnorePattern_" .. localizedRT])
            local localizedPattern = "^{" .. translation .. "}"
            self:TracePrint("CheckForPreversableText : localizedPattern:", localizedPattern)
            local pos1, pos2 = find(lowerText, localizedPattern)
            if (pos1 == 1) and (pos2 ~= nil) then
                self:DebugPrint("CheckForPreversableText : Found localized raid target marker \"%s\" at (%d, %d)", localizedPattern, pos1, pos2)
                return pos2
            end
        end
    end
    return nil
end

--- looks for ooc with brackets
function Grichelde:CheckForOocBrackets(text, currentChar)
    local lowerText = toLower(text)
    if (currentChar == "(") then
        for _, pattern in ipairs(Grichelde.IGNORE_PATTERNS.OOC_BRACKETS) do
            local pos1, pos2 = find(lowerText, "^" .. pattern)
            if (pos1 == 1) and (pos2 ~= nil) then
                self:DebugPrint("CheckForPreversableText : Found ooc pattern \"%s\" at (%d, %d)", pattern, pos1, pos2)
                return pos2
            end
        end
    end
    return nil
end

--- looks for ooc without brackets
function Grichelde:CheckForOocNoBrackets(text, currentChar, previousChar)
    local lowerText = toLower(text)
    if (currentChar == "o") then
        local pattern = Grichelde.IGNORE_PATTERNS.OOC_NO_BRACKETS[1]
        if ((previousChar == nil) or (find(previousChar, "%s") ~= nil)) and (find(lowerText, pattern) ~= nil) then
            self:DebugPrint("CheckForPreversableText : ooc for remaing text")
            -- remaing text is treated as ooc completely!
            return length(text)
        end
    end
    return nil
end

--- Checks if the text starts with a preversable ignore pattern, such as itemLinks, textures, raid target icons,
--- emotes, ooc or %-substitutons and returns the end location of the match, or 0 if no pattern was found
-- @param text string
-- @param currentChar string(1) current character (first one) of the text, given for performance reasons
-- @param previousChar string(1) previous character of the text, otherwise unreachable
-- @param preserveEmotes boolean ignore replacements for emotes, for testing purposes
-- @return number
function Grichelde:CheckForPreversableText(text, currentChar, previousChar, replaceEmotes)
    self:DebugPrint("CheckForPreversableText : text:", text)
    local replaceEmotes = replaceEmotes or self.db.profile.channels.emote or false

    local linkPos = self:CheckForLink(text, currentChar)
    if (linkPos ~= nil) then
        return linkPos
    end

    local emotePos = self:CheckForEmote(text, currentChar, replaceEmotes)
    if (emotePos ~= nil) then
        return emotePos
    end

    local substPos = self:CheckForSubstitutions(text, currentChar)
    if (substPos ~= nil) then
        return substPos
    end

    local rtmPos = self:CheckForRaidTargetMarkers(text, currentChar)
    if (rtmPos ~= nil) then
        return rtmPos
    end

    local oocBracketPos = self:CheckForOocBrackets(text, currentChar)
    if (oocBracketPos ~= nil) then
        return oocBracketPos
    end

    local oocNoBracketPos = self:CheckForOocNoBrackets(text, currentChar, previousChar)
    if (oocNoBracketPos ~= nil) then
        return oocNoBracketPos
    end

    self:DebugPrint("CheckForPreversableText : no ignore pattern found")
    return 0
end

--- Replaces all character occurrences for which replacements have been defined in the options,
--- while preserving any itemLinks or textures. (http://www.wowwiki.com/ItemLink)
-- @param text string the text to apply the mappings on
-- @param preserveEmotes boolean ignore replacements for emotes, for testing purposes
-- @return string
function Grichelde:ReplaceText(text, replacements, replaceEmotes)
    local lookAheads = { '|', '*', '<', '%', '{', '(', 'o' }
    local newText = text
    local preserveEmotes = replaceEmotes or self.db.profile.channels.emote or false
    local replacements = replacements or self.db.profile.replacements or {}
    local finalText, replaceText = "", ""
    local currentChar
    local escape = 0

    -- must not enforce UTF-8 support here, as the positions are used
    while ((length(newText) > 0) and (escape < Grichelde.ENDLESS_LOOP_LIMIT)) do
        escape = escape + 1
        local previousChar = currentChar
        local first, textAhead = getNextCharUtf8(newText)
        currentChar = first
        self:TracePrint("ReplaceText : currentChar : %s", currentChar)

        -- as there is not OR in Luas pattern matching, search for all of the exclude patterns after another is
        -- cumbersome and inefficient -> look for each char consecutively if it matches the starting pattern only
        -- and if if matches do full pattern matching
        if (tContains(lookAheads, currentChar)) then
            -- lookahead-check for all preservable patterns (itemLinks, textures, emotes, ooc, etc.)
            --local textAhead = sub(newText, current)
            local posEnd = self:CheckForPreversableText(newText, currentChar, previousChar, preserveEmotes)
            if (posEnd > 0) then
                self:DebugPrint("ReplaceText : Found an ignore pattern")

                -- replace all text up until now
                local replacement = self:ReplaceAndConsolidate(replaceText, replacements)
                local preserved = sub(newText, 1, posEnd)

                finalText = finalText .. replacement .. preserved
                replaceText = ""
                newText = sub(newText, posEnd + 1)
                self:DebugPrint("ReplaceText : remaining text", newText)
            else
                -- no corresponding end was found to start pattern, continue loop with next char
                replaceText = replaceText .. currentChar
                newText = textAhead
            end
        else
            replaceText = replaceText .. currentChar
            newText = textAhead
        end
    end

    -- catchup remaining text to the end
    local replacement = self:ReplaceAndConsolidate(replaceText, replacements)
    finalText = finalText .. replacement

    self:DebugPrint("ReplaceText : replaced \"%s\"", text)
    self:DebugPrint("ReplaceText : with        \"%s\"", finalText)
    return finalText
end

function Grichelde:IsOneBigEmote(text)
    local firstWord, _ = self:SplitOnFirstMatch(text)
    assert(firstWord ~= nil, "firstWord is never nil")

    -- emote detection
    local isEmote = false
    local firstChar, rest = getNextCharUtf8(firstWord)

    -- scheme <emote>
    if (firstChar == "<") then
        -- search for emote end
        local _, emoteEnd = find(text, "%>", 2)
        isEmote = (emoteEnd == lengthUtf8(text))
    end
    if (not isEmote and (firstChar == "*")) then
        if (getNextCharUtf8(rest) == "*") then
            -- scheme **emote**
            local _, emoteEnd = find(text, "%*%*", 3)
            isEmote = (emoteEnd == lengthUtf8(text))
        else
            -- scheme *emote*
            local _, emoteEnd = find(text, "%*", 2)
            isEmote = (emoteEnd == lengthUtf8(text))
        end
    end

    -- the whole text is one big emote
    return isEmote
end

function Grichelde:ConvertBlizzTypeToOption(channel)
    local option = tFilter(self.BLIZZ_TYPE_TO_OPTIONS,
        function(_, k, _) return channel == k end)
    if (not tIsEmpty(option)) then
        self:DebugPrint("ConvertBlizzTypeToOption : convert %s to %s", channel, option[1])
        return option[1]
    else
        return nil
    end
end

--- Checks if a message can be replaced according to configuration.
-- @return boolean
function Grichelde:CheckReplacementAllowed(text, channel)
    self:DebugPrint("CheckReplacementAllowed : text:", text)

    -- skip if disabled
    if (not self.db.profile.enabled) then
        self:DebugPrint("CheckReplacementAllowed : disabled")
        return false
    end

    -- skip if no text
    if (nilOrEmpty(text)) then
        return false
    end

    -- skip if wrong channel
    local chan = self:ConvertBlizzTypeToOption(channel)
    local allowedChannels = tFilter(self.db.profile.channels,
        function(_, k, v) return k == chan and v == true end,
        function(_, k, _) return k end
    )
    self:DebugPrint("CheckReplacementAllowed : allowed channels:")
    self:DebugPrint(allowedChannels)

    if (tIsEmpty(allowedChannels)) then
        self:DebugPrint("CheckReplacementAllowed : skip channel type:", chan)
        return false
    end

    local firstWord, _ = self:SplitOnFirstMatch(text)
    assert(firstWord ~= nil, "firstWord is never nil")

    -- don't replace slash commands
    if (getNextCharUtf8(firstWord) == "/") then
        self:DebugPrint("CheckReplacementAllowed : skip other slash commands:", firstWord)
        return false
    end

    -- emote detection
    if (self:IsOneBigEmote(text)) then
        self:DebugPrint("CheckReplacementAllowed : one big emote")
        return self.db.profile.channels.emote
    end

    -- in any other case, treat as ordinary text or emote
    return true
end

--- Checks if the text from the Grichelde slash command can be replaced and if so
--- returns the replacable text, the chat type and target (player or channel) from the message text.
--- self is used the the override slash command: "/gri /emote text to replace".
-- @param message (string) the whole message
-- @return message (string) stripped message
-- @return type (string) chat type
-- @return channel (string|number) channel number for whispers
function Grichelde:CheckAndExtractMessageTypeTarget(message)
    self:DebugPrint("CheckAndExtractMessageTypeTarget : text:", message)

    -- skip if no further text
    if (nilOrEmpty(message)) then
        -- dont send text at all
        return nil
    end

    -- first word should be a chat command
    if (getNextCharUtf8(message) == "/") then
        -- extract chat command
        local chatCmd, targetAndText = self:SplitOnFirstMatch(message)
        assert(chatCmd ~= nil, "chatCmd is never nil")

        local type = tFilter(self.SUPPORTED_CHAT_COMMANDS,
            function(_, k, _) return chatCmd == k end
        )
        assert(#type < 2)

        if (not tIsEmpty(type)) then
            self:DebugPrint("CheckAndExtractMessageTypeTarget : determined type: %s", type[1])

            -- valid /chattype
            if (type[1] == "WHISPER") then
                -- special reply handling
                if ("/r" == chatCmd) or ("/reply" == chatCmd) then
                    -- reuse last type and target if possible
                    local lastTold, lastToldType = ChatEdit_GetLastToldTarget()
                    self:DebugPrint("CheckAndExtractMessageTypeTarget : lastTell, lastTellType =", lastTold, lastToldType)
                    return targetAndText, lastToldType or "WHISPER", lastTold
                elseif ("/tt" == chatCmd) then
                    -- determine target from game world selection
                    if (not UnitExists("target") or not UnitIsPlayer("target")) then
                        self:ErrorPrint(self.L.Error_InvalidWhisperTarget)
                        -- dont send text at all
                        return nil
                    end

                    local target = UnitName("target");
                    if (target == nil) then
                        self:ErrorPrint(self.L.Error_InvalidWhisperTarget)
                        -- dont send text at all
                        return nil
                    end

                    -- eventually we found our target
                    self:DebugPrint("CheckAndExtractMessageTypeTarget : target:", target)
                    return targetAndText, "WHISPER", target
                else
                    -- determine target from text
                    local target, text = self:SplitOnFirstMatch(targetAndText)
                    if (target == nil) then
                        self:ErrorPrint(self.L.Error_InvalidWhisperTarget)
                        -- dont send text at all
                        return nil
                    end

                    -- eventually we found our target
                    self:DebugPrint("CheckAndExtractMessageTypeTarget : determined target:", target)
                    return text, "WHISPER", target
                end
            else
                -- all other chat types
                return targetAndText, type[1], nil
            end
        else
            self:DebugPrint("CheckAndExtractMessageTypeTarget : not a standard channel: %s", chatCmd)

            -- if not a valid chat command, try as a numbered channel
            local _, _, channelNumber = find(chatCmd, "^/(%d+)")
            if (channelNumber ~= nil) then
                local channelId = GetChannelName(channelNumber)
                if (channelId ~= nil) then
                    return targetAndText, "CHANNEL", channelId
                end
            end

            -- ignore any other slash commands
            self:ErrorPrint(self.L.Error_InvalidChannel)
            -- dont send text at all
            return nil
        end
    elseif self:IsOneBigEmote(message) then
        self:DebugPrint("CheckAndExtractMessageTypeTarget : determined EMOTE type")
        return message, "EMOTE", nil
    else
        -- in any other case, treat as ordinary text, assume default type and channel
        return message
    end
end

function Grichelde:CleanseMessage(message)
    local text = message or ""
    if (IsAddOnLoaded("Misspelled")) then
        self:DebugPrint("Misspelled detected: cleansing message")
        text = _G.Misspelled:RemoveHighlighting(message)
    end
    return trim(text)
end

--- Always replaces the text accoording to the configuration, even if activation or channel was disabled.
--- self is used the the override slash command: "/gri /emote text to replace".
--- NOTE: type and channel (in case of whispers) are determined from the message text.
-- @param message string
-- @param type string
function Grichelde:SendChatMessageOverride(message, ...)
    local cleasendText = self:CleanseMessage(message)

    local fallbackType, fallbackLang = DEFAULT_CHAT_FRAME.editBox.chatType or "SAY", DEFAULT_CHAT_FRAME.editBox.languageID
    local msg, type, chan = self:CheckAndExtractMessageTypeTarget(cleasendText)

    if (msg ~= nil) then
        msg = self:ReplaceText(msg)

        if (type ~= nil) then
            self:SendChunkifiedChatMessage(msg, type, fallbackLang, chan, ...)
        else
            self:SendChunkifiedChatMessage(msg, fallbackType, fallbackLang, chan, ...)
        end
    else
        -- suppress invalid messages/channels/targets
    end
end

--- Before a chat message is sent, check if replacement is required and replace the text accordingly.
-- @param message string
-- @param type string
-- @param language string
-- @param channel string
function Grichelde:SendChatMessage(message, type, ...)
    if (not self.db.profile.enabled) then
        self:DebugPrint("SendChatMessage : disabled")
        self.hooks["SendChatMessage"](message, type, ...);
    elseif (nilOrEmpty(message)) then
        self:DebugPrint("SendChatMessage : no text")
        self.hooks["SendChatMessage"](message, type, ...);
    else
        local cleasendText = self:CleanseMessage(message)
        if (self:CheckReplacementAllowed(cleasendText, type)) then
            cleasendText = self:ReplaceText(cleasendText)
            self:SendChunkifiedChatMessage(cleasendText, type, ...)
        else
            self.hooks["SendChatMessage"](message, type, ...);
        end
    end
end