-- File: tkz_elements_utils.lua
-- Copyright (c) 2026 Alain Matthes
-- SPDX-License-Identifier: LPPL-1.3c
-- Maintainer: Alain Matthes

utils = {}

-- ----------------------------
-- Parsing / formatting helpers
-- ----------------------------

-- Parse a string of the form "(x,y)" into two numbers.
function utils.parse_point(str)
	local x, y = tostring(str):match("^%s*%(?%s*([%+%-%.%deE]+)%s*,%s*([%+%-%.%deE]+)%s*%)%s*$")
	if not x or not y then
		tex.error("Invalid point string: " .. tostring(str))
	end
	return tonumber(x), tonumber(y)
end

-- Internal: format a number to fixed decimals (with checks)
local function _fmt_number(x, decimals, ctx)
	decimals = decimals or 5
	local n = tonumber(x)
	if not n then
		tex.error((ctx or "Invalid number") .. ": " .. tostring(x))
	end
	return string.format("%." .. decimals .. "f", n)
end

-- Public: format a number with a specified number of decimal places.
function utils.format_number(x, decimals)
	return _fmt_number(x, decimals, "Invalid input to format_number")
end

-- Public: alias (kept for backward compatibility)
utils.checknumber       = utils.format_number


-- Coordinates formatting (single component)
function utils.format_coord(x, decimals)
	return _fmt_number(x, decimals, "Invalid coordinate")
end

-- Format a complex number (table with .re and .im) as a coordinate string.
function utils.format_point(z, decimals)
	if type(z) ~= "table" or type(z.re) ~= "number" or type(z.im) ~= "number" then
		tex.error("format_point expects a table with fields 're' and 'im'")
	end
	decimals = decimals or 5
	local x = _fmt_number(z.re, decimals, "Invalid x in format_point")
	local y = _fmt_number(z.im, decimals, "Invalid y in format_point")
	return "(" .. x .. "," .. y .. ")"
end

-- ----------------------------
-- Numeric helpers
-- ----------------------------

-- Compare two numbers with tolerance (robust default).
function utils.almost_equal(a, b, EPS)
	EPS = EPS or (tkz and tkz.epsilon) or 1e-9
	local na, nb = tonumber(a), tonumber(b)
	if not na or not nb then
		tex.error("almost_equal expects numbers: " .. tostring(a) .. ", " .. tostring(b))
	end
	return math.abs(na - nb) <= EPS
end

-- Optional: Clamp a number between min and max.
function utils.clamp(x, min_val, max_val)
	return math.max(min_val, math.min(max_val, x))
end

-- Optional: Return the sign of a number.
function utils.sign(x)
	return (x > 0 and 1) or (x < 0 and -1) or 0
end

-- ----------------------------
-- Logging helpers
-- ----------------------------

-- Print formatted message to terminal and log.
function utils.w(...)
	texio.write_nl("term and log", "[tkz-elements] " .. string.format(...))
end

-- Print formatted message to log only.
function utils.wlog(...)
	texio.write_nl("log", "[tkz-elements] " .. string.format(...))
end

-- ----------------------------
-- Table helpers
-- ----------------------------

-- Length of contiguous array part 1..n (stops at first nil).
-- (Name explicit to avoid confusion with #t on sparse tables.)
function utils.table_len_contiguous(t)
	if type(t) ~= "table" then
		tex.error("bad argument #1 (table expected)", 2)
	end
	local i = 0
	while t[i + 1] ~= nil do
		i = i + 1
	end
	return i
end

-- Backward compatibility
utils.table_getn = utils.table_len_contiguous


-- --- internal helper: compile a chunk returning a function -------------------
local function compile_func_(chunk, where, payload, fallback)
	fallback = fallback or function(_) return 0 end

	local loader, err = load(chunk, where, "t", math)
	if not loader then
		tex.error("Invalid function expression", {
			payload or chunk,
			err
		})
		return fallback
	end

	local ok, fn = pcall(loader)
	if not ok or type(fn) ~= "function" then
		tex.error("Invalid function expression", {
			payload or chunk,
			tostring(fn)
		})
		return fallback
	end

	return fn
end

-- Compile a TeX-provided expression into a Lua function y = f(x)
function utils.compile_fx_(expr)
	local chunk = "return function(x) return (" .. expr .. ") end"
	return compile_func_(
		chunk,
		"tkz.compile_fx",
		expr,
		function(_) return 0 end
	)
end

-- Compile a parametric TeX-provided expression into a Lua function (x,y) = f(t)
function utils.compile_ft_(exprx, expry)
	local chunk = ("return function(t) return (%s), (%s) end"):format(exprx, expry)
	-- fallback paramétrique : renvoie (0,0) pour rester compatible avec build_path
	return compile_func_(
		chunk,
		"tkz.compile_ft",
		("x(t)=%s ; y(t)=%s"):format(exprx, expry),
		function(_) return 0, 0 end
	)
end


-- Build a path from a function f(x)
function utils.build_path_(f, xmin, xmax, n)
	xmin   = tonumber(xmin)
	xmax   = tonumber(xmax)
	n      = tonumber(n) or 100

	local p = path()
	local step = (xmax - xmin) / n
	for i = 0, n do
		local x = xmin + i * step
		local y = f(x)
		if y == y and y ~= math.huge and y ~= -math.huge then
			p:add_point(point(x, y))
		end
	end
	return p
end

-- Build a path from a parametric function g(t) -> x, y
function utils.build_param_path_(g, tmin, tmax, n)
	tmin = tonumber(tmin)
	tmax = tonumber(tmax)
	n    = tonumber(n) or 200

	local p = path()
	local step = (tmax - tmin) / n

	for i = 0, n do
		local t = tmin + i * step
		local x, y = g(t)

		if x == x and y == y
			 and x ~= math.huge and x ~= -math.huge
			 and y ~= math.huge and y ~= -math.huge then
			p:add_point(point(x, y))
		end
	end
	return p
end

return utils
