-- Support for : Colors | Styles
-- Minetest :    Yes    | No
-- IRC :         Yes    | Yes
-- Discord :     No     | Yes

-- Assumptions : 
-- 1. IRC users only use IRC formatting chars and don't rely on Markdown
-- 2. Minetest users only use Minetest color codes + Markdown
-- 3. Discord users only rely on Markdown
-- Note : Markdown means Discord Markdown

-- Resulting conversions :
-- Minetest colors -> IRC colors
-- IRC colors -> Minetest colors
-- Discord styles -> IRC styles
-- IRC styles -> Discord styles

-- Resources : 
-- https://modern.ircdocs.horse/formatting.html
-- https://support.discordapp.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline-
-- https://github.com/minetest/minetest/blob/master/doc/lua_api.txt

-- Notes: 
-- While code ("`code`") to IRC is straightforward (monospace), monospace to code is undefined behavior (concat code blocks? no code blocks at all?)

local function is_digit(char)
    return char >= "0" and char <= "9"
end

local md_escape={
    ["*"]=true, ["_"]=true, ["~"]=true, ["`"]=true, ["\\"]=true, ["|"]=true
}

function escape_markdown(text)
    local res={}
    for i=1, text:len() do
        local char=text:sub(i,i)
        if md_escape[char] then
            table.insert(res, "\\")
        end
        table.insert(res, char)
    end
    return table.concat(res)
end

-- minetest characters: color starter
local minetest_color_starter=string.char(0x1b)

-- irc characters
local irc_escape_code=string.char(0x02)..string.char(0x02)
local irc_disable=string.char(0x0F)
local irc_color_reverse=string.char(0x16)
local irc_color_starter=string.char(0x03)
local irc_hex_color_starter=string.char(0x04)
local irc_bold=string.char(0x02)
local irc_italics=string.char(0x1D)
local irc_underlined=string.char(0x1F)
local irc_strikethrough=string.char(0x1F)
local irc_monospace=string.char(0x11)

-- Converts Discord-style Markdown to IRC format
local irc_style_to_md={
    [irc_bold]="**", -- Bold
    [irc_italics]="*", -- Italics
    [irc_underlined]="__", -- Underlined
    [irc_strikethrough]="~~", -- Strikethrough
}

local md_style_to_irc=modlib.table.flip(irc_style_to_md)

local irc_escape={
    [irc_bold]=true, [irc_italics]=true, [irc_underlined]=true, [irc_strikethrough]="~~", [irc_disable]=true, [irc_color_reverse]=true, [irc_monospace]=true
}

local function skip_color_code(text, i)
    for j=1,2 do
        if is_digit(text:sub(i+1,i+1)) then
            i=i+1
        end
    end
    if text:sub(i+1,i+1) == "," and is_digit(text:sub(i+1,i+1)) then
        i=i+1
        if is_digit(text:sub(i+1,i+1)) then
            i=i+1
        end
    end
    return i
end

function escape_irc(text)
    local res={}
    local i=1
    while i <= text:len() do
        local char=text:sub(i,i)
        if char == irc_color_starter then
            i=skip_color_code(text, i)
        elseif char == irc_hex_color_starter then
            i=i+6
        elseif not irc_escape[char] then
            table.insert(res, char)
        end
        i=i+1
    end
    return table.concat(res)
end

md_style_to_irc["_"]=md_style_to_irc["*"]

local markdown_trie = trie.new()
for tag, toggle in pairs(md_style_to_irc) do
    trie.insert(markdown_trie, tag, {name=tag, reversed=tag, opening=toggle, closing=toggle, space_sensitive=true})
end
trie.insert(markdown_trie, "||", {name="||", reversed="||", opening=irc_color_starter.."01,01", closing=irc_color_starter, escape_func=is_digit})
trie.insert(markdown_trie, "***", {name="***", reversed="***", opening=irc_italics..irc_bold, closing=irc_italics..irc_bold,
    space_sensitive=true, conversion={
        ["*"]={name="**", reversed=irc_italics.."**", opening=irc_italics..irc_bold, closing=irc_bold},
        ["**"]={name="*", reversed=irc_bold.."*", opening=irc_italics..irc_bold, closing=irc_italics}
    }
})
trie.insert(markdown_trie, "___", {reversed="___", opening=irc_underlined..irc_italics, closing=irc_underlined..irc_italics,
    space_sensitive=true, conversion={
        ["_"]={name="__", reversed=irc_italics.."__", opening=irc_italics..irc_underlined, closing=irc_underlined},
        ["__"]={name="_", reversed=irc_underlined.."_", opening=irc_underlined..irc_italics, closing=irc_italics}
    }
})

