--[[
Lua interface between md and the chat window.
Primary goal is to intercept chat text and pass it back to md, with some
string parsing to break apart command arguments.
Secondary goal is to be able to print to the chat window from md.

7.0+ notes (written in 8.0):
- This version overhauls the chat window.
- menu.editboxSendMessage handles user written inputs, triggered when
deactivating the text box. Escaping out of the box will set wasteableclick
and do nothing.
- Text not starting with "/" is passed to OnlineSendChatMessage.
  - If not logged in, entering text does nothing (eg. is not displayed).
- Text starting with "/" is passed to ExecuteDebugCommand.
- Has various logic for private chats.
- menu.messagetexts holds the lines, filled by getChatMessages, which gets
then from OnlineGetChatMessages.
- message fields:
  - author
  - authorid
  - time
  - reported
  - text
  - isprivate
- Chat window size can be changed with dropdown, adjusting max lines shown.
  - menu.numchatlines holds this line count, from 6 to 15.
- Uses C.TriggerInputFeedback to print notification messages when the
chat window opens/closes (based on hotkey press) and when the edit box
is activated.

New api plan:
- Dont edit the menu at all, only the global callbacks.
- Wrap ExecuteDebugCommand
  - If command matches api registered command, do api callback.
  - Otherwise, hand off to ego ExecuteDebugCommand.
- Replace OnlineSendChatMessage
  - Record the message into a local messages table.
- Replace OnlineGetChatMessages
  - Return the local messages table.
- Replace OnlineGetUserName
  - Return a generic name and id, eg. "self" and 0.
- API originating messages also go into the messages table.
- Message table should include the egosoft subfields.
  - Always set private=false, to avoid dealing with getMessageReceiverID,
    OnlineGetChatGroupUsers, etc.

Notes:
- ego menu scrollbar gets stale and is stuck on the line when the chat
window was open (and scrollbar first appeared), rescrolling window to that
line when selecting the edit box; need to close/reopen chat to reset line to 
the latest.
]]

-- FFI stuff.
local ffi = require("ffi")
local C = ffi.C
ffi.cdef[[
	int64_t GetCurrentUTCDataTime(void);
]]

local Lib = require("extensions.sn_mod_support_apis.ui.Library")

-- Local functions and data.
local debug = {
    print_messages = false,
}
local L = {
    ego_ExecuteDebugCommand = nil,
    ego_OnlineSendChatMessage = nil,
    ego_OnlineGetChatMessages = nil,
    ego_OnlineGetUserName = nil,

    -- List of messages, each a subtable with ego suitable fields.
    messages = {},

    -- Shortened commands that users may give.
    short_commands = {
        rui = "reloadui",
        rai = "refreshai",
        rmd = "refreshmd",
    },

    -- Names of known egosoft commands.
    ego_commands = {
        -- Give a non-nil value to keep key alive.
        reloadui = 1,
        refreshai = 1,
        refreshmd = 1,
    },

    -- Name to use for the player.
    user_name = "self",

    -- Name to use for messages from md api.
    mdapi_name = "md",
    
    -- Pairing of authors to ids (autogenerated).
    author_ids = {},
    next_id = 0,

    -- TODO: maybe ui setting for this.
    max_messages = 30,

    -- The last timestamp applied to a message.
    last_timestamp = 0,
}

-- Signalling results from lua to md.
-- args is nil or a list of space separated strings.
function L.Raise_Signal(name, args)
    AddUITriggeredEvent("Chat_Window_API", name, args)
end


function L.Init()
    RegisterEvent("Chat_Window_API.Print", L.onPrint)
    
    -- Look up the menu, store in this module's local.
    L.ego_menu = Lib.Get_Egosoft_Menu("ChatWindow")
    
    -- Trigger ID generation for default names, to consistently be 0 and 1.
    L.Get_Author_ID(L.user_name)
    L.Get_Author_ID(L.mdapi_name)
    
    -- Wrap/replace these globals.
    L.ego_ExecuteDebugCommand = ExecuteDebugCommand
    ExecuteDebugCommand = L.ExecuteDebugCommand
    
    L.ego_OnlineSendChatMessage = OnlineSendChatMessage
    OnlineSendChatMessage = L.OnlineSendChatMessage
    
    L.ego_OnlineGetChatMessages = OnlineGetChatMessages
    OnlineGetChatMessages = L.OnlineGetChatMessages
    
    L.ego_OnlineGetUserName = OnlineGetUserName
    OnlineGetUserName = L.OnlineGetUserName
end

