-- ffi/readline.lua — GNU readline binding.
-- Phase 0: readline + add_history + EOF handling.
-- Phase 1: custom key bindings via rl_bind_keyseq.
-- Phase 3: rl_insert_text + rl_redisplay so bound key handlers can
--          stuff text into the in-progress line buffer (used by \C-n
--          to insert ":norris " in repl.lua).
-- See docs/PHASE0.md §9 and docs/PHASE1.md §7 and docs/PHASE3.md §3.

local ffi = require("ffi")

ffi.cdef[[
char *readline(const char *prompt);
void  add_history(const char *line);
void  free(void *ptr);

typedef int (*rl_command_func_t)(int, int);
int rl_bind_keyseq(const char *keyseq, rl_command_func_t function);
int rl_insert_text(const char *text);
int rl_redisplay(void);
]]

-- libreadline-dev (which ships the unversioned `libreadline.so` symlink) is
-- not assumed to be installed on the runtime host; fall back to versioned
-- sonames so a base Debian/Arch with just libreadline runtime works.
local function load_readline()
    local errs = {}
    for _, name in ipairs({"readline", "readline.so.8", "readline.so.7"}) do
        local ok, lib = pcall(ffi.load, name)
        if ok then return lib end
        errs[#errs+1] = name .. ": " .. tostring(lib)
    end
    error("libreadline not loadable: " .. table.concat(errs, "; "))
end

local rl = load_readline()
local C  = ffi.C

local M = {}

-- Read one line of input.
-- Returns:
--   string : the line (no trailing newline)
--   nil    : EOF (Ctrl-D on empty line)
function M.readline(prompt)
    local cstr = rl.readline(prompt)
    if cstr == nil then return nil end
    local s = ffi.string(cstr)
    C.free(cstr)
    return s
end

-- Append a non-empty line to readline's in-memory history.
function M.add_history(line)
    if line and #line > 0 then
        rl.add_history(line)
    end
end

-- Bind `seq` (e.g. "\\C-n") to a Lua function that runs when the user types
-- that key sequence at the readline prompt. The Lua fn takes no arguments
-- (readline passes count + key, but consumers don't need them).
-- Callback trampolines are pinned in module-local state for process
-- lifetime. We do NOT free the previous binding on rebind: readline
-- retains the function pointer in its keymap, and the window between
-- :free() and the new rl_bind_keyseq is a potential use-after-free.
-- Memory cost is bounded — one closure per bound key sequence.
-- (Phase 3 R-C4 fold-in.)
-- `_pinned` keeps every callback ever cast alive for process lifetime
-- (so readline's keymap pointers never dangle even after a re-bind).
-- `_bound` indexes by seq for "what's currently bound here" lookup but
-- both old and new closures stay reachable via _pinned.
local _bound  = {}
local _pinned = {}

function M.bind(seq, fn)
    local cb = ffi.cast("rl_command_func_t", function(_count, _key)
        local ok, err = pcall(fn)
        if not ok then
            io.stderr:write("ffi/readline bind handler error: " .. tostring(err) .. "\n")
        end
        return 0
    end)
    _pinned[#_pinned + 1] = cb       -- never freed; bounded by N rebinds
    local rc = rl.rl_bind_keyseq(seq, cb)
    _bound[seq] = cb
    return rc == 0
end

-- Insert `text` at the cursor in the in-progress readline buffer.
-- Used by bound key handlers to stuff e.g. ":norris " into the line.
-- Caller typically follows with M.redisplay() to refresh the display.
function M.insert_text(text)
    if text and text ~= "" then
        rl.rl_insert_text(text)
    end
end

-- Force readline to redraw the current line. Call after insert_text or
-- any other buffer mutation from inside a bound handler.
function M.redisplay()
    rl.rl_redisplay()
end

return M
