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

local nilOrEmpty, ipairs, spairs, tContains, tFilter, tInsert, tConcat, find, sub, isUpper, isLower, toUpper, toLower, trim, length
    = Grichelde.functions.nilOrEmpty, Grichelde.functions.ipairs, Grichelde.functions.spairs, Grichelde.functions.tContains, Grichelde.functions.tFilter, Grichelde.functions.tInsert, Grichelde.functions.tConcat,
      Grichelde.functions.find, Grichelde.functions.sub, Grichelde.functions.isUpper, Grichelde.functions.isLower, Grichelde.functions.toUpper, Grichelde.functions.toLower, Grichelde.functions.trim, Grichelde.functions.length

--- 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, language, channel, ...)
    local replacedText = self:CheckAndReplace(message, type)


    -- Send text in chunks if length exceeds 255 bytes after replacement
    local chunks = self:SplitText(replacedText)
    self:DebugPrint("SendChatMessage : #chunks:", #chunks)

    for _, chunk in ipairs(chunks) do
        self.hooks["SendChatMessage"](chunk, type, language, channel, ...);
    end
end

function Grichelde:CheckAndReplace(message, type)
    local text = message
    if (self:CheckReplacement(text, type)) then
        if (_G.Misspelled) then
            self:DebugPrint("Misspelled detected: cleansing message")
            text = _G.Misspelled:RemoveHighlighting(text)
        end
        text = self:ReplaceText(trim(text))
    end
    return text
end

function Grichelde:CheckReplacement(text, channel)
    -- skip if not enabled
    if (not self.db.profile.enabled) then
        self:DebugPrint("CheckReplacement : disabled")
        return false
    end

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

    -- skip if wrong channel
    local allowedChannels = tFilter(self.db.profile.channels,
        function(_,v) return v == true end,
        function(k,_) return k end
    )
    self:DebugPrint("CheckReplacement : allowed channels:", tConcat(allowedChannels, ", "))
    local type = self:ConvertBlizChannelToType(channel)
    if (type == nil or not tContains(allowedChannels, type)) then
        self:DebugPrint("CheckReplacement : skip channel type:", type)
        return false
    end

    -- don't replace slash commands except chat related commands
    if sub(text, 1, 1) == "/" then
        local firstWord, _ = self:SplitOnFirstMatch(text)
        -- todo: adapt allowed slash commands
        if (firstWord == nil or not tContains(self.slashCommands, firstWord)) then
            self:DebugPrint("CheckReplacement : ignore slash command")
            return false
        end
    end

    -- in any other case
    return true
end

function Grichelde:ConvertBlizChannelToType(channel)
    local type = toLower(channel)
    self:DebugPrint("ConvertBlizChannelToType : convert %s to %s", channel, type)
    return type
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
-- @return string
function Grichelde:ReplaceText(text)
    local lookAheads = {'|', '{', '%'}
    local finalText = ""
    local newText = text

    -- don't replace non-chat related slash commands
    local firstWord, line = self:SplitOnFirstMatch(text)
    if (firstWord ~= nil and tContains(self.slashCommands, firstWord)) then
        self:DebugPrint("ReplaceText : Found slash command:", firstWord )
        -- skip chat slash command
        finalText = finalText .. firstWord .. ' '
        newText = line
    end

    local current = 1
    local lastStart = 1

    while current <= length(newText) do
        local currentChar = sub(newText, current, current)
        self:TracePrint("current/char : %s,%s", current, currentChar)

        if ( not tContains(lookAheads, currentChar)) then
            current = current + 1
        else

            -- lookahead-check for itemLinks, textures and raid target icons
            local textAhead = sub(newText, current)
            local posEnd = self:CheckForPreversableText(textAhead)
            if posEnd > 0 then
                self:DebugPrint("ReplaceText : Found an ignore pattern")

                local textBehind = sub(newText, lastStart, current - 1)
                local replacement = self:ReplaceCharacters(textBehind)
                local preservedText = sub(textAhead, 1, posEnd)

                finalText = finalText .. replacement .. preservedText
                current = current + posEnd
                lastStart = current
                self:DebugPrint("ReplaceText : restarting at", lastStart)
            else
                -- no corresponding end was found to start pattern, continue loop with next char
                current = current + 1
            end
        end
    end

    -- cleanup remaining text to the end
    local remainingText = sub(newText, lastStart)
    local replacement = self:ReplaceCharacters(remainingText)
    finalText = finalText .. replacement

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


--- Checks if the text starts with a preversable ignore pattern, such as itemLinks, textures, raid target icons or
--- %-substitutons and returns the end location of the match, or 0 if no pattern was found
-- @param text string
-- @return number
function Grichelde:CheckForPreversableText(text)
    self:DebugPrint("CheckForPreversableText : text:", text)

    -- do not replace these patterns
    local ignorePatterns = {
        "|[Cc]%x%x%x%x%x%x%x%x.-|r", -- colored items (or links)
        "|H.-|h", -- item links (http://www.wowwiki.com/ItemLink)
        "|T.-|t", -- textures
        "|K.-|k", -- Battle.net
        "|n", -- newline

        "{rt[1-8]}",  -- rumbered raid target icons
        "{Star}",     -- named raid target icon 1
        "{Circle}",   -- named raid target icon 2
        "{Coin}",     -- named raid target icon 2
        "{Diamond}",  -- named raid target icon 3
        "{Triangle}", -- named raid target icon 4
        "{Moon}",     -- named raid target icon 5
        "{Square}",   -- named raid target icon 6
        "{Cross}",    -- named raid target icon 7
        "{X}",        -- named raid target icon 7
        "{Skull}",    -- named raid target icon 8

        "%%n",   -- player's name
        "%%z",   -- player's currnt zone
        "%%sz",  -- player's current sub-zone
        "%%loc", -- player's map coordinates
        "%%t",   -- name of target
        "%%f",   -- name of focus target
        "%%m",   -- name of mouseover unit
        "%%p",   -- name of player pet
        "%%tt"   -- name of player's target's target
    }

    -- Calling find on ever pattern might be inefficient but its way less code.
    for _, pattern in ipairs(ignorePatterns) do
        local pos1, pos2 = find(text, pattern)
        if pos1 == 1 and pos2 ~= nil then
            self:DebugPrint("CheckForPreversableText : Found ignore pattern \"%s\" at (%d, %d)", pattern, pos1, pos2)
            return pos2
        end
    end
    self:DebugPrint("CheckForPreversableText : no ignore pattern found")
    return 0
end

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

    local result = text
    local consolidate = {}

    -- replacements are done first
    for replName, replTable in spairs(replacements) do
        local before = result
        local search = replTable.searchText

        if not nilOrEmpty(search) then
            local replace = replTable.replaceText
            consolidate[replName] = {}

            if replTable.exactCase then
                -- exact case
                self:DebugPrint("ReplaceCharacters : \"%s => %s\" (exact case)", search, replace)
                local pos, offset = 1, 0
                local oldResult = result

                local pos1, pos2 = find(oldResult, search, pos)
                while (pos1 and pos2) do
                    self:TracePrint("pos1: %d, pos2: %d", pos1, pos2)
                    local pre = sub(result, 1, pos1 - 1 + offset)
                    local post = sub(result, pos2 + 1 + offset)
                    self:TracePrint("pre: %s, post: %s", pre, post)

                    -- actual replacement
                    result = pre .. replace .. post
                    self:DebugPrint("result: %s", result)

                    -- remember positions for consolidate
                    if replTable.consolidate then
                        tInsert(consolidate[replName], pos1 + offset)
                    end

                    -- replacement text can lengthen or shorten the resulting text
                    -- after replacement result and lowerResult can have different sizes
                    offset = offset + length(replace) - length(search)
                    -- update values for next iteration
                    pos = pos2 + 1
                    pos1, pos2 = find(oldResult, search, pos)
                end
            else
                self:DebugPrint("ReplaceCharacters : \"%s => %s\" (ignoreCase)", search, replace)
                local pos, offset = 1, 0
                local lowerResult = toLower(result)
                local lowerSearch = toLower(search)

                local pos1, pos2 = find(lowerResult, lowerSearch, pos)
                while (pos1 and pos2) do
                    self:TracePrint("pos1: %d, pos2: %d", pos1, pos2)
                    local pre = sub(result, 1, pos1 - 1 + offset)
                    local match = sub(result, pos1 + offset, pos2 + offset)
                    local post = sub(result, pos2 + 1 + offset)
                    self:TracePrint("pre: %s, match: %s, post: %s", pre, match, post)

                    -- keep cases
                    local repl = ""
                    local lastCase = nil
                    for p = pos1, pos2 do
                        self:TracePrint("p: %d", p)
                        local c = sub(match, p - pos1 + 1, p - pos1 + 1)
                        local r = sub(replace, p - pos1 + 1, p - pos1 + 1) or ""

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

                    self:TracePrint("length %d > %d", length(replace), pos2 - pos1 + 1)
                    if (length(replace) > pos2 - pos1 + 1) then
                        local remainingReplace = sub(replace, pos2 - pos1 + 2)
                        local nextLetter = sub(post, 1, 1)

                        self:TracePrint("rest: %s, n: %s, lastCase: %s", remainingReplace, nextLetter, lastCase)

                        if (isUpper(nextLetter)) then
                            if lastCase == nil or lastCase == false then
                                repl = repl .. remainingReplace
                            else
                                repl = repl .. toUpper(remainingReplace)
                            end
                        elseif (isLower(nextLetter)) then
                            if lastCase == nil or lastCase == true then
                                repl = repl .. remainingReplace
                            else
                                repl = repl .. toLower(remainingReplace)
                            end
                        else
                            -- no letter
                            if lastCase == nil then
                                repl = repl .. remainingReplace
                            elseif lastCase == false then
                                repl = repl .. toLower(remainingReplace)
                            else
                                repl = repl .. toUpper(remainingReplace)
                            end
                        end
                    end

                    -- actual replacement
                    result = pre .. repl .. post
                    self:DebugPrint("result: %s", result)

                    -- remember positions for consolidate
                    if replTable.consolidate then
                        tInsert(consolidate[replName], pos1 + offset)
                    end

                    -- replacement text can be longer or shorter the resulting text
                    -- after replacement result and lowerResult can have different sizes
                    offset = offset + length(repl) - length(lowerSearch)
                    -- update values for next iteration
                    pos = pos2 + 1
                    pos1, pos2 = find(lowerResult, lowerSearch, pos)
                end
            end

            if before ~= result then
                self:DebugPrint("ReplaceCharacters : replaced \"%s\" with \"%s\"", before, result)
            end
        else
            self:DebugPrint("ReplaceCharacters : Skip replacement for empty mapping")
        end
    end

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

        if not nilOrEmpty(search) then
            local replace = replTable.replaceText
            local lowerResult = toLower(result)
            local offset = 0

            if replTable.consolidate then
                self:DebugPrint("ReplaceCharacters : consolidating \"%s => %s\"", search, replace)
                self:DebugPrint("consolidate[" .. replName .. "] is:")
                self:DebugPrint(consolidate[replName])

                for _, pos1 in spairs(consolidate[replName]) do
                    local pos2 = pos1 + length(replace) - 1
                    self:TracePrint("pos1: %d, pos2: %d", pos1, pos2)
                    local match = sub(lowerResult, pos1, pos2)
                    local next = sub(lowerResult, pos2 + 1, pos2 + 1 + pos2 - pos1)
                    self:TracePrint("match: %s, next: %s", match, next)

                    local _, p2 = find(next, "^" .. match)
                    self:TracePrint("p2: %d", p2)
                    if (p2) 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("result: %s", result)
                end
            end

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

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

--- Splits a long text in longest possible chunks of <= 255 length, split at last available space
-- @param text string
-- @return table
function Grichelde:SplitText(text)
    local chunks = {}
    local splitText = text
    local textSize = length(splitText or "")

    while textSize > 255 do
        local chunk = sub(splitText, 1, 255)
        local remaining = ""

        -- special case: if space is the start of the next chunk, don't split this chunk
        if sub(splitText, 256, 256) ~= ' ' then
            -- split at last space, don't assign directly as nil might be returned
            local left, right = self:SplitOnLastMatch(chunk)
            if left ~= nil then
                chunk = left
            end
            if right ~= nil then
                remaining = right
            end
        end

        self:DebugPrint("SplitText : chunk:", chunk)

        tInsert(chunks, chunk)
        splitText = remaining .. sub(splitText, 256)
        textSize = length(splitText)
    end

    -- pickup remaining text < 255
    self:DebugPrint("SplitText : last chunk:", splitText)
    tInsert(chunks, splitText)

    return chunks
end