function L.ExecuteDebugCommand(text, parameter)
    -- Swap text for special short commands.
    if L.short_commands[text] ~= nil then
        text = L.short_commands[text]
    end
    
    -- Signal the command specifically, for user script convenience.
    -- This way md scripts can detect /refreshmd, ai /refreshai.
    -- These signals should be processed on the next frame, so
    -- the script refresh will be complete.
    if text == "refreshmd" or text == "refreshai" then
        L.Raise_Signal(text)
    end

    -- TODO: get registered commands from md and only return to md if
    -- one seen. For now, just send all to md that arent known ego commands.
    if L.ego_commands[text] ~= nil then
        L.ego_ExecuteDebugCommand(text, parameter)
    else
        -- Construct terms list and send to md.
        -- Note: keep the / for this for now.
        local orig_command = '/'..text
        local terms = L.Get_Terms(parameter)
        table.insert(terms, 1, orig_command)
        local orig_text = orig_command
        if parameter ~= "" then
            orig_text = orig_text.." "..parameter
        end
        L.Raise_Signal("text_entered", {terms = terms, text = orig_text})
    end
end

function L.Get_Terms(text)
    -- Space separate terms.
    local terms = Lib.Split_String_Multi(text, " ")

    -- If double+ spacing was used, some of these terms are empty.
    -- Trim empty terms here.
    local terms_trimmed = {}
    for i, term in ipairs(terms) do
        if term ~= "" then
            table.insert(terms_trimmed, term)
        end
    end
    terms = terms_trimmed
    return terms
end

function L.OnlineSendChatMessage(text, userid)
    
    -- Skip empty text.
    if text == "" then return end

    if debug.print_messages then
        DebugError("Chatwindow user text: " .. text)
    end

    local terms = L.Get_Terms(text)
    
    -- If there are no terms (eg. user entered just spaces), ignore.
    if #terms == 0 then return end

    -- Echo this back to ai/md scope.
    L.Raise_Signal("text_entered", {terms = terms, text = text})
    
    L.Record_Message(text, L.user_name)
end

function L.OnlineGetChatMessages()
    return L.messages
end

function L.OnlineGetUserName()
    return L.user_name, L.Get_Author_ID(L.user_name)
end

-- Print a line sent from md.
-- TODO: support an author field.
function L.onPrint(_, text)
    if debug.print_messages then
        DebugError("Chatwindow md print: " .. text)
    end
    
    L.Record_Message(text, L.mdapi_name)

    -- Raise an event for the ego menu to hear, so it updates the window.
    L.ego_menu.onChatMessageReceived()
end

-- Get an id for an author.
function L.Get_Author_ID(author)
    if L.author_ids[author] == nil then
        L.author_ids[author] = L.next_id
        L.next_id = L.next_id + 1
    end
    return L.author_ids[author]
end

-- Record a message to the messages table.
function L.Record_Message(text, author)
    -- Remove the oldest message if at max.
    if #L.messages >= L.max_messages then
        table.remove(L.messages, 1)
    end

    --[[
    Note: ego code will sort messages based on timestamp, then author,
    then subline of wordwrap. If multiple messages are recorded on the same
    frame with the same timestamp, they can get randomly jumbled.
    As a workaround, always ensure timestamps increment by at least 1ms.
    ]]
    local timestamp = C.GetCurrentUTCDataTime() * 1000
    if timestamp <= L.last_timestamp then
        timestamp = L.last_timestamp + 1
    end
    L.last_timestamp = timestamp

    local new_message = {
        author = author,
        authorid = L.Get_Author_ID(author),
        time = tostring(timestamp),
        reported = false,
        text = text,
        isprivate = false,
        }
    table.insert(L.messages, new_message)
        
    if debug.print_messages then
        Lib.Print_Table(new_message, "Chatwindow stored message:")
    end

    -- Trigger the ego menu to update its messages.
    L.ego_menu.messagesOutdated = true
end


