--
-- Chat (PocketOS Store app)
--
-- Direct messages between PocketOS pockets over the ender modem
-- (chat channel from OS config "PingChannel").
--
-- Self-contained protocol: { t="pkt.chat/1", from, from_name, to, to_name, text }
--   to = computer id (number) or "all" (broadcast)
--
-- A background modem handler stores incoming messages in the app's
-- config folder even while the app is closed (offline inbox).
--

local core = core
local cpair = core.cpair
local ALIGN = core.ALIGN

local container = pocketos.container
local os_config = pocketos.os_config

local my_id = os.getComputerID()
local my_name = os_config.get("UserName") or "Player"
local channel = os_config.get("PingChannel") or 54124

local modem = peripheral.find("modem")

--#region PERSISTENCE (app config folder)

local function cfg_load()
    return pocketos.config.load()
end

local function cfg_save(cfg)
    pocketos.config.save(cfg)
end

local function normalize_conv(cfg)
    if type(cfg.contacts) ~= "table" then cfg.contacts = {} end
    if type(cfg.convs) ~= "table" then cfg.convs = {} end
    return cfg
end

--#endregion

--#region PROTOCOL

local function send_msg(to_id, text)
    if not modem then return false, "no modem" end
    if not text or #text == 0 then return false, "empty" end

    pcall(modem.open, channel)

    local payload = {
        t = "pkt.chat/1",
        from = my_id,
        from_name = my_name,
        to = to_id,
        to_name = "unknown",
        text = text,
    }

    local ok, err = pcall(modem.transmit, channel, channel, textutils.serialiseJSON(payload))
    if not ok then return false, err end
    return true
end

local function parse_msg(payload)
    if type(payload) ~= "string" then return nil end

    local ok, msg = pcall(textutils.unserialiseJSON, payload)
    if not ok or type(msg) ~= "table" or msg.t ~= "pkt.chat/1" then return nil end

    -- addressed to us (or broadcast), not from us
    if (msg.to == my_id or msg.to == "all") and msg.from ~= my_id then
        return {
            from = msg.from,
            from_name = msg.from_name or ("#" .. tostring(msg.from)),
            text = msg.text or "",
            time = os.epoch("utc") / 1000,
        }
    end
    return nil
end

-- store an incoming message in the persistent inbox + conversation
local function store_incoming(msg)
    local cfg = normalize_conv(cfg_load())

    -- auto-add unknown senders as contacts
    local known = false
    for _, c in ipairs(cfg.contacts) do
        if c.id == msg.from then
            known = true
            if msg.from_name and msg.from_name ~= "" then c.name = msg.from_name end
            break
        end
    end
    if not known then
        table.insert(cfg.contacts, { id = msg.from, name = msg.from_name })
    end

    local key = tostring(msg.from)
    local conv = type(cfg.convs[key]) == "table" and cfg.convs[key] or {}
    table.insert(conv, { from = true, text = msg.text, time = msg.time })
    cfg.convs[key] = conv

    cfg_save(cfg)
    return cfg
end

--#endregion

--#region UI

