--
-- CONFIGS
--
vim.opt.termguicolors = true -- enable 24-bit RGB colors in the terminal

-- Line numbers
vim.opt.number = true -- show absolute line number on current line
vim.opt.relativenumber = true -- show relative line numbers for all other lines
vim.opt.cursorline = true -- highlight the row the cursor is on
vim.opt.wrap = false -- do not wrap long lines
vim.opt.scrolloff = 10 -- keep at least 10 lines above/below the cursor
vim.opt.sidescrolloff = 10 -- keep at least 10 columns left/right of the cursor

-- Indentation: 2-space soft tabs
vim.opt.tabstop = 2 -- a <Tab> counts as 2 columns
vim.opt.shiftwidth = 2 -- >> / << shift by 2 columns
vim.opt.softtabstop = 2 -- <Tab> in insert mode inserts 2 spaces
vim.opt.expandtab = true -- insert spaces instead of tab characters
vim.opt.smartindent = true
vim.opt.autoindent = true

-- Search
vim.opt.ignorecase = true -- case-insensitive search by default
vim.opt.smartcase = true -- switch to case-sensitive when query contains an uppercase letter
vim.opt.hlsearch = true -- highlight all matches
vim.opt.incsearch = true -- show matches while typing the query

-- UI
vim.opt.showmatch = true -- briefly jump to matching bracket when one is inserted
vim.opt.cmdheight = 1 -- always show the command line (1 row)
vim.opt.showmode = false -- hide "-- INSERT --" etc.; the statusline shows the mode instead
vim.opt.pumheight = 10 -- limit the completion popup to 10 items
vim.opt.pumblend = 10 -- make the completion popup slightly transparent
vim.opt.winblend = 0 -- floating windows are fully opaque
vim.opt.conceallevel = 0 -- never hide markup characters (e.g. Markdown asterisks)
vim.opt.concealcursor = "" -- never hide markup on the cursor line either
vim.opt.synmaxcol = 300 -- stop syntax highlighting for columns beyond 300 (perf guard)
vim.opt.fillchars = { eob = " " } -- replace the "~" placeholder on empty lines with a space

-- Persistent undo: survive across sessions even after the buffer is closed
local undodir = vim.fn.expand("~/.vim/undodir")
if
	vim.fn.isdirectory(undodir) == 0 -- create undodir if nonexistent
then
	vim.fn.mkdir(undodir, "p")
end

vim.opt.backup = false -- do not create a backup file
vim.opt.writebackup = false -- do not keep a backup while overwriting a file
vim.opt.swapfile = false -- do not create a swapfile
vim.opt.undofile = true -- persist undo history to disk
vim.opt.undodir = undodir -- where to store undo files
vim.opt.updatetime = 300 -- write swap file (and trigger CursorHold) after 300 ms of inactivity
vim.opt.timeoutlen = 500 -- wait 500 ms for a mapped key sequence to complete
vim.opt.ttimeoutlen = 50 -- wait 50 ms for a terminal key code to complete
vim.opt.autoread = true -- reload a file changed outside Neovim without prompting
vim.opt.autowrite = false -- do not silently save on :make / buffer switch