-- Old (pre-6.0) api commented out but kept for reference for now.
--[[
Notes on integrating with the chat window:
- All functions and data in chatwindow.lua are local.
- View.registerMenu is called with the onChatWindowCreated function, though
  this is done before mod lua can intercept it.
- View.menus can be used to access the onChatWindowCreated function.
- onChatWindowCreated attaches the onCommandBarDeactivated function to the
  editbox widget deactivation (eg. <enter>) using SetScript.
- If onChatWindowCreated is called before modded lua can run, then it cannot
  be intercepted, but this case can be detected using View.hasMenu and
  corrected.
  
TODO:
- Add text coloring support, similar to stock menu.
- Mimic the egosoft style of handling messages (eg. red color, ; separated and
  putting first term in brackets).
- Easy way to enable/disable for debug purposes.
- pcalls and such for better error handling.
- Detect wordwrap and plan around it to avoid text going outside the table.
- Find a way to suppress the ego ExecuteDebugCommand

]]
---- FFI stuff.
--local ffi = require("ffi")
--local C = ffi.C
--ffi.cdef[[
--    void SetEditBoxText(const int editboxid, const char* text);
--]]
--
---- Imports.
--local Lib = require("extensions.sn_mod_support_apis.ui.library")
--local Time = require("extensions.sn_mod_support_apis.ui.time.interface")
--local T = require("extensions.sn_mod_support_apis.ui.text")
--
---- Copy of ego config terms of interest.
--local config = {
--    maxOutputLines = 8,
--    textColor = {
--        ["command"]       = "#FFFFFFFF", -- white
--        ["directMessage"] = "#FFFF2B2B", -- bright red
--        ["otherMessage"]  = "#FFF2F200", -- yellow
--        ["ownMessage"]    = "#FF1B893C", -- dark green
--        ["serverMessage"] = "#FFAE02AE"  -- bright purple
--    },
--}
--
--local L = {
--    -- The original egosoft callback function on menu creation.
--    ego_onChatWindowCreated = nil,
--
--    -- List of recorded text lines entered.
--    text_lines = {},
--
--    -- The editbox widget.
--    edit_box = nil,
--    -- The text table widget. Has only one cell.
--    text_table = nil,
--
--    -- If this code should control the text display.
--    control_text = true,
--
--    -- Shortened commands that users may give.
--    short_commands = {
--        rui = "reloadui",
--        rai = "refreshai",
--        rmd = "refreshmd",
--    },
--}
--
---- Signalling results from lua to md.
---- args is nil or a list of space separated strings.
--function L.Raise_Signal(name, args)
--    AddUITriggeredEvent("Chat_Window_API", name, args)
--end
--
--
--function L.Init()
--    RegisterEvent("Chat_Window_API.Print", L.onPrint)
--    -- Also listen to the normal event, for compatability.
--    RegisterEvent("directChatMessageReceived", L.onPrint)
--    
--
--    -- The window could already have been created, or have yet to be created.
--    -- Aim to handle both cases.
--    -- Intercepting View.registerMenu to listen for new chat windows.
--    local ego_registerMenu = View.registerMenu
--    View.registerMenu = function(id, ...)
--        -- Let View do the initial setup.
--        ego_registerMenu(id, ...)
--        -- Use shared code to search for the chat window.
--        if id == "chatWindow" then
--            L.Patch_New_Menu()
--        end
--    end
--
--    -- The chat window might already be set up in View, so always run
--    -- this once, to find an exiting menu.
--    L.Patch_New_Menu()
--
--end
--
---- Search View for the chatWindow and patch it if found.
--function L.Patch_New_Menu()
--    local chat_menu = nil
--    for i, menu in ipairs(View.menus) do
--        if menu.id == "chatWindow" then
--            chat_menu = menu
--            break
--        end
--    end
--
--    -- If not found, this may be the init call with no shown window, so skip.
--    if chat_menu == nil then
--        return
--    end
--
--    -- Intercept the creation callback.
--    L.ego_onChatWindowCreated = chat_menu.callback
--    chat_menu.callback = L.onChatWindowCreated
--
--    -- Check if the chat window is already open.
--    if View.hasMenu({chatWindow = true}) then
--        -- Update it; triggers onChatWindowCreated callback linked above.
--        View.updateMenu(chat_menu)
--    end
--end
--
---- On creation, link to the window widgets, and set a callback editbox script.
--function L.onChatWindowCreated(frames)
--    -- Run the original setup function to connect its onHideChatWindow
--    -- and other links.
--    L.ego_onChatWindowCreated(frames)
--
--    -- Copy some of the setup code to record widgets.
--    local children = table.pack(GetChildren(frames[1]))
--    L.edit_box = GetCellContent(children[2], 1, 1)
--    L.text_table  = children[1]
--
--    -- Set the callback script. This is called in addition to the
--    -- egosoft onCommandBarDeactivated callback, but should be called after.
--    SetScript(L.edit_box, "onEditBoxDeactivated", L.onCommandBarDeactivated)
--
--    -- Overwrite the standard inintial window text (may have stuff if this
--    -- isn't the first time shown).
--    L.rebuildWindowOutput()
--end
--
--
---- Process edit box text when user presses <enter> on it.
--function L.onCommandBarDeactivated(_, text, _, wasConfirmed)
--    -- Skip if deactivated without confirmation (eg. <enter>).
--    if not wasConfirmed then return end
--    L.Process_Text(text)
--end
--
--function L.Process_Text(text)
--    -- Skip empty text.
--    if text == "" then return end
--
--    --DebugError("Chatwindow saw text: " .. text)
--    
--    if L.control_text then
--        -- Clear the edit box.
--        C.SetEditBoxText(L.edit_box, "")
--    end
--
--    -- Space separate terms.
--    local terms = Lib.Split_String_Multi(text, " ")
--
--    -- If double+ spacing was used, some of these terms are empty.
--    -- Trim empty terms here.
--    local terms_trimmed = {}
--    for i, term in ipairs(terms) do
--        if term ~= "" then
--            table.insert(terms_trimmed, term)
--        end
--    end
--    terms = terms_trimmed
--
--    -- If there are no terms (eg. user entered just spaces), ignore.
--    if #terms == 0 then return end
--
--    -- Echo this back to ai/md scope.
--    L.Raise_Signal("text_entered", {terms = terms, text = text})
--    
--    if L.control_text then
--        L.Add_Line(text)
--
--        -- Do some egosoft style processing.
--        -- This looks for a starting "/" for backend commands, and passes along
--        -- any potential parameters (not currently used in any actual commands).
--        if string.sub(terms[1], 1, 1) == "/" then
--            -- Remove the "/" from the command.
--            local command = string.sub(terms[1], 2, #terms[1])
--
--            -- Pack all remaining terms as a single parameter string.
--            local param = ""
--            if #terms > 1 then
--                for i = 2, #terms do
--                    param = param .. " " .. terms[i]
--                end
--            end
--
--            -- Special short commands.
--            if L.short_commands[command] ~= nil then
--                command = L.short_commands[command]
--                -- Hand off to backend for /refreshmd and similar.
--                -- Note: don't do this for the normal full command, since the
--                -- ego code will also check this string and run the command.
--                -- TODO: suppress the ego ExecuteDebugCommand and always do
--                -- it here, so the log doesn't have an unknown command error.
--                ExecuteDebugCommand(command, param)
--            end
--        
--
--            -- Signal the command specifically, for user script convenience.
--            -- This way md scripts can detect /refreshmd, ai /refreshai.
--            -- These signals should be processed on the next frame, so
--            -- the script refresh will be complete.
--            if command == "refreshmd" or command == "refreshai" then
--                L.Raise_Signal(command)
--            end
--        end
--    end
--end
--
--
---- Add a line to the text table.
---- Line will be formatted, and may be broken into multiple lines by word wrap.
---- Oldest lines will be removed, past the textbox line limit.
--function L.Add_Line(line)
--
--    -- Format the line; expect to maybe get newlines back.
--    -- TODO: think about this. Also consider if original text has newlines.
--    local f_line = line
--
--    -- Split and add to existing text lines.
--    local sublines = Lib.Split_String_Multi(f_line, "\n")
--    for i, subline in ipairs(sublines) do
--        table.insert(L.text_lines, subline)
--    end
--
--    -- Remove older entries.
--    if #L.text_lines > config.maxOutputLines then
--        local new_text_lines = {}
--        for i = #L.text_lines - config.maxOutputLines + 1, #L.text_lines do
--            table.insert(new_text_lines, L.text_lines[i])
--        end
--        L.text_lines = new_text_lines
--    end
--
--    -- Update the text window.
--    L.rebuildWindowOutput()
--end
--
---- Print a line sent from md.
--function L.onPrint(_, text)
--    -- Ignore if not controlling the text.
--    if not L.control_text then return end
--    L.Add_Line(text)
--end
--
--
---- On each update, do a fresh rebuild of the window text.
---- This works somewhat differently than the ego code, aiming to fix an ego
---- problem when text wordwraps (in ego code causes it to print outside/below
---- the text window).
--function L.rebuildWindowOutput()
--
--    -- Skip if the table isn't set up yet.
--    if L.text_table == nil then return end
--
--    -- Merge the lines into one string.
--    local text = ""
--    for i, line in ipairs(L.text_lines) do
--        text = text .. "\n" .. line
--    end
--
--    -- Jump a couple hoops to update the table cell. Copy/edit of ego code.
--    local contentDescriptor = CreateFontString(text, "left", 255, 255, 255, 100, "Zekton", 10, true, 0, 0, 160)
--    local success = SetCellContent(L.text_table, contentDescriptor, 1, 1)
--    if not success then
--        DebugError("ChatWindow error - failed to update output.")
--    end
--    ReleaseDescriptor(contentDescriptor)
--end

Register_OnLoad_Init(L.Init, "extensions.sn_mod_support_apis.ui.chat_window.Interface")