--[[
	extra mathematical functions
]]

local mathx = setmetatable({}, {
	__index = math,
})

--wrap v around range [lo, hi)
function mathx.wrap(v, lo, hi)
	return (v - lo) % (hi - lo) + lo
end

--wrap i around the indices of t
function mathx.wrap_index(i, t)
	return math.floor(mathx.wrap(i, 1, #t + 1))
end

--clamp v to range [lo, hi]
function mathx.clamp(v, lo, hi)
	return math.max(lo, math.min(v, hi))
end

--clamp v to range [0, 1]
function mathx.clamp01(v)
	return mathx.clamp(v, 0, 1)
end

--round v to nearest whole, away from zero
function mathx.round(v)
	if v < 0 then
		return math.ceil(v - 0.5)
	end
	return math.floor(v + 0.5)
end

--round v to one-in x
-- (eg x = 2, v rounded to increments of 0.5)
function mathx.to_one_in(v, x)
	return mathx.round(v * x) / x
end

--round v to a given decimal precision
function mathx.to_precision(v, decimal_points)
	return mathx.to_one_in(v, math.pow(10, decimal_points))
end

--0, 1, -1 sign of a scalar
--todo: investigate if a branchless or `/abs` approach is faster in general case
function mathx.sign(v)
	if v < 0 then return -1 end
	if v > 0 then return 1 end
	return 0
end

--linear interpolation between a and b
function mathx.lerp(a, b, t)
	return a * (1.0 - t) + b * t
end

--linear interpolation with a minimum "final step" distance
--useful for making sure dynamic lerps do actually reach their final destination
function mathx.lerp_eps(a, b, t, eps)
	local v = mathx.lerp(a, b, t)
	if math.abs(v - b) < eps then
		v = b
	end
	return v
end

--bilinear interpolation between 4 samples
function mathx.bilerp(a, b, c, d, u, v)
	return mathx.lerp(
		mathx.lerp(a, b, u),
		mathx.lerp(c, d, u),
		v
	)
end

--get the lerp factor on a range, inverse_lerp(6, 0, 10) == 0.6
function mathx.inverse_lerp(v, min, max)
	return (v - min) / (max - min)
end

--remap a value from one range to another
function mathx.remap_range(v, in_min, in_max, out_min, out_max)
	return mathx.lerp(out_min, out_max, mathx.inverse_lerp(v, in_min, in_max))
end

--remap a value from one range to another, staying within that range
function mathx.remap_range_clamped(v, in_min, in_max, out_min, out_max)
	return mathx.lerp(out_min, out_max, mathx.clamp01(mathx.inverse_lerp(v, in_min, in_max)))
end

--easing curves
--(generally only "safe" for 0-1 range, see mathx.clamp01)

--no curve - can be used as a default to avoid needing a branch
function mathx.identity(f)
	return f
end

--classic smoothstep
function mathx.smoothstep(f)
	return f * f * (3 - 2 * f)
end

--classic smootherstep; zero 2nd order derivatives at 0 and 1
function mathx.smootherstep(f)
	return f * f * f * (f * (f * 6 - 15) + 10)
end

--pingpong from 0 to 1 and back again
function mathx.pingpong(f)
	return 1 - math.abs(1 - (f * 2) % 2)
end

--quadratic ease in
function mathx.ease_in(f)
	return f * f
end

--quadratic ease out
function mathx.ease_out(f)
	local oneminus = (1 - f)
	return 1 - oneminus * oneminus
end

--quadratic ease in and out
--(a lot like smoothstep)
function mathx.ease_inout(f)
	if f < 0.5 then
		return f * f * 2
	end
	local oneminus = (1 - f)
	return 1 - 2 * oneminus * oneminus
end

--branchless but imperfect quartic in/out
--either smooth or smootherstep are usually a better alternative
function mathx.ease_inout_branchless(f)
	local halfsquared = f * f / 2
	return halfsquared * (1 - halfsquared) * 4
end

--todo: more easings - back, bounce, elastic

--(internal; use a provided random generator object, or not)
local function _random(rng, ...)
	if rng then return rng:random(...) end
	if love then return love.math.random(...) end
	return math.random(...)
end

--return a random sign
function mathx.random_sign(rng)
	return _random(rng) < 0.5 and -1 or 1
end

--return a random value between two numbers (continuous)
function mathx.random_lerp(min, max, rng)
	return mathx.lerp(min, max, _random(rng))
end

--nan checking
function mathx.isnan(v)
	return v ~= v
end

--angle handling stuff
--superior constant handy for expressing things in turns
mathx.tau = math.pi * 2

--normalise angle onto the interval [-math.pi, math.pi)
--so each angle only has a single value representing it
function mathx.normalise_angle(a)
	return mathx.wrap(a, -math.pi, math.pi)
end

--alias for americans
mathx.normalize_angle = mathx.normalise_angle

--get the normalised difference between two angles
function mathx.angle_difference(a, b)
	a = mathx.normalise_angle(a)
	b = mathx.normalise_angle(b)
	return mathx.normalise_angle(b - a)
end

--mathx.lerp equivalent for angles
function mathx.lerp_angle(a, b, t)
	local dif = mathx.angle_difference(a, b)
	return mathx.normalise_angle(a + dif * t)
end

--mathx.lerp_eps equivalent for angles
function mathx.lerp_angle_eps(a, b, t, eps)
	--short circuit to avoid having to wrap so many angles
	if a == b then
		return a
	end
	--same logic as lerp_eps
	local v = mathx.lerp_angle(a, b, t)
	if math.abs(mathx.angle_difference(v, b)) < eps then
		v = b
	end
	return v
end

--geometric functions standalone/"unpacked" components and multi-return
--consider using vec2 if you need anything complex!

--rotate a point around the origin by an angle
function mathx.rotate(x, y, r)
	local s = math.sin(r)
	local c = math.cos(r)
	return c * x - s * y, s * x + c * y
end

--get the length of a vector from the origin
function mathx.length(x, y)
	return math.sqrt(x * x + y * y)
end

--get the distance between two points
function mathx.distance(x1, y1, x2, y2)
	local dx = x1 - x2
	local dy = y1 - y2
	return mathx.length(dx, dy)
end

return mathx
