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

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

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
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("none-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.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.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({
					filter = function(c)
						local filetype = vim.bo.filetype
						local n = require("null-ls")
						local s = require("null-ls.sources")
						local method = n.methods.FORMATTING
						local available_formatters = s.get_available(filetype, method)
						if #available_formatters > 0 then
							return c.name == "null-ls"
						end
						return true
					end,
				})
			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.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.Code .. " LSP" }
		end
		if lsp_mappings.v["<Leader>u"] then
			lsp_mappings.v["<Leader>u"] = { desc = icons.Code .. " LSP" }
		end
	end

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

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

return M