-- company.lua
-- ============================================================================
-- Text completion framework for lumacs.
-- Similar to Emacs company-mode.
--
-- Features:
-- - Multiple completion backends (words, buffer, path, etc.)
-- - Completion popup display in echo area
-- - Configurable triggers and keybindings
-- ============================================================================

local company = {}

-- Configuration
company.config = {
    enabled = true,
    idle_delay_ms = 500,       -- Delay before showing completions
    min_prefix_length = 2,    -- Minimum chars before completing
    max_candidates = 10,       -- Max candidates to show
    backends = {"buffer", "words"},  -- Active backends in order
    auto_complete = false,     -- Auto-show completions while typing
}

-- State
company.active = false
company.candidates = {}
company.selected_index = 1
company.prefix = ""
company.prefix_start_col = 0

-- ============================================================================
-- Backend implementations
-- ============================================================================

company.backends = {}

-- Buffer backend: Complete from words in current buffer
company.backends.buffer = {
    name = "buffer",

    get_candidates = function(prefix)
        local candidates = {}
        local seen = {}
        local buf = editor.buffer
        local line_count = buf:line_count()

        prefix = prefix:lower()

        for i = 0, line_count - 1 do
            local line = buf:line(i)
            -- Extract words (alphanumeric + underscore)
            for word in line:gmatch("[%w_]+") do
                local word_lower = word:lower()
                if #word >= company.config.min_prefix_length and
                   word_lower:sub(1, #prefix) == prefix and
                   not seen[word_lower] then
                    seen[word_lower] = true
                    table.insert(candidates, {
                        text = word,
                        description = "buffer",
                    })
                end
            end
        end

        return candidates
    end
}

-- Words backend: Complete from a dictionary of common words
company.backends.words = {
    name = "words",

    -- Common programming keywords
    keywords = {
        "function", "return", "if", "else", "elseif", "then", "end",
        "for", "while", "do", "repeat", "until", "break", "continue",
        "local", "true", "false", "nil", "and", "or", "not",
        "class", "struct", "enum", "interface", "public", "private", "protected",
        "static", "const", "final", "abstract", "virtual", "override",
        "import", "export", "require", "module", "package",
        "try", "catch", "throw", "finally", "except", "raise",
        "async", "await", "yield", "defer",
        "self", "this", "super", "new", "delete",
        "int", "float", "double", "string", "bool", "void", "char",
        "array", "list", "map", "set", "dict", "table", "vector",
        "print", "println", "printf", "console", "log", "debug", "error",
    },

    get_candidates = function(prefix)
        local candidates = {}
        prefix = prefix:lower()

        for _, word in ipairs(company.backends.words.keywords) do
            if word:sub(1, #prefix) == prefix then
                table.insert(candidates, {
                    text = word,
                    description = "keyword",
                })
            end
        end

        return candidates
    end
}

-- Path backend: Complete file paths
company.backends.path = {
    name = "path",

    get_candidates = function(prefix)
        local candidates = {}

        -- Only trigger on path-like prefixes
        if not prefix:match("^[%.~/]") and not prefix:match("/") then
            return candidates
        end

        -- Expand ~ to home directory
        local path = prefix:gsub("^~", os.getenv("HOME") or "~")

        -- Get directory and partial filename
        local dir, partial
        if path:match("/$") then
            dir = path
            partial = ""
        else
            dir = path:match("(.+)/") or "."
            partial = path:match("[^/]+$") or ""
        end

        -- List directory contents
        local cmd = string.format("ls -1 '%s' 2>/dev/null", dir)
        local handle = io.popen(cmd)
        if not handle then
            return candidates
        end

        partial = partial:lower()

        for entry in handle:lines() do
            if entry:lower():sub(1, #partial) == partial then
                local full_path
                if prefix:match("^~") then
                    full_path = "~" .. dir:sub(#(os.getenv("HOME") or "") + 1) .. "/" .. entry
                else
                    full_path = dir .. "/" .. entry
                end

                table.insert(candidates, {
                    text = full_path,
                    description = "path",
                })
            end
        end

        handle:close()
        return candidates
    end
}

-- ============================================================================
-- Core completion logic
-- ============================================================================

-- Get the word prefix at cursor
function company.get_prefix()
    local buf = editor.buffer
    local cursor = editor.cursor
    local line = buf:line(cursor.line)

    -- Find word start (alphanumeric + underscore, or path chars)
    local col = cursor.column
    local start = col

    while start > 0 do
        local c = line:sub(start, start)
        if c:match("[%w_%.~/]") then
            start = start - 1
        else
            break
        end
    end

    local prefix = line:sub(start + 1, col)
    return prefix, start
end

-- Gather candidates from all active backends
function company.gather_candidates(prefix)
    local all_candidates = {}

    for _, backend_name in ipairs(company.config.backends) do
        local backend = company.backends[backend_name]
        if backend and backend.get_candidates then
            local candidates = backend.get_candidates(prefix)
            for _, c in ipairs(candidates) do
                c.backend = backend_name
                table.insert(all_candidates, c)
            end
        end
    end

    -- Sort by text
    table.sort(all_candidates, function(a, b)
        return a.text < b.text
    end)

    -- Remove duplicates (keep first occurrence)
    local seen = {}
    local unique = {}
    for _, c in ipairs(all_candidates) do
        if not seen[c.text] then
            seen[c.text] = true
            table.insert(unique, c)
        end
    end

    return unique
end

-- Format candidates for echo area display
function company.format_display()
    local lines = {}
    local start_idx = 1
    local end_idx = math.min(#company.candidates, company.config.max_candidates)

    -- Adjust window if selected is out of view
    if company.selected_index > end_idx then
        start_idx = company.selected_index - company.config.max_candidates + 1
        end_idx = company.selected_index
    end

    for i = start_idx, end_idx do
        local c = company.candidates[i]
        local marker = (i == company.selected_index) and ">" or " "
        local line = string.format("%s %s", marker, c.text)
        if c.description then
            line = line .. " (" .. c.description .. ")"
        end
        table.insert(lines, line)
    end

    if #company.candidates > company.config.max_candidates then
        table.insert(lines, string.format("  [%d/%d]", company.selected_index, #company.candidates))
    end

    return lines
end

-- Show completion popup
function company.show()
    if not company.active or #company.candidates == 0 then
        return
    end

    local lines = company.format_display()
    editor:set_echo_area(lines)
end

-- Hide completion popup
function company.hide()
    if company.active then
        company.active = false
        company.candidates = {}
        company.selected_index = 1
        editor:clear_echo_area()
    end
end

-- Start completion at point
function company.complete()
    if not company.config.enabled then
        return
    end

    company.prefix, company.prefix_start_col = company.get_prefix()

    if #company.prefix < company.config.min_prefix_length then
        company.hide()
        return
    end

    company.candidates = company.gather_candidates(company.prefix)

    if #company.candidates == 0 then
        company.hide()
        return
    end

    company.active = true
    company.selected_index = 1
    company.show()
end

-- Select next candidate
function company.select_next()
    if not company.active or #company.candidates == 0 then
        return
    end

    company.selected_index = (company.selected_index % #company.candidates) + 1
    company.show()
end

-- Select previous candidate
function company.select_previous()
    if not company.active or #company.candidates == 0 then
        return
    end

    company.selected_index = ((company.selected_index - 2) % #company.candidates) + 1
    company.show()
end

-- Insert selected candidate
function company.insert_selection()
    if not company.active or #company.candidates == 0 then
        return false
    end

    local candidate = company.candidates[company.selected_index]
    if not candidate then
        return false
    end

    -- Delete the prefix and insert the completion
    local buf = editor.buffer
    local cursor = editor.cursor
    local delete_start = lumacs.Position(cursor.line, company.prefix_start_col)
    local delete_end = cursor

    if delete_start.column < delete_end.column then
        buf:erase(lumacs.Range(delete_start, delete_end))
    end

    -- Insert at the start position
    buf:insert(delete_start, candidate.text)

    -- Move cursor to end of inserted text
    editor.cursor = lumacs.Position(cursor.line, company.prefix_start_col + #candidate.text)

    company.hide()
    return true
end

-- Abort completion
function company.abort()
    company.hide()
end

-- ============================================================================
-- Idle hook for auto-completion
-- ============================================================================

function company.check_idle()
    if not company.config.enabled or not company.config.auto_complete then
        return
    end

    -- Only auto-complete if we've been idle long enough
    local idle_ms = editor:idle_time_ms()
    if idle_ms < company.config.idle_delay_ms then
        return
    end

    -- Don't auto-complete if minibuffer is active
    if editor.minibuffer_manager and editor.minibuffer_manager:is_active() then
        return
    end

    -- Don't auto-complete if already showing
    if company.active then
        return
    end

    -- Try to complete
    company.complete()
end

-- ============================================================================
-- Commands
-- ============================================================================

editor:register_command("company-complete", "Start completion at point", function(args)
    company.complete()
    return {success = true}
end, {"complete"})

editor:register_command("company-select-next", "Select next completion candidate", function(args)
    company.select_next()
    return {success = true}
end)

editor:register_command("company-select-previous", "Select previous completion candidate", function(args)
    company.select_previous()
    return {success = true}
end)

editor:register_command("company-complete-selection", "Insert selected completion", function(args)
    if company.insert_selection() then
        return {success = true, message = "Completion inserted"}
    else
        return {success = false, message = "No completion active"}
    end
end)

editor:register_command("company-abort", "Cancel completion", function(args)
    company.abort()
    return {success = true}
end)

editor:register_command("company-mode", "Toggle company-mode", function(args)
    company.config.enabled = not company.config.enabled
    if company.config.enabled then
        editor:message("company-mode enabled")
    else
        company.hide()
        editor:message("company-mode disabled")
    end
    return {success = true}
end)

editor:register_command("company-auto-complete", "Toggle auto-completion", function(args)
    company.config.auto_complete = not company.config.auto_complete
    if company.config.auto_complete then
        editor:message("Auto-completion enabled")
    else
        editor:message("Auto-completion disabled")
    end
    return {success = true}
end)

editor:register_command("company-backends", "List or set completion backends", function(args)
    if #args == 0 then
        return {success = true, message = "Backends: " .. table.concat(company.config.backends, ", ")}
    end

    -- Parse comma-separated list
    local backends = {}
    for backend in args[1]:gmatch("[^,]+") do
        backend = backend:match("^%s*(.-)%s*$")  -- trim
        if company.backends[backend] then
            table.insert(backends, backend)
        else
            return {success = false, message = "Unknown backend: " .. backend}
        end
    end

    company.config.backends = backends
    return {success = true, message = "Backends set to: " .. table.concat(backends, ", ")}
end, {}, true, "s")

-- ============================================================================
-- Keybindings
-- ============================================================================

editor:bind_key("M-Tab", "company-complete", "Complete at point")
editor:bind_key("C-n", function()
    if company.active then
        company.select_next()
    else
        editor:move_down()
    end
end, "Next line or completion")

editor:bind_key("C-p", function()
    if company.active then
        company.select_previous()
    else
        editor:move_up()
    end
end, "Previous line or completion")

-- Note: These override default behavior when completion is active
-- Tab to complete selection when active
editor:bind_key("Tab", function()
    if company.active then
        company.insert_selection()
    else
        -- Default tab behavior (insert spaces)
        editor.buffer:insert(editor.cursor, "    ")
        for i = 1, 4 do editor:move_right() end
    end
end, "Tab or complete")

-- Escape to abort
editor:bind_key("Escape", function()
    if company.active then
        company.abort()
    else
        editor:execute_command("keyboard-quit", {})
    end
end, "Abort or quit")

-- Register idle hook
lumacs.company_check_idle = company.check_idle

-- Store module
lumacs.company = company

return company
