--[[
	various intuitive assertions that
		- avoid garbage generation upon success,
		- build nice formatted error messages
		- post one level above the call site by default
		- return their first argument so they can be used inline

	default call is builtin global assert

	can call nop() to dummy out everything for "release mode"
	(if you're worried about that sort of thing)
]]

local _assert = assert

--proxy calls to global assert
local assert = setmetatable({}, {
	__call = function(self, ...)
		return _assert(...)
	end,
})

local function _extra(msg)
	if not msg then
		return ""
	end
	return "\n\n\t(note: " .. msg .. ")"
end

--assert a value is not nil
--return the value, so this can be chained
function assert:some(v, msg, stack_level)
	if v == nil then
		error(("assertion failed: value is nil %s"):format(
			_extra(msg)
		), 2 + (stack_level or 0))
	end
	return v
end

--assert two values are equal
function assert:equal(a, b, msg, stack_level)
	if a ~= b then
		error(("assertion failed: %s is not equal to %s %s"):format(
			tostring(a),
			tostring(b),
			_extra(msg)
		), 2 + (stack_level or 0))
	end
	return a
end

--assert two values are not equal
function assert:not_equal(a, b, msg, stack_level)
	if a == b then
		error(("assertion failed: values are equal %s"):format(
			_extra(msg)
		), 2 + (stack_level or 0))
	end
	return a
end

--assert a value is of a certain type
function assert:type(a, t, msg, stack_level)
	if type(a) ~= t then
		error(("assertion failed: %s (%s) not of type %s %s"):format(
			tostring(a),
			type(a),
			tostring(t),
			_extra(msg)
		), 2 + (stack_level or 0))
	end
	return a
end

--assert a value is nil or a certain type.
-- useful for optional parameters.
function assert:type_or_nil(a, t, msg, stack_level)
	if a ~= nil then
		assert:type(a, t, msg, stack_level + 1)
	end
	return a
end

--assert a value is one of those in a table of options
function assert:one_of(a, t, msg, stack_level)
	for _, value in ipairs(t) do
		if value == a then
			return a
		end
	end

	local values = {}
	for index = 1, #t do
		values[index] = tostring(t[index])
	end

	error(("assertion failed: %s not one of %s %s"):format(
		tostring(a),
		table.concat(values, ", "),
		_extra(msg)
	), 2 + (stack_level or 0))
end

--replace everything in assert with nop functions that just return their second argument, for near-zero overhead on release
function assert:nop()
	local nop = function(_, a)
		return a
	end
	setmetatable(self, {
		__call = nop,
	})
	for k, v in pairs(self) do
		self[k] = nop
	end
end

return assert