local markdown_code_tag="`"

-- Strips Markdown for Minetest. Won't strip invalid Markdown (like 1*1=2)
function strip_markdown(markdown)
    local i=1
    local res={}
    local tags={}
    while i <= markdown:len() do
        local char = markdown:sub(i,i)
        local function process()
        if char == markdown_code_tag then
            local closing = markdown:find("[^\\]`", i+1)
            if closing then
                table.insert(res, markdown:sub(i+1, closing))
                i=closing+1
                return
            end
        elseif char == "\\" and md_escape[markdown:sub(i+1,i+1)] then
            table.insert(res, markdown:sub(i+1,i+1))
            i=i+1
            return
        elseif char == " " then
            if res[#res] and res[#res].space_sensitive then
                res[#res] = res[#res].reversed
                table.remove(tags)
            end
        else
            local tag, offset = trie.find_longest(markdown_trie, markdown, i)
            if tag then
                for index, tag_index in modlib.table.rpairs(tags) do
                    local conversion = res[tag_index].conversion and res[tag_index].conversion[tag.name]
                    if res[tag_index].name == tag.name or conversion then
                        if tag.space_sensitive and markdown:sub(i-1,i-1) == " " then
                            table.insert(res, tag.reversed)
                        else
                            local index_2 = #tags
                            while index_2 > index do
                                local tag_index_2 = tags[index_2]
                                res[tag_index_2] = res[tag_index_2].reversed
                                table.remove(tags)
                                index_2 = index_2 - 1
                            end
                            if conversion then
                                res[tag_index] = conversion
                            else
                                table.remove(tags)
                                res[tag_index] = ""
                            end
                        end
                        i=offset
                        return
                    end
                end
                table.insert(res, tag)
                table.insert(tags, #res)
                i=offset
                return
            end
        end
        table.insert(res, char)
        end
        process()
        i=i+1
    end
    for _, tag in pairs(tags) do
        if res[tag].reversed then
            res[tag]=res[tag].reversed
        end
    end
    return table.concat(res)
end

function markdown_to_irc(markdown)
    local i=1
    local res={}
    local tags={}
    while i <= markdown:len() do
        local char = markdown:sub(i,i)
        local function process()
        if char == markdown_code_tag then
            local closing = markdown:find("[^\\]`", i+1)
            if closing then
                table.insert(res, irc_monospace)
                table.insert(res, markdown:sub(i+1, closing))
                table.insert(res, irc_monospace)
                i=closing+1
                return
            end
        elseif char == "\\" and md_escape[markdown:sub(i+1,i+1)] then
            table.insert(res, markdown:sub(i+1,i+1))
            i=i+1
            return
        elseif char == " " then
            if res[#res] and res[#res].space_sensitive then
                res[#res] = res[#res].reversed
                table.remove(tags)
            end
        else
            local tag, offset = trie.find_longest(markdown_trie, markdown, i)
            if tag then
                for index, tag_index in modlib.table.rpairs(tags) do
                    local conversion = res[tag_index].conversion and res[tag_index].conversion[tag.name]
                    if res[tag_index].name == tag.name or conversion then
                        if tag.space_sensitive and markdown:sub(i-1,i-1) == " " then
                            table.insert(res, tag.reversed)
                        else
                            local index_2 = #tags
                            while index_2 > index do
                                local tag_index_2 = tags[index_2]
                                res[tag_index_2] = res[tag_index_2].reversed
                                table.remove(tags)
                                index_2 = index_2 - 1
                            end
                            if conversion then
                                res[tag_index] = conversion
                            else
                                table.remove(tags)
                                res[tag_index] = res[tag_index].opening
                            end
                            table.insert(res, tag.closing)
                            if tag.escape_func and tag.escape_func(markdown:sub(offset+1,offset+1)) then
                                table.insert(res, irc_escape_code)
                            end
                        end
                        i=offset
                        return
                    end
                end
                table.insert(res, tag)
                table.insert(tags, #res)
                i=offset
                return
            end
        end
        table.insert(res, char)
        end
        process()
        i=i+1
    end
    for _, tag in pairs(tags) do
        if res[tag].reversed then
            res[tag]=res[tag].reversed
        end
    end
    return table.concat(res)
end

-- Converts Markdown to IRC
function markdown_to_irc(markdown)
    local i=1
    local res={}
    local tags={}
    while i <= markdown:len() do
        local char = markdown:sub(i,i)
        local function process()
        if char == markdown_code_tag then
            local closing = markdown:find("[^\\]`", i+1)
            if closing then
                table.insert(res, irc_monospace)
                table.insert(res, markdown:sub(i+1, closing))
                table.insert(res, irc_monospace)
                i=closing+1
                return
            end
        elseif char == "\\" and md_escape[markdown:sub(i+1,i+1)] then
            table.insert(res, markdown:sub(i+1,i+1))
            i=i+1
            return
        elseif char == " " then
            if res[#res] and res[#res].space_sensitive then
                res[#res] = res[#res].reversed
                table.remove(tags)
            end
        else
            local tag, offset = trie.find_longest(markdown_trie, markdown, i)
            if tag then
                for index, tag_index in modlib.table.rpairs(tags) do
                    local conversion = res[tag_index].conversion and res[tag_index].conversion[tag.name]
                    if res[tag_index].name == tag.name or conversion then
                        if tag.space_sensitive and markdown:sub(i-1,i-1) == " " then
                            table.insert(res, tag.reversed)
                        else
                            local index_2 = #tags
                            while index_2 > index do
                                local tag_index_2 = tags[index_2]
                                res[tag_index_2] = res[tag_index_2].reversed
                                table.remove(tags)
                                index_2 = index_2 - 1
                            end
                            if conversion then
                                res[tag_index] = conversion
                            else
                                table.remove(tags)
                                res[tag_index] = res[tag_index].opening
                            end
                            table.insert(res, tag.closing)
                            if tag.escape_func and tag.escape_func(markdown:sub(offset+1,offset+1)) then
                                table.insert(res, irc_escape_code)
                            end
                        end
                        i=offset
                        return
                    end
                end
                table.insert(res, tag)
                table.insert(tags, #res)
                i=offset
                return
            end
        end
        table.insert(res, char)
        end
        process()
        i=i+1
    end
    for _, tag in pairs(tags) do
        if res[tag].reversed then
            res[tag]=res[tag].reversed
        end
    end
    return table.concat(res)
end

-- Converts IRC text modifiers to Discord markdown, escaping included
function irc_to_markdown(irc)
    local res={}
    local active={}
    local i=1
    while i <= irc:len() do
        local char=irc:sub(i,i)
        local md=irc_style_to_md[char]
        if md then
            while irc:sub(i+1)==" " do
                table.insert(res, " ")
                i=i+1
            end
            local insert = true
            for index, open_md in modlib.table.rpairs(active) do
                if open_md == md then
                    table.remove(active, index)
                    local i=#res
                    while res[i] == " " do
                        i=i-1
                    end
                    table.insert(res, i+1, md)
                    insert = false
                    break
                end
            end
            if insert then
                table.insert(active, md)
                table.insert(res, md)
            end
        elseif char == irc_disable then
            for _, md in modlib.table.rpairs(active) do
                table.insert(res, md)
            end
            active={}
        elseif md_escape[char] then
            table.insert(res, "\\")
        elseif char == irc_color_starter then --color
            if irc:sub(i+2, i+2) == "," then
                if is_digit(irc:sub(i+1, i+1)) and irc:sub(i+1, i+1) == irc:sub(i+3, i+3) and not is_digit(irc:sub(i+4, i+4)) then
                    table.insert(active, "||")
                    table.insert(res, md)
                end
            elseif irc:sub(i+3, i+3) == "," then
                local fg, bg = irc:sub(i+1, i+2), irc:sub(i+4, i+5)
                if is_digit(irc:sub(i+1, i+1)) and is_digit(irc:sub(i+2, i+2)) and fg == bg and fg ~= "99" then
                    table.insert(active, "||")
                    table.insert(res, md)
                end
            else
                for index, open_md in modlib.table.rpairs(active) do
                    if open_md == "||" then
                        table.remove(active, index)
                        local j=#res
                        while res[j] == " " do
                            j=j-1
                        end
                        table.insert(res, j+1, md)
                    end
                end
            end
            i=skip_color_code(irc, i)
        else
            table.insert(res, char)
        end
        i=i+1
    end
    for _, thing in modlib.table.rpairs(active) do
        table.insert(res, thing)
    end
    return table.concat(res)
end

-- Converts IRC colors to Minetest colors, background colors included
local user_defined_colors={'000000','0000FF','00FF00','FF0000', '654321','FF00FF','FFA500','FFFF00',
'90EE90','00FFFF','E0FFFF','ADD8E6','FF69B4','808080','D3D3D3'}
user_defined_colors[0]='FFFFFF'
local conversion_table={[16]='470000',[17]='472100',[18]='474700',[19]='324700',[20]='004700',[21]='00472c',[22]='004747',[23]='002747',[24]='000047',[25]='2e0047',[26]='470047',[27]='47002a',[28]='740000',[29]='743a00',[30]='747400',[31]='517400',[32]='007400',[33]='007449',[34]='007474',[35]='004074',[36]='000074',[37]='4b0074',[38]='740074',[39]='740045',[40]='b50000',[41]='b56300',[42]='b5b500',[43]='7db500',[44]='00b500',[45]='00b571',[46]='00b5b5',[47]='0063b5',[48]='0000b5',[49]='7500b5',[50]='b500b5',[51]='b5006b',[52]='ff0000',[53]='ff8c00',[54]='ffff00',[55]='b2ff00',[56]='00ff00',[57]='00ffa0',[58]='00ffff',[59]='008cff',[60]='0000ff',[61]='a500ff',[62]='ff00ff',[63]='ff0098',[64]='ff5959',[65]='ffb459',[66]='ffff71',[67]='cfff60',[68]='6fff6f',[69]='65ffc9',[70]='6dffff',[71]='59b4ff',[72]='5959ff',[73]='c459ff',[74]='ff66ff',[75]='ff59bc',[76]='ff9c9c',[77]='ffd39c',[78]='ffff9c',[79]='e2ff9c',[80]='9cff9c',[81]='9cffdb',[82]='9cffff',[83]='9cd3ff',[84]='9c9cff',[85]='dc9cff',[86]='ff9cff',[87]='ff94d3',[88]='000000',[89]='131313',[90]='282828',[91]='363636',[92]='4d4d4d',[93]='656565',[94]='818181',[95]='9f9f9f',[96]='bcbcbc',[97]='e2e2e2',[98]='ffffff'}
function hex_to_table(color)
    return {tonumber(color:sub(1, 2), 16), tonumber(color:sub(3, 4), 16), tonumber(color:sub(5, 6), 16)}
end
function table_to_hex(color)
    return string.format("%02X", color[1])..string.format("%02X", color[2])..string.format("%02X", color[3])
end
modlib.table.add_all(conversion_table, user_defined_colors)
local reversed = modlib.table.flip(conversion_table)
for k, v in pairs(reversed) do
    reversed[string.upper(k)]=v
    reversed[string.lower(k)]=v
end
reversed.FFFFFF=0
reversed.ffffff=0

function irc_to_minetest(irc)
    local fg, background
    local reversed = false
    local rope={}
    local i=1
    while i <= irc:len() do
        local char = irc:sub(i,i)
        if char == irc_color_starter then
            local j=i+1
            if is_digit(irc:sub(j,j)) then
                fg=irc:byte(j,j)-string.byte("0")
                i=j
                j=j+1
                if is_digit(irc:sub(j,j)) then
                    fg=fg*10+(irc:byte(j,j)-string.byte("0"))
                    i=j
                    j=j+1
                end
                local bg
                if irc:sub(j,j) == "," and is_digit(irc:sub(j+1,j+1)) then
                    local bg=irc:byte(j,j)-string.byte("0")
                    i=j
                    j=j+1
                    if is_digit(irc:sub(j,j)) then
                        bg=bg*10+(irc:byte(j,j)-string.byte("0"))
                        i=j
                    end
                end
                if bg then
                    -- no proper implementation for background escape sequences yet - see the "Escape sequences" part of the Lua API
                    -- table.insert(rope, minetest.get_background_escape_sequence("#"..conversion_table[bg]))
                    background = bg
                end
                if reversed then
                    table.insert(rope, minetest.get_color_escape_sequence("#"..conversion_table[bg]))
                else
                    table.insert(rope, minetest.get_color_escape_sequence("#"..conversion_table[fg]))
                end
            else
                table.insert(rope, minetest.get_color_escape_sequence("#FFFFFF"))
            end
        elseif char == irc_disable then
            table.insert(rope, minetest.get_color_escape_sequence("#FFFFFF"))
            background = nil
        elseif char == irc_color_reverse then
            reversed = not reversed
            if background then
                table.insert(rope, minetest.get_color_escape_sequence("#"..conversion_table[background]))
                -- no proper implementation for background escape sequences yet - see the "Escape sequences" part of the Lua API
                -- table.insert(rope, minetest.get_background_escape_sequence("#"..conversion_table[fg]))
                background, fg = fg, background
            end
        elseif not irc_style_to_md[char] and char ~= irc_monospace then
            table.insert(rope, char)
        end
        i=i+1
    end
    return table.concat(rope)
end

local color_conv = (bridges.irc and bridges.irc.convert_minetest_colors) or "hex"

if color_conv == "hex" then -- always use hex, no matter what
    function convert_color_to_irc(color)
        return nil, irc_hex_color_starter..color
    end
elseif color_conv == "hex_safer" then
    function convert_color_to_irc(color)
        -- prefer simple colors
        local rev = reversed[color]
        if rev then
            return ",", irc_color_starter..((rev < 10 and "0") or "")..tostring(rev)
        end
        return nil, irc_hex_color_starter..color
    end
elseif color_conv == "disabled" then
    function convert_color_to_irc(color)
        return error("Color conversion to IRC is disabled. Check your config.")
    end
else
    local color_chooser
    if color_conv == "safest" then
        local closest_color_basic = closest_color_finder(modlib.table.process(user_defined_colors, function(k, color) return hex_to_table(color) end))
        color_chooser = closest_color_basic
    else
        local closest_color_extended = closest_color_finder(modlib.table.process(conversion_table, function(k, color) return hex_to_table(color) end))
        color_chooser = closest_color_extended
    end
    function convert_color_to_irc(color)
        local rev = reversed[color]
        if rev and rev <= 15 then
            return function(c) return c=="," end, irc_color_starter..((rev < 10 and "0") or "")..tostring(reversed[color])
        end
        local closest = reversed[table_to_hex(color_chooser(hex_to_table(color)))]
        if not closest then error(table_to_hex(color_chooser(hex_to_table(color)))) end
        return function(c) return c=="," end, irc_color_starter..((closest < 10 and "0") or "")..tostring(closest)
    end
end

local old_convert_color_to_irc = convert_color_to_irc
function convert_color_to_irc(color)
    if color:lower() == "ffffff" then -- treat white as color reset, by default. TODO think of doing the same for close colors (can we?)
        return function(c) return c >= "0" and c <= "9" end, irc_color_starter
    end
    return old_convert_color_to_irc(color)
end

function minetest_to_irc(message)
    local i=1
    local res={}
    while i <= message:len() do
        local color
        if message:sub(i,i) == minetest_color_starter and message:sub(i+1, i+4) == "(c@#" and message:sub(i+11, i+11) == ")" then
            is_color = true
            color = message:sub(i+5, i+10)
            for j=1, 6 do
                local c = color:sub(j, j):lower()
                if not (c >= "0" and c <= "9") and not (c >= "a" and c <= "f") then
                    color = nil
                    break
                end
            end
        end
        if color then
            local needs_escape, color = convert_color_to_irc(color)
            table.insert(res, color)
            if needs_escape and needs_escape(message:sub(i+12, i+12)) then
                table.insert(res, irc_escape_code)
            end
            i=i+11
        else
            table.insert(res, message:sub(i,i))
        end
        i=i+1
    end
    return table.concat(res)
end
