local M = {}
local icons = require("config.icons")

function M.set_title()
    local title = " %t"
    local f = io.popen([[zsh -c '
        source $XDG_CONFIG_HOME/zsh/configs/autogenerated/hashes
        print -Pn "$USER@$HOST [%3~] "
    ']])
    if f ~= nil then
        title = f:read("*a") or ""
        title = title .. " %t"
        f:close()
    end
    vim.opt.titlestring = title
end

function M.buf_close(bufnr, force)
    local kill_command = "bd"

    local bo = vim.bo
    local api = vim.api
    local fnamemodify = vim.fn.fnamemodify

    if bufnr == 0 or bufnr == nil then
        bufnr = api.nvim_get_current_buf()
    end

    local bufname = api.nvim_buf_get_name(bufnr)

    if not force then
        local warning
        if bo[bufnr].modified then
            warning = string.format([[No write since last change for (%s)]], fnamemodify(bufname, ":t"))
        elseif api.nvim_buf_get_option(bufnr, "buftype") == "terminal" then
            warning = string.format([[Terminal %s will be killed]], bufname)
        end
        if warning then
            vim.ui.input({
                prompt = string.format([[%s. Close it anyway? [y]es or [n]o (default: no): ]], warning),
            }, function(choice)
                if choice:match "ye?s?" then force = true end
            end)
            if not force then return end
        end
    end

    -- Get list of window IDs with the buffer to close
    local windows = vim.tbl_filter(function(win)
        return api.nvim_win_get_buf(win) == bufnr
    end, api.nvim_list_wins())

    if #windows == 0 then return end

    if force then
        kill_command = kill_command .. "!"
    end

    -- Get list of active buffers
    local buffers = vim.tbl_filter(function(buf)
        return api.nvim_buf_is_valid(buf) and bo[buf].buflisted
    end, api.nvim_list_bufs())

    -- If there is only one buffer (which has to be the current one), vim will
    -- create a new buffer on :bd.
    -- For more than one buffer, pick the previous buffer (wrapping around if necessary)
    if #buffers > 1 then
        for i, v in ipairs(buffers) do
            if v == bufnr then
                local prev_buf_idx = i == 1 and (#buffers - 1) or (i - 1)
                local prev_buffer = buffers[prev_buf_idx]
                for _, win in ipairs(windows) do
                    api.nvim_win_set_buf(win, prev_buffer)
                end
            end
        end
    else
        vim.cmd('q!')
    end

    -- Check if buffer still exists, to ensure the target buffer wasn't killed
    -- due to options like bufhidden=wipe.
    if api.nvim_buf_is_valid(bufnr) and bo[bufnr].buflisted then
        vim.cmd(string.format("%s %d", kill_command, bufnr))
    end
end

function M.is_available(plugin)
    local lazy_config_avail, lazy_config = pcall(require, "lazy.core.config")
    return lazy_config_avail and lazy_config.spec.plugins[plugin] ~= nil
end

function M.empty_map_table()
    local maps = {}
    for _, mode in ipairs { "", "n", "v", "x", "s", "o", "!", "i", "l", "c", "t" } do
        maps[mode] = {}
    end
    -- TODO: Check this on 0.10.0 release
    if vim.fn.has "nvim-0.10.0" == 1 then
        for _, abbr_mode in ipairs { "ia", "ca", "!a" } do
            maps[abbr_mode] = {}
        end
    end
    return maps
end

function M.toggle_term_cmd(opts)
    if not vim.g.user_terminals then
        vim.g.user_terminals = {}
    end
    local terms = vim.g.user_terminals
    -- if a command string is provided, create a basic table for Terminal:new() options
    if type(opts) == "string" then opts = { cmd = opts, hidden = true } end
    local num = vim.v.count > 0 and vim.v.count or 1
    -- if terminal doesn't exist yet, create it
    if not terms[opts.cmd] then terms[opts.cmd] = {} end
    if not terms[opts.cmd][num] then
        if not opts.count then opts.count = vim.tbl_count(terms) * 100 + num end
        if not opts.on_exit then opts.on_exit = function() terms[opts.cmd][num] = nil end end
        terms[opts.cmd][num] = require("toggleterm.terminal").Terminal:new(opts)
    end
    -- toggle the terminal
    terms[opts.cmd][num]:toggle()
end

-- TODO: test this
function M.file_worktree(file, worktrees)
    worktrees = worktrees or vim.g.git_worktrees
    if not worktrees then return end
    file = file or vim.fn.expand("%")
    for _, worktree in ipairs(worktrees) do
        local r = vim.fn.system({
            "git",
            "--work-tree",
            worktree.toplevel,
            "--git-dir",
            worktree.gitdir,
            "ls-files",
            "--error-unmatch",
            file
        }):wait()
        if r.code == 0 then
            return worktree
        end
    end
end

function M.which_key_register()
    if M.which_key_queue then
        local wk_avail, wk = pcall(require, "which-key")
        if wk_avail then
            for mode, registration in pairs(M.which_key_queue) do
                wk.register(registration, { mode = mode })
            end
            M.which_key_queue = nil
        end
    end
end

function M.set_mappings(map_table, base)
    base = base or {}
    for mode, maps in pairs(map_table) do
        for keymap, options in pairs(maps) do
            if options then
                local cmd = options
                local keymap_opts = base
                if type(options) == "table" then
                    cmd = options[1]
                    keymap_opts = vim.tbl_deep_extend("force", keymap_opts, options)
                    keymap_opts[1] = nil
                end
                if not cmd or keymap_opts.name then -- which-key mapping
                    if not keymap_opts.name then keymap_opts.name = keymap_opts.desc end
                    if not M.which_key_queue then M.which_key_queue = {} end
                    if not M.which_key_queue[mode] then M.which_key_queue[mode] = {} end
                    M.which_key_queue[mode][keymap] = keymap_opts
                else -- not which-key mapping
                    vim.keymap.set(mode, keymap, cmd, keymap_opts)
                end
            end
        end
    end
    if package.loaded["which-key"] then M.which_key_register() end
end

local function del_buffer_autocmd(augroup, bufnr)
    local cmds_found, cmds = pcall(vim.api.nvim_get_autocmds, { group = augroup, buffer = bufnr })
    if cmds_found then vim.tbl_map(function(cmd) vim.api.nvim_del_autocmd(cmd.id) end, cmds) end
end

local function add_buffer_autocmd(augroup, bufnr, autocmds)
    if not vim.tbl_islist(autocmds) then autocmds = { autocmds } end
    local cmds_found, cmds = pcall(vim.api.nvim_get_autocmds, { group = augroup, buffer = bufnr })
    if not cmds_found or vim.tbl_isempty(cmds) then
        vim.api.nvim_create_augroup(augroup, { clear = false })
        for _, autocmd in ipairs(autocmds) do
            local events = autocmd.events
            autocmd.events = nil
            autocmd.group = augroup
            autocmd.buffer = bufnr
            vim.api.nvim_create_autocmd(events, autocmd)
        end
    end
end

local function has_capability(capability, filter)
    for _, client in ipairs(vim.lsp.get_active_clients(filter)) do
        if client.supports_method(capability) then return true end
    end
    return false
end

-- NOTE: LSP Keymaps here
-- TODO: Optimize these lookups with a fucking table jesus
-- https://stackoverflow.com/a/37493047
function M.lsp_on_attach(client, bufnr)
    local lsp_mappings = M.empty_map_table()

    lsp_mappings.n["<leader>ld"] = { function() vim.diagnostic.open_float() end, desc = "Hover diagnostics" }
    lsp_mappings.n["[d"] = { function() vim.diagnostic.goto_prev() end, desc = "Previous diagnostic" }
    lsp_mappings.n["]d"] = { function() vim.diagnostic.goto_next() end, desc = "Next diagnostic" }
    lsp_mappings.n["gl"] = { function() vim.diagnostic.open_float() end, desc = "Hover diagnostics" }

    if M.is_available("telescope.nvim") then
        lsp_mappings.n["<leader>lD"] = {
            function() require("telescope.builtin").diagnostics() end,
            desc = "Search diagnostics",
        }
    end

    if M.is_available("mason-lspconfig.nvim") then
        lsp_mappings.n["<leader>li"] = { "<cmd>LspInfo<cr>", desc = "LSP information" }
    end

    if M.is_available("null-ls.nvim") then
        lsp_mappings.n["<leader>lI"] = { "<cmd>NullLsInfo<cr>", desc = "Null-ls information" }
    end

    if client.supports_method("textDocument/codeAction") then
        lsp_mappings.n["<leader>la"] = {
            function() vim.lsp.buf.code_action() end,
            desc = "Code action",
        }
        lsp_mappings.v["<leader>la"] = lsp_mappings.n["<leader>la"]
    end

    if client.supports_method("textDocument/codeLens") then
        vim.lsp.codelens.refresh()
        lsp_mappings.n["<leader>ll"] = {
            function() vim.lsp.codelens.refresh() end,
            desc = "Refresh CodeLens",
        }
        lsp_mappings.n["<leader>lL"] = {
            function() vim.lsp.codelens.run() end,
            desc = "Run CodeLens",
        }
        lsp_mappings.n["<Leader>u"] = { desc = " " .. icons.ui.Gear .. " Utility" }
        lsp_mappings.n["<leader>uL"] = {
            function() vim.lsp.codelens.clear() end,
            desc = "Toggle CodeLens"
        }
    end

    if client.supports_method("textDocument/definition") then
        lsp_mappings.n["gd"] = {
            function() vim.lsp.buf.definition() end,
            desc = "Go to definition",
        }
    end

    if client.supports_method "textDocument/typeDefinition" then
        lsp_mappings.n["gy"] = {
            function() vim.lsp.buf.type_definition() end,
            desc = "Go to type definition",
        }
    end

    if client.supports_method("textDocument/declaration") then
        lsp_mappings.n["gD"] = {
            function() vim.lsp.buf.declaration() end,
            desc = "Go to declaration",
        }
    end

    if client.supports_method("textDocument/implementation") then
        lsp_mappings.n["gI"] = {
            function() vim.lsp.buf.implementation() end,
            desc = "List implementations",
        }
    end

    if client.supports_method("textDocument/references") then
        lsp_mappings.n["gr"] = {
            function() vim.lsp.buf.references() end,
            desc = "List references",
        }
    end

    if client.supports_method "workspace/symbol" then
        lsp_mappings.n["<leader>lG"] = {
            function() vim.lsp.buf.workspace_symbol() end,
            desc = "List symbols"
        }
    end

    if client.supports_method "textDocument/rename" then
        lsp_mappings.n["<leader>lr"] = {
            function() vim.lsp.buf.rename() end,
            desc = "Rename symbol",
        }
    end

    -- TODO: Check this on 0.10.0 release
    if client.supports_method "textDocument/semanticTokens/full" and vim.lsp.semantic_tokens then
        vim.b[bufnr].semantic_tokens_enabled = true
        lsp_mappings.n["<Leader>u"] = { desc = " " .. icons.ui.Gear .. " Utility" }
        lsp_mappings.n["<leader>uY"] = {
            function()
                vim.b[bufnr].semantic_tokens_enabled = not vim.b[bufnr].semantic_tokens_enabled
                for _, active_client in ipairs(vim.lsp.get_active_clients { bufnr = bufnr }) do
                    if active_client.server_capabilities.semanticTokensProvider then
                        vim.lsp.semantic_tokens[vim.b[bufnr].semantic_tokens_enabled and "start" or "stop"](bufnr,
                            active_client.id)
                    end
                end
            end,
            desc = "Toggle LSP semantic highlight (buffer)",
        }
    end

    if client.supports_method("textDocument/formatting") then
        lsp_mappings.n["<leader>lf"] = {
            function()
                vim.lsp.buf.format(require("config.lsp.format"))
            end,
            desc = "Format buffer",
        }
        lsp_mappings.v["<leader>lf"] = lsp_mappings.n["<leader>lf"]
    end

    if client.supports_method("textDocument/documentHighlight") then
        add_buffer_autocmd("lsp_document_highlight", bufnr, {
            {
                events = { "CursorHold", "CursorHoldI" },
                desc = "highlight references when cursor holds",
                callback = function()
                    if not has_capability("textDocument/documentHighlight", { bufnr = bufnr }) then
                        del_buffer_autocmd("lsp_document_highlight", bufnr)
                        return
                    end
                    vim.lsp.buf.document_highlight()
                end,
            },
            {
                events = { "CursorMoved", "CursorMovedI", "BufLeave" },
                desc = "clear references when cursor moves",
                callback = function() vim.lsp.buf.clear_references() end,
            },
        })
    end

    if client.supports_method("textDocument/hover") then
        -- TODO: Check this on 0.10.0 release
        if vim.fn.has "nvim-0.10" == 0 then
            lsp_mappings.n["K"] = {
                function() vim.lsp.buf.hover() end,
                desc = "Hover symbol",
            }
        end
    end

    if client.supports_method("textDocument/inlayHint") then
        if vim.b.inlay_hints_enabled == nil then vim.b.inlay_hints_enabled = true end
        -- TODO: Check this on 0.10.0 release
        if vim.lsp.inlay_hint then
            if vim.b.inlay_hints_enabled then vim.lsp.inlay_hint.enable(bufnr, true) end
            lsp_mappings.n["<Leader>u"] = { desc = " " .. icons.ui.Gear .. " Utility" }
            lsp_mappings.n["<leader>uH"] = {
                function()
                    vim.b[bufnr].inlay_hints_enabled = not vim.b[bufnr].inlay_hints_enabled
                    if vim.lsp.inlay_hint then
                        vim.lsp.inlay_hint.enable(bufnr, vim.b[bufnr].inlay_hints_enabled)
                    end
                end,
                desc = "Toggle inlay hints",
            }
        end
    end

    if client.supports_method "textDocument/signatureHelp" then
        lsp_mappings.n["<leader>lh"] = {
            function() vim.lsp.buf.signature_help() end,
            desc = "Signature help",
        }
    end

    if M.is_available "telescope.nvim" then
        if lsp_mappings.n.gd then
            lsp_mappings.n.gd[1] = function() require("telescope.builtin").lsp_definitions() end
        end
        if lsp_mappings.n.gI then
            lsp_mappings.n.gI[1] = function() require("telescope.builtin").lsp_implementations() end
        end
        if lsp_mappings.n.gr then
            lsp_mappings.n.gr[1] = function() require("telescope.builtin").lsp_references() end
        end
        if lsp_mappings.n["<leader>lR"] then
            lsp_mappings.n["<leader>lR"][1] = function() require("telescope.builtin").lsp_references() end
        end
        if lsp_mappings.n.gy then
            lsp_mappings.n.gy[1] = function() require("telescope.builtin").lsp_type_definitions() end
        end
        if lsp_mappings.n["<leader>lG"] then
            lsp_mappings.n["<leader>lG"][1] = function()
                vim.ui.input({ prompt = "Symbol Query: (leave empty for word under cursor)" }, function(query)
                    if query then
                        -- word under cursor if given query is empty
                        if query == "" then query = vim.fn.expand "<cword>" end
                        require("telescope.builtin").lsp_workspace_symbols {
                            query = query,
                            prompt_title = ("Find word (%s)"):format(query),
                        }
                    end
                end)
            end
        end
    end

    if not vim.tbl_isempty(lsp_mappings.v) then
        if lsp_mappings.v["<leader>l"] then lsp_mappings.v["<leader>l"] = { desc = " " .. icons.ui.Note .. " LSP" } end
        if lsp_mappings.v["<leader>u"] then lsp_mappings.v["<leader>u"] = { desc = " " .. icons.ui.Note .. " LSP" } end
    end

    M.set_mappings(lsp_mappings, { buffer = bufnr })
end

return M