-- Bring vim into local scope.
local vim = vim
local api = vim.api
local inspect = vim.inspect

local function extend(t, o)
	local mt = getmetatable(t)
	for k, v in pairs(o) do
		rawset(mt, k, v)
	end
	return t
end

-- Equivalent to `echo vim.inspect(...)`
local function nvim_print(...)
	if select("#", ...) == 1 then
		api.nvim_out_write(inspect((...)))
	else
		api.nvim_out_write(inspect({ ... }))
	end
	api.nvim_out_write("\n")
end

--- Equivalent to `echo` EX command
local function nvim_echo(...)
	for i = 1, select("#", ...) do
		local part = select(i, ...)
		api.nvim_out_write(tostring(part))
		-- vim.api.nvim_out_write("\n")
		api.nvim_out_write(" ")
	end
	api.nvim_out_write("\n")
end

local window_options = {
	arab = true,
	arabic = true,
	breakindent = true,
	breakindentopt = true,
	bri = true,
	briopt = true,
	cc = true,
	cocu = true,
	cole = true,
	colorcolumn = true,
	concealcursor = true,
	conceallevel = true,
	crb = true,
	cuc = true,
	cul = true,
	cursorbind = true,
	cursorcolumn = true,
	cursorline = true,
	diff = true,
	fcs = true,
	fdc = true,
	fde = true,
	fdi = true,
	fdl = true,
	fdm = true,
	fdn = true,
	fdt = true,
	fen = true,
	fillchars = true,
	fml = true,
	fmr = true,
	foldcolumn = true,
	foldenable = true,
	foldexpr = true,
	foldignore = true,
	foldlevel = true,
	foldmarker = true,
	foldmethod = true,
	foldminlines = true,
	foldnestmax = true,
	foldtext = true,
	lbr = true,
	lcs = true,
	linebreak = true,
	list = true,
	listchars = true,
	nu = true,
	number = true,
	numberwidth = true,
	nuw = true,
	previewwindow = true,
	pvw = true,
	relativenumber = true,
	rightleft = true,
	rightleftcmd = true,
	rl = true,
	rlc = true,
	rnu = true,
	scb = true,
	scl = true,
	scr = true,
	scroll = true,
	scrollbind = true,
	signcolumn = true,
	spell = true,
	statusline = true,
	stl = true,
	wfh = true,
	wfw = true,
	winbl = true,
	winblend = true,
	winfixheight = true,
	winfixwidth = true,
	winhighlight = true,
	winhl = true,
	wrap = true,
}

local function validate(conf)
	assert(type(conf) == "table")
	local type_names = {
		t = "table",
		s = "string",
		n = "number",
		b = "boolean",
		f = "function",
		c = "callable",
		["table"] = "table",
		["string"] = "string",
		["number"] = "number",
		["boolean"] = "boolean",
		["function"] = "function",
		["callable"] = "callable",
		["nil"] = "nil",
		["thread"] = "thread",
		["userdata"] = "userdata",
	}
	for k, v in pairs(conf) do
		if not (v[3] and v[1] == nil) and type(v[1]) ~= type_names[v[2]] then
			error(string.format("validation_failed: %q: expected %s, received %s", k, type_names[v[2]], type(v[1])))
		end
	end
	return true
end

local function make_meta_accessor(get, set, del)
	validate({
		get = { get, "f" },
		set = { set, "f" },
		del = { del, "f", true },
	})
	local mt = {}
	if del then
		function mt:__newindex(k, v)
			if v == nil then
				return del(k)
			end
			return set(k, v)
		end
	else
		function mt:__newindex(k, v)
			return set(k, v)
		end
	end
	function mt:__index(k)
		return get(k)
	end
	return setmetatable({}, mt)
end

local function pcall_ret(status, ...)
	if status then
		return ...
	end
end

local function nil_wrap(fn)
	return function(...)
		return pcall_ret(pcall(fn, ...))
	end
end

local fn = setmetatable({}, {
	__index = function(t, k)
		local f = function(...)
			return api.nvim_call_function(k, { ... })
		end
		rawset(t, k, f)
		return f
	end,
})

local function getenv(k)
	local v = fn.getenv(k)
	if v == vim.NIL then
		return nil
	end
	return v
