local M = {}

function M.lsp_on_attach(client, bufnr)
    -- 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
    end

    if client.supports_method("textDocument/documentHighlight") then
        M.add_buffer_autocmd("lsp_document_highlight", bufnr, {
            {
                events = { "CursorHold", "CursorHoldI" },
                desc = "highlight references when cursor holds",
                callback = function()
                    if not M.has_capability("textDocument/documentHighlight", { bufnr = bufnr }) then
                        M.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/inlayHint") then
        if vim.b[bufnr].inlay_hints_enabled == nil then
            vim.b[bufnr].inlay_hints_enabled = true
        end
        -- TODO: Check this on 0.10.0 release
        if vim.lsp.inlay_hint and vim.b[bufnr].inlay_hints_enabled then
            vim.lsp.inlay_hint.enable(bufnr, true)
        end
    end

    -- TODO: Check this on 0.10.0 release
    if vim.lsp.inlay_hint and vim.b[bufnr].inlay_hints_enabled then
        vim.lsp.inlay_hint.enable(bufnr, true)
    end

    local maps = require("config.keymaps").lsp_maps(client, bufnr)
    M.set_maps(maps, { buffer = bufnr })
end

function M.set_title()
    local title = " %t"
    local f = io.popen([[ zsh -lc 'print -P $PS1' | sed 's/\[[0-9;]*m//g;s/» / %t/g' ]])
    if f ~= nil then
        title = f:read("*a") or ""
        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 type(opts) == "string" then
        opts = { cmd = opts, hidden = true }
    end
    local num = vim.v.count > 0 and vim.v.count or 1
    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
    terms[opts.cmd][num]:toggle()
end

function M.cmd(cmd, show_error)
    if type(cmd) == "string" then
        cmd = { cmd }
    end
    local result = vim.fn.system(cmd)
    local success = vim.api.nvim_get_vvar("shell_error") == 0
    if not success and (show_error == nil or show_error) then
        vim.api.nvim_err_writeln(
            ("Error running command %s\nError message:\n%s"):format(table.concat(cmd, " "), result)
        )
    end
    return success and result:gsub("[\27\155][][()#;?%d]*[A-PRZcf-ntqry=><~]", "") or nil
end

function M.file_worktree(file, worktrees)
    worktrees = worktrees or vim.g.git_worktrees
    file = file or vim.fn.expand("%")
    for _, worktree in ipairs(worktrees) do
        if
            M.cmd({
                "git",
                "--work-tree",
                worktree.toplevel,
                "--git-dir",
                worktree.gitdir,
                "ls-files",
                "--error-unmatch",
                file,
            }, false)
        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_maps(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

function M.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

function M.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

function M.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

function M.diagnostic_setup(conf)
    for _, sign in ipairs(conf.signs) do
        vim.fn.sign_define(sign.name, sign)
    end
    vim.diagnostic.config(conf.diagnostic)
end

function M.has_value(table, value)
    for _, v in ipairs(table) do
        if v == value then
            return true
        end
    end
    return false
end

function M.get_gwd(path)
    return require("lspconfig.util").find_git_ancestor(path or vim.fn.getcwd())
end

function M.set_cwd()
    local dir = M.get_gwd()
    if dir ~= nil then
        vim.fn.chdir(dir)
    end
end

function M.get_lsp_key(key)
    local ok, keys = pcall(require, "config.keys")
    if ok then
        return keys[key]
    end
end

function M.lazy_file()
end

return M