-- Buffer and editing behaviour
vim.opt.hidden = true -- allow switching away from an unsaved buffer without closing it
vim.opt.errorbells = false -- disable the error bell
vim.opt.backspace = "indent,eol,start" -- allow backspace over auto-indent, line breaks, and insert start
vim.opt.autochdir = false -- keep the working directory fixed (don't follow the open file)
vim.opt.iskeyword:append("-") -- treat "foo-bar" as a single word for motions like w/b
vim.opt.path:append("**") -- make :find search recursively into subdirectories
vim.opt.selection = "inclusive" -- visual selection includes the character under the cursor
vim.opt.mouse = "a" -- enable mouse support in all modes
vim.opt.clipboard:append("unnamedplus") -- share the system clipboard (+ register)

-- Over SSH, wl-clipboard is unavailable; use OSC 52 to write yanks to the local terminal clipboard.
if os.getenv("SSH_TTY") then
	vim.g.clipboard = {
		name = "OSC 52",
		copy = {
			["+"] = require("vim.ui.clipboard.osc52").copy("+"),
			["*"] = require("vim.ui.clipboard.osc52").copy("*"),
		},
		paste = {
			["+"] = require("vim.ui.clipboard.osc52").paste("+"),
			["*"] = require("vim.ui.clipboard.osc52").paste("*"),
		},
	}
end
vim.opt.modifiable = true -- allow buffer modifications (safety default)
vim.opt.encoding = "utf-8" -- internal character encoding

-- Folding: driven by Tree-sitter when available; foldlevel = 99 means all folds start open
vim.opt.foldmethod = "expr"
vim.opt.foldexpr = "v:lua.vim.treesitter.foldexpr()"
vim.opt.foldlevel = 99 -- a high value ensures every fold is open on buffer load

-- Window splits
vim.opt.splitbelow = true -- :split opens the new window below
vim.opt.splitright = true -- :vsplit opens the new window to the right

-- Miscellaneous
vim.opt.wildmenu = true -- enable enhanced command-line completion
vim.opt.wildmode = "longest:full,full" -- first Tab completes the longest common prefix and lists all; subsequent Tabs cycle
vim.opt.diffopt:append("linematch:60") -- use the improved diff algorithm for blocks up to 60 lines
vim.opt.redrawtime = 10000 -- allow up to 10 s for a full screen redraw (useful for large files)
vim.opt.maxmempattern = 20000 -- raise the memory cap for pattern matching (avoids "pattern too complex" errors)

--
-- KEYMAPS
--
vim.g.mapleader = " " -- <Space> as the leader key
vim.g.maplocalleader = " " -- <Space> as the local leader key

-- j/k navigate display lines when there is no count prefix, so wrapped lines
-- feel natural. With a count (e.g. 5j) they use real lines so relative jumps work.
vim.keymap.set("n", "j", function()
	return vim.v.count == 0 and "gj" or "j"
end, { expr = true, silent = true, desc = "Down (wrap-aware)" })
vim.keymap.set("n", "k", function()
	return vim.v.count == 0 and "gk" or "k"
end, { expr = true, silent = true, desc = "Up (wrap-aware)" })

-- Keep search results and half-page jumps vertically centred
vim.keymap.set("n", "n", "nzzzv", { desc = "Next search result (centered)" })
vim.keymap.set("n", "N", "Nzzzv", { desc = "Previous search result (centered)" })
vim.keymap.set("n", "<C-d>", "<C-d>zz", { desc = "Half page down (centered)" })
vim.keymap.set("n", "<C-u>", "<C-u>zz", { desc = "Half page up (centered)" })

-- Window navigation without the <C-w> prefix
vim.keymap.set("n", "<C-h>", "<C-w>h", { desc = "Move to left window" })
vim.keymap.set("n", "<C-j>", "<C-w>j", { desc = "Move to bottom window" })
vim.keymap.set("n", "<C-k>", "<C-w>k", { desc = "Move to top window" })
vim.keymap.set("n", "<C-l>", "<C-w>l", { desc = "Move to right window" })

-- Re-select visual block after indent so you can keep indenting with < or >
vim.keymap.set("v", "<", "<gv", { desc = "Indent left and reselect" })
vim.keymap.set("v", ">", ">gv", { desc = "Indent right and reselect" })

-- J joins lines but restores the cursor position (mz saves, `z restores)
vim.keymap.set("n", "J", "mzJ`z", { desc = "Join lines and keep cursor position" })

vim.keymap.set("n", "H", "<cmd>bprev<cr>", { desc = "Previous buffer" })
vim.keymap.set("n", "L", "<cmd>bnext<cr>", { desc = "Next buffer" })

-- Toggle LSP diagnostics on/off for the current session
vim.keymap.set("n", "<leader>td", function()
	vim.diagnostic.enable(not vim.diagnostic.is_enabled())
end, { desc = "Toggle diagnostics" })

-- Toggle inline virtual text only (keeps underlines and signs)
vim.keymap.set("n", "<leader>tv", function()
	local current = vim.diagnostic.config().virtual_text
	vim.diagnostic.config({ virtual_text = not current })
end, { desc = "Toggle virtual text" })

--
-- AUTOCOMMANDS
--
local augroup = vim.api.nvim_create_augroup("UserConfig", { clear = true })

-- Format on save via the EFM language server.
-- Only runs when: the buffer is a real file, it is modifiable, it has a name,
-- and efm is attached. This avoids spurious save prompts on scratch buffers.
vim.api.nvim_create_autocmd("BufWritePre", {
	group = augroup,
	pattern = {
		"*.cs",
		"*.lua",
		"*.py",
		"*.go",
		"*.js",
		"*.jsx",
		"*.ts",
		"*.tsx",
		"*.json",
		"*.css",
		"*.scss",
		"*.html",
		"*.sh",
		"*.bash",
	},
	callback = function(args)
		-- avoid formatting non-file buffers (helps prevent weird write prompts)
		if vim.bo[args.buf].buftype ~= "" then
			return
		end
		if not vim.bo[args.buf].modifiable then
			return
		end
		if vim.api.nvim_buf_get_name(args.buf) == "" then
			return
		end

		local has_efm = false
		for _, c in ipairs(vim.lsp.get_clients({ bufnr = args.buf })) do
			if c.name == "efm" then
				has_efm = true
				break
			end
		end
		if not has_efm then
			return
		end

		pcall(vim.lsp.buf.format, {
			bufnr = args.buf,
			timeout_ms = 2000,
			filter = function(c)
				return c.name == "efm"
			end,
		})
	end,
})

-- Flash the yanked region briefly so it is clear what was copied
vim.api.nvim_create_autocmd("TextYankPost", {
	group = augroup,
	callback = function()
		vim.hl.on_yank()
	end,
})

-- Restore the cursor to where it was when the file was last closed
vim.api.nvim_create_autocmd("BufReadPost", {
	group = augroup,
	desc = "Restore last cursor position",
	callback = function()
		if vim.o.diff then -- except in diff mode
			return
		end

		local last_pos = vim.api.nvim_buf_get_mark(0, '"') -- {line, col}
		local last_line = vim.api.nvim_buf_line_count(0)

		local row = last_pos[1]
		if row < 1 or row > last_line then
			return
		end

		pcall(vim.api.nvim_win_set_cursor, 0, last_pos)
	end,
})

-- Prose-friendly settings for Markdown, plain text, and commit messages
vim.api.nvim_create_autocmd("FileType", {
	group = augroup,
	pattern = { "markdown", "text", "gitcommit" },
	callback = function()
		vim.opt_local.wrap = true -- soft-wrap at window edge
		vim.opt_local.linebreak = true -- break at word boundaries, not mid-word
		vim.opt_local.spell = true -- enable spell-checking
	end,
})

--
-- PLUGINS
--
vim.pack.add({
	"https://github.com/folke/which-key.nvim", -- shows pending keymap hints
	"https://github.com/folke/tokyonight.nvim", -- colorscheme
	"https://github.com/nvim-lualine/lualine.nvim", -- statusline
	"https://www.github.com/lewis6991/gitsigns.nvim", -- git hunk signs in the gutter
	"https://www.github.com/echasnovski/mini.nvim", -- collection of small plugins
	"https://www.github.com/ibhagwan/fzf-lua", -- fuzzy finder (files, grep, LSP, …)
	"https://www.github.com/neovim/nvim-lspconfig", -- provides cmd/root_dir/filetypes for each LSP server
	"https://github.com/mason-org/mason.nvim", -- LSP / tool installer UI
	"https://github.com/creativenull/efmls-configs-nvim", -- pre-built efm-langserver tool configs
	"https://github.com/WhoIsSethDaniel/mason-tool-installer.nvim", -- auto-install Mason tools on startup
	"https://github.com/folke/flash.nvim", -- enhanced f/t/s jumps with labels
	"https://github.com/L3MON4D3/LuaSnip", -- snippet engine (used by blink.cmp)
	{
		src = "https://github.com/saghen/blink.cmp", -- completion engine
		version = vim.version.range("1.*"), -- pin to any 1.x release
	},
	{
		src = "https://github.com/nvim-treesitter/nvim-treesitter",
		branch = "main",
		build = ":TSUpdate", -- keep parser binaries up to date after install/update
	},
	"https://github.com/nvim-treesitter/nvim-treesitter-textobjects",
	"https://github.com/HakonHarnes/img-clip.nvim", -- paste images from clipboard (no Python required)
})

--
-- PLUGIN CONFIGS
--
require("lualine").setup()

vim.cmd.colorscheme("tokyonight")

-- which-key: shows a popup of available keymaps after a delay.
-- Groups give the prefixes human-readable titles in the popup.
require("which-key").setup({})
require("which-key").add({
	{ "<leader>f", group = "Find" }, -- fzf-lua pickers (files, grep, LSP symbols, …)
	{ "<leader>g", group = "Go to" }, -- LSP navigation (definition, split, …)
	{ "<leader>c", group = "Code" }, -- code actions
	{ "<leader>r", group = "Refactor" }, -- rename, extract, …
	{ "<leader>t", group = "Toggle" }, -- toggle settings (diagnostics, …)
	{ "<leader>o", group = "Organize" }, -- organize imports, sort, …
	{ "<leader>d", group = "Diagnostics" }, -- diagnostic float / list
	{ "<leader>n", group = "Next" }, -- jump to next item
	{ "<leader>p", group = "Prev" }, -- jump to previous item
	{ "]m", hidden = true },
	{ "[m", hidden = true },
	{ "]M", hidden = true },
	{ "[M", hidden = true },
})

-- Tree-sitter: start the parser for a filetype only when its grammar is
-- already installed. Missing grammars are installed asynchronously on first
-- launch so the editor never blocks at startup.
local setup_treesitter = function()
	local treesitter = require("nvim-treesitter")
	treesitter.setup({})
	local ensure_installed = {
		"vim",
		"vimdoc",
		"go",
		"html",
		"css",
		"javascript",
		"json",
		"lua",
		"markdown",
		"python",
		"typescript",
		"bash",
		"c_sharp",
	}

	local config = require("nvim-treesitter.config")

	-- Only install parsers that are not already present
	local already_installed = config.get_installed()
	local parsers_to_install = {}

	for _, parser in ipairs(ensure_installed) do
		if not vim.tbl_contains(already_installed, parser) then
			table.insert(parsers_to_install, parser)
		end
	end

	if #parsers_to_install > 0 then
		treesitter.install(parsers_to_install)
	end

	-- Start the Tree-sitter parser whenever a supported filetype is opened.
	-- Using an autocmd instead of the built-in auto_install avoids the
	-- blocking install prompt on first open.
	local group = vim.api.nvim_create_augroup("TreeSitterConfig", { clear = true })
	vim.api.nvim_create_autocmd("FileType", {
		group = group,
		callback = function(args)
			if vim.list_contains(treesitter.get_installed(), vim.treesitter.language.get_lang(args.match)) then
				vim.treesitter.start(args.buf)
			end
		end,
	})
end

setup_treesitter()

require("nvim-treesitter-textobjects").setup({ move = { set_jumps = true } })

local move = require("nvim-treesitter-textobjects.move")
vim.keymap.set({ "n", "x", "o" }, "]f", function()
	move.goto_next_start("@function.outer", "textobjects")
end, { desc = "Next function" })
vim.keymap.set({ "n", "x", "o" }, "[f", function()
	move.goto_previous_start("@function.outer", "textobjects")
end, { desc = "Prev function" })
vim.keymap.set({ "n", "x", "o" }, "]c", function()
	move.goto_next_start("@class.outer", "textobjects")
end, { desc = "Next class" })
vim.keymap.set({ "n", "x", "o" }, "[c", function()
	move.goto_previous_start("@class.outer", "textobjects")
end, { desc = "Prev class" })

-- fzf-lua: fuzzy finder keymaps
require("fzf-lua").setup({
	files = {
		actions = {
			["ctrl-h"] = { require("fzf-lua").actions.toggle_hidden },
		},
	},
})
vim.keymap.set("n", "<leader><leader>", function()
	require("fzf-lua").files()
end, { desc = "Find files" })
vim.keymap.set("n", "<leader>fg", function()
	require("fzf-lua").live_grep()
end, { desc = "FZF Live Grep" })
vim.keymap.set("n", "<leader>fb", function()
	require("fzf-lua").buffers()
end, { desc = "FZF Buffers" })
vim.keymap.set("n", "<leader>fh", function()
	require("fzf-lua").help_tags()
end, { desc = "FZF Help Tags" })
vim.keymap.set("n", "<leader>fx", function()
	require("fzf-lua").diagnostics_document()
end, { desc = "FZF Diagnostics Document" })
vim.keymap.set("n", "<leader>fX", function()
	require("fzf-lua").diagnostics_workspace()
end, { desc = "FZF Diagnostics Workspace" })

-- mini.nvim: a suite of small, focused plugins. Each module is opted in explicitly.
require("mini.ai").setup({}) -- extended text objects (e.g. cia = inside any argument)
require("mini.comment").setup({}) -- gcc / gc<motion> to toggle comments
require("mini.move").setup({}) -- move lines/selections with Option+hjkl (macOS Option = Alt)
require("mini.surround").setup({}) -- sa/sd/sr to add/delete/replace surrounding chars
require("mini.cursorword").setup({}) -- highlight all occurrences of the word under the cursor
require("mini.indentscope").setup({}) -- animated indent-scope indicator line
require("mini.pairs").setup({}) -- auto-close brackets, quotes, etc.
require("mini.trailspace").setup({}) -- highlight and trim trailing whitespace
require("mini.notify").setup({}) -- non-blocking notification popups
require("mini.icons").setup({}) -- icon provider (replaces nvim-web-devicons calls)

-- flash.nvim: label-based jumps for f/t and anywhere on screen
require("flash").setup({})
vim.keymap.set({ "n", "x", "o" }, "s", function()
	require("flash").jump()
end, { desc = "Flash jump" })
vim.keymap.set({ "n", "x", "o" }, "S", function()
	require("flash").treesitter()
end, { desc = "Flash treesitter" })
vim.keymap.set("o", "r", function()
	require("flash").remote()
end, { desc = "Flash remote" })
vim.keymap.set({ "o", "x" }, "R", function()
	require("flash").treesitter_search()
end, { desc = "Flash treesitter search" })

-- img-clip: paste images from clipboard into markdown files (no Python required)
require("img-clip").setup({
	default = {
		use_absolute_path = false,
		relative_to_current_file = true,
		file_name = function()
			return os.date("%Y-%m-%d_%H-%M-%S")
		end,
	},
	filetypes = {
		markdown = { template = "![]($FILE_PATH)" },
	},
})
vim.keymap.set({ "n", "x" }, "<leader>pi", "<cmd>PasteImage<CR>", { desc = "Paste image" })

-- gitsigns: decorates the sign column with added/changed/removed hunk markers
require("gitsigns").setup({
	signs = {
		add = { text = "\u{2590}" }, -- ▏
		change = { text = "\u{2590}" }, -- ▐
		delete = { text = "\u{2590}" }, -- ◦
		topdelete = { text = "\u{25e6}" }, -- ◦
		changedelete = { text = "\u{25cf}" }, -- ●
		untracked = { text = "\u{25cb}" }, -- ○
	},
	signcolumn = true,
	current_line_blame = false, -- set to true to show inline git blame on the cursor line
})

--
-- LSP - Too anoying to have in this file
--
require("lsp-nix")