end

local function new_win_accessor(winnr)
	local function get(k)
		if winnr == nil and type(k) == "number" then
			return new_win_accessor(k)
		end
		return api.nvim_win_get_var(winnr or 0, k)
	end
	local function set(k, v)
		return api.nvim_win_set_var(winnr or 0, k, v)
	end
	local function del(k)
		return api.nvim_win_del_var(winnr or 0, k)
	end
	return make_meta_accessor(nil_wrap(get), set, del)
end

local function new_win_opt_accessor(winnr)
	local function get(k)
		if winnr == nil and type(k) == "number" then
			return new_win_opt_accessor(k)
		end
		return api.nvim_win_get_option(winnr or 0, k)
	end
	local function set(k, v)
		return api.nvim_win_set_option(winnr or 0, k, v)
	end
	return make_meta_accessor(nil_wrap(get), set)
end

local function new_buf_accessor(bufnr)
	local function get(k)
		if bufnr == nil and type(k) == "number" then
			return new_buf_accessor(k)
		end
		return api.nvim_buf_get_var(bufnr or 0, k)
	end
	local function set(k, v)
		return api.nvim_buf_set_var(bufnr or 0, k, v)
	end
	local function del(k)
		return api.nvim_buf_del_var(bufnr or 0, k)
	end
	return make_meta_accessor(nil_wrap(get), set, del)
end

local function new_buf_opt_accessor(bufnr)
	local function get(k)
		if window_options[k] then
			return api.nvim_err_writeln(k .. " is a window option, not a buffer option")
		end
		if bufnr == nil and type(k) == "number" then
			return new_buf_opt_accessor(k)
		end
		return api.nvim_buf_get_option(bufnr or 0, k)
	end
	local function set(k, v)
		if window_options[k] then
			return api.nvim_err_writeln(k .. " is a window option, not a buffer option")
		end
		return api.nvim_buf_set_option(bufnr or 0, k, v)
	end
	return make_meta_accessor(nil_wrap(get), set)
end

-- `nvim.$method(...)` redirects to `nvim.api.nvim_$method(...)`
-- `nvim.fn.$method(...)` redirects to `vim.api.nvim_call_function($method, {...})`
-- TODO `nvim.ex.$command(...)` is approximately `:$command {...}.join(" ")`
-- `nvim.print(...)` is approximately `echo vim.inspect(...)`
-- `nvim.echo(...)` is approximately `echo table.concat({...}, '\n')`
-- Both methods cache the inital lookup in the metatable, but there is api small overhead regardless.
return setmetatable({
	print = nvim_print,
	echo = nvim_echo,
	fn = rawget(vim, "fn") or fn,
	validate = validate,
	g = rawget(vim, "g") or make_meta_accessor(nil_wrap(api.nvim_get_var), api.nvim_set_var, api.nvim_del_var),
	v = rawget(vim, "v") or make_meta_accessor(nil_wrap(api.nvim_get_vvar), api.nvim_set_vvar),
	o = rawget(vim, "o") or make_meta_accessor(api.nvim_get_option, api.nvim_set_option),
	w = new_win_accessor(nil),
	b = new_buf_accessor(nil),
	env = rawget(vim, "env") or make_meta_accessor(getenv, fn.setenv),
	wo = rawget(vim, "wo") or new_win_opt_accessor(nil),
	bo = rawget(vim, "bo") or new_buf_opt_accessor(nil),
	buf = {
		line = api.nvim_get_current_line,
		nr = api.nvim_get_current_buf,
	},
	ex = setmetatable({}, {
		__index = function(t, k)
			local command = k:gsub("_$", "!")
			local f = function(...)
				local flattened = nil
				if vim.iter then
					flattened = vim.iter({ command, ... }):flatten():totable()
				else
					flattened = vim.tbl_flatten({ command, ... })
				end
				return api.nvim_command(table.concat(flattened, " "))
			end
			rawset(t, k, f)
			return f
		end,
	}),
}, {
	__index = function(t, k)
		local f = api["nvim_" .. k]
		if f then
			rawset(t, k, f)
		end
		return f
	end,
})
-- vim:et ts=2 sw=2
