-- lx-bundle:true

-- This file is part of Lx, created by Liqube <www.liqube.com>
-- Copyright Liqube Audio. All rights reserved.
-- Lx Interim License. See LICENSE for details.

--- Load optional user-setting fragments without deciding which fragments a user's init file includes
-- @module core.settings

local utf8 = require("utf8")

local settings = {}
-- Shared patterns keep generated and direct settings names portable
local forbidden_filename_pattern = "[%z\1-\31<>:\"/\\|?*]"
local trailing_filename_pattern = "[%. ]+$"
local reserved_windows_names = {
	aux = true,
	["clock$"] = true,
	con = true,
	nul = true,
	prn = true,
}

for index = 1, 9 do
	reserved_windows_names["com" .. index] = true
	reserved_windows_names["lpt" .. index] = true
end

--- Return the directory containing optional setting fragments
-- @return string|nil directory
-- @return string|nil error_text
local function settings_directory()
	if type(USER_SETTINGS_DIR) ~= "string" or USER_SETTINGS_DIR == "" then
		return nil, "USER_SETTINGS_DIR is unavailable"
	end

	return USER_SETTINGS_DIR
end

--- Convert an OS-provided name into one safe lowercase filename component
-- @param name string name to sanitize
-- @return string|nil filename
-- @return string|nil error_text
function settings.sanitize_filename(name)
	if type(name) ~= "string" or name == "" then
		return nil, "filename must be a non-empty string"
	end

	local sanitized = utf8.lower(name)
	-- Pattern breakdown: the set replaces NUL, control bytes, and characters forbidden by Windows filenames
	sanitized = sanitized:gsub(forbidden_filename_pattern, "_")
	-- Pattern breakdown: one or more trailing dots or spaces are invalid on Windows and unsafe in portable profiles
	sanitized = sanitized:gsub(trailing_filename_pattern, "")
	if sanitized == "" or sanitized == "." or sanitized == ".." then
		return nil, "filename has no usable characters"
	end

	local first_dot = sanitized:find(".", 1, true)
	local stem = first_dot and sanitized:sub(1, first_dot - 1) or sanitized
	if reserved_windows_names[stem] then
		sanitized = "_" .. sanitized
	end

	return sanitized
end

--- Return the full path for one validated lowercase settings filename
-- @param filename string plain filename without a directory
-- @return string|nil path
-- @return string|nil error_text
function settings.path(filename)
	if type(filename) ~= "string" or filename == "" then
		return nil, "settings filename must be a non-empty string"
	end
	if filename:find("/", 1, true) or filename:find("\\", 1, true) or filename ~= utf8.lower(filename) then
		return nil, "settings filename must be one lowercase filename without a directory"
	end
	if filename:sub(-4) ~= ".lua" then
		return nil, "settings filename must end in '.lua'"
	end
	if filename:find(forbidden_filename_pattern) then
		return nil, "settings filename contains unsupported characters"
	end

	local first_dot = filename:find(".", 1, true)
	local stem = first_dot and filename:sub(1, first_dot - 1) or filename
	if reserved_windows_names[stem] then
		return nil, "settings filename uses a reserved Windows name"
	end

	local directory, directory_error = settings_directory()
	if not directory then
		return nil, directory_error
	end

	return system.join_path(directory, filename)
end

--- Load one optional lowercase Lua file from the user settings directory
-- @param filename string plain filename without a directory
-- @return boolean|nil loaded false means the optional file does not exist
-- @return string|nil error_text
function settings.include(filename)
	local path, path_error = settings.path(filename)
	if not path then
		return nil, path_error
	end
	if not system.is_file(path) then
		return false
	end

	local chunk, load_error = loadfile(path)
	if not chunk then
		return nil, string.format("could not load settings file '%s': %s", path, tostring(load_error))
	end

	local ran, run_error = pcall(chunk)
	if not ran then
		return nil, string.format("settings file '%s' failed: %s", path, tostring(run_error))
	end

	return true
end

--- Return the lowercase settings filename for the current computer
-- @return string|nil filename
-- @return string|nil error_text
function settings.computer_filename()
	local computer, computer_error = system.get_computer_name()
	if not computer then
		return nil, computer_error
	end

	local safe_computer, sanitize_error = settings.sanitize_filename(computer)
	if not safe_computer then
		return nil, sanitize_error
	end

	return "@" .. safe_computer .. ".lua"
end

--- Return the lowercase settings filename for the current user on the current computer
-- @return string|nil filename
-- @return string|nil error_text
function settings.user_computer_filename()
	local user, user_error = system.get_user_name()
	if not user then
		return nil, user_error
	end

	local computer, computer_error = system.get_computer_name()
	if not computer then
		return nil, computer_error
	end

	local safe_user, user_sanitize_error = settings.sanitize_filename(user)
	if not safe_user then
		return nil, user_sanitize_error
	end

	local safe_computer, computer_sanitize_error = settings.sanitize_filename(computer)
	if not safe_computer then
		return nil, computer_sanitize_error
	end

	return safe_user .. "@" .. safe_computer .. ".lua"
end

--- Return the full path for settings shared by every user on the current computer
-- @return string|nil path
-- @return string|nil error_text
function settings.computer_path()
	local filename, filename_error = settings.computer_filename()
	if not filename then
		return nil, filename_error
	end

	return settings.path(filename)
end

--- Return the full path for the current user's settings on the current computer
-- @return string|nil path
-- @return string|nil error_text
function settings.user_computer_path()
	local filename, filename_error = settings.user_computer_filename()
	if not filename then
		return nil, filename_error
	end

	return settings.path(filename)
end

return settings