local function init_container()
    local page_pane

    -- forward declarations
    local redraw_convs, redraw_chat, redraw_contacts, refresh_ui

    local btn_fg_bg  = cpair(colors.lightBlue, colors.black)
    local btn_active = cpair(colors.white, colors.black)
    local label      = cpair(colors.lightGray, colors.black)

    local cfg = normalize_conv(cfg_load())

    local open_contact = nil

    --#region PAGE 1: CONVERSATIONS

    local home = Div{parent=container, y=1}

    TextBox{parent=home, y=1, text="Chat", alignment=ALIGN.CENTER, fg_bg=cpair(colors.lightBlue, colors._INHERIT)}

    local my_info = TextBox{parent=home, y=2, text="you: " .. my_name .. " #" .. my_id,
        fg_bg=cpair(colors.gray, colors._INHERIT)}

    local conv_list = ListBox{parent=home, y=4, height=12, scroll_height=100,
        nav_fg_bg=cpair(colors.lightGray, colors.gray), nav_active=cpair(colors.white, colors.gray)}

    redraw_convs = function ()
        conv_list.remove_all()

        cfg = normalize_conv(cfg_load())

        if #cfg.contacts == 0 then
            TextBox{parent=conv_list, text="No contacts yet.", fg_bg=cpair(colors.gray, colors._INHERIT)}
            TextBox{parent=conv_list, text="Add one in Contacts.", fg_bg=cpair(colors.gray, colors._INHERIT)}
        end

        for _, contact in ipairs(cfg.contacts) do
            local key = tostring(contact.id)
            local conv = type(cfg.convs[key]) == "table" and cfg.convs[key] or {}
            local last = conv[#conv]
            local preview = last and string.sub(last.text, 1, 16) or "no messages"

            local row = Div{parent=conv_list, height=2}
            PushButton{parent=row, x=1, y=1, text=string.sub(contact.name or "?", 1, 10) .. " (" .. #conv .. ")",
                fg_bg=btn_fg_bg, active_fg_bg=btn_active,
                callback=function ()
                    open_contact = contact
                    redraw_chat()
                    page_pane.set_value(2)
                end}
            TextBox{parent=row, x=1, y=2, text=preview, fg_bg=cpair(colors.gray, colors._INHERIT)}
            local _ = Div{parent=conv_list, height=1}
        end
    end

    PushButton{parent=home, y=17, text="Contacts        >", fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=function ()
            redraw_contacts()
            page_pane.set_value(3)
        end}

    --#endregion

    --#region PAGE 2: ONE CONVERSATION

    local chat_page = Div{parent=container, y=1}

    local chat_title = TextBox{parent=chat_page, y=1, text="(contact)", alignment=ALIGN.CENTER,
        fg_bg=cpair(colors.lightBlue, colors._INHERIT)}

    PushButton{parent=chat_page, x=1, y=1, text="<",
        fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=function ()
            page_pane.set_value(1)
            redraw_convs()
        end}

    local msg_list = ListBox{parent=chat_page, y=3, height=11, scroll_height=500,
        nav_fg_bg=cpair(colors.lightGray, colors.gray), nav_active=cpair(colors.white, colors.gray)}

    local send_status = TextBox{parent=chat_page, y=2, x=13, text="", fg_bg=cpair(colors.gray, colors._INHERIT)}

    redraw_chat = function ()
        if not open_contact then return end
        chat_title.set_value(string.sub(open_contact.name or "?", 1, 20))

        msg_list.remove_all()

        local key = tostring(open_contact.id)
        local conv = type(cfg.convs[key]) == "table" and cfg.convs[key] or {}

        if #conv == 0 then
            TextBox{parent=msg_list, text="say hi!", fg_bg=cpair(colors.gray, colors._INHERIT)}
        end

        for _, m in ipairs(conv) do
            local prefix = m.from and "<" or ">"
            local color = m.from and colors.cyan or colors.lime
            TextBox{parent=msg_list, text=prefix .. " " .. string.sub(m.text, 1, 20),
                fg_bg=cpair(color, colors._INHERIT)}
        end
    end

    local input_field = TextField{parent=chat_page, y=15, width=17, max_len=120,
        fg_bg=cpair(colors.white, colors.gray)}

    PushButton{parent=chat_page, x=19, y=15, text="Send",
        fg_bg=cpair(colors.green, colors.black), active_fg_bg=btn_active,
        callback=function ()
            if not open_contact then return end
            local text = input_field.get_value()
            if not text or #text == 0 then return end

            local ok, err = send_msg(open_contact.id, text)
            if ok then
                local key = tostring(open_contact.id)
                local conv = type(cfg.convs[key]) == "table" and cfg.convs[key] or {}
                table.insert(conv, { from = false, text = text, time = os.epoch("utc") / 1000 })
                cfg.convs[key] = conv
                cfg_save(cfg)
                input_field.set_value("")
                send_status.set_value("sent")
                redraw_chat()
            else
                send_status.set_value(tostring(err))
            end
        end}

    PushButton{parent=chat_page, y=17, text="Clear history", min_width=8,
        fg_bg=cpair(colors.red, colors.black), active_fg_bg=btn_active,
        callback=function ()
            if open_contact then
                cfg.convs[tostring(open_contact.id)] = {}
                cfg_save(cfg)
                redraw_chat()
            end
        end}

    --#endregion

    --#region PAGE 3: CONTACTS

    local contacts_page = Div{parent=container, y=1}

    TextBox{parent=contacts_page, y=1, text="Contacts", alignment=ALIGN.CENTER,
        fg_bg=cpair(colors.lightBlue, colors._INHERIT)}

    PushButton{parent=contacts_page, x=1, y=1, text="<",
        fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=function () page_pane.set_value(1) end}

    local contact_list = ListBox{parent=contacts_page, y=3, height=8, scroll_height=100,
        nav_fg_bg=cpair(colors.lightGray, colors.gray), nav_active=cpair(colors.white, colors.gray)}

    redraw_contacts = function ()
        contact_list.remove_all()

        cfg = normalize_conv(cfg_load())

        if #cfg.contacts == 0 then
            TextBox{parent=contact_list, text="no contacts", fg_bg=cpair(colors.gray, colors._INHERIT)}
        end

        for _, contact in ipairs(cfg.contacts) do
            local row = Div{parent=contact_list, height=2}
            TextBox{parent=row, text=(contact.name or "?") .. " (#" .. contact.id .. ")",
                fg_bg=cpair(colors.white, colors._INHERIT)}
            PushButton{parent=row, x=1, y=2, text="remove",
                fg_bg=cpair(colors.red, colors.black), active_fg_bg=btn_active,
                callback=function ()
                    cfg = normalize_conv(cfg_load())
                    for i, c in ipairs(cfg.contacts) do
                        if c.id == contact.id then
                            table.remove(cfg.contacts, i)
                            break
                        end
                    end
                    cfg_save(cfg)
                    redraw_contacts()
                end}
            local _ = Div{parent=contact_list, height=1}
        end
    end

    -- add contact form
    TextBox{parent=contacts_page, y=12, text="Add contact (their About app", fg_bg=label}
    TextBox{parent=contacts_page, y=13, text="shows their computer id)", fg_bg=label}

    local id_field = NumberField{parent=contacts_page, y=14, width=6, default=0, min=0, max=65535,
        max_int_digits=5, fg_bg=cpair(colors.white, colors.gray)}

    local name_field = TextField{parent=contacts_page, y=15, width=17, max_len=16,
        fg_bg=cpair(colors.white, colors.gray)}

    local contact_status = TextBox{parent=contacts_page, y=17, text="", fg_bg=label}

    PushButton{parent=contacts_page, x=19, y=15, text="Add",
        fg_bg=cpair(colors.green, colors.black), active_fg_bg=btn_active,
        callback=function ()
            local id = math.floor(tonumber(id_field.get_value()) or 0)
            local name = name_field.get_value()

            if id <= 0 then
                contact_status.set_value("bad computer id")
                return
            end
            if not name or #name == 0 then
                contact_status.set_value("name required")
                return
            end
            if id == my_id then
                contact_status.set_value("that's yourself!")
                return
            end

            cfg = normalize_conv(cfg_load())
            table.insert(cfg.contacts, { id = id, name = name })
            cfg_save(cfg)

            name_field.set_value("")
            id_field.set_value(0)
            contact_status.set_value("added " .. name)
            redraw_contacts()
        end}

    --#endregion

    --#region LIVE UPDATES

    -- refresh whichever page is visible with new cfg state
    refresh_ui = function ()
        -- keep our local cfg in sync with what background storage wrote
        cfg = normalize_conv(cfg_load())
        if open_contact then
            redraw_chat()
        else
            redraw_convs()
        end
    end

    -- register handlers: live while open, inbox while closed
    pocketos.set_handlers{
        -- while the chat app is open
        on_modem = function (_channel, payload, _distance)
            local msg = parse_msg(payload)
            if msg then
                store_incoming(msg)
                refresh_ui()
            end
        end,
        -- always (messages land in the app config even when closed)
        background_modem = function (_channel, payload, _distance)
            local msg = parse_msg(payload)
            if msg then
                store_incoming(msg)
            end
        end,
    }

    --#endregion

    -- build panes and show home
    page_pane = MultiPane{parent=container, y=1, panes={ home, chat_page, contacts_page }}
    page_pane.set_value(1)

    redraw_convs()
    redraw_contacts()
end

init_container()
