-- lx-bundle:true

--- Project scanning and project module loading helpers
-- Keep project-specific runtime behavior outside 'core.init'
-- @module core.project

local config = require("core.config")
local fileops = require("core.file.ops")

local project = {}

-- TODO: Replace the coroutine scanner with a real asynchronous project worker
-- Build it on core.workerboot and worker after that proof of concept becomes a dependable API
-- The project system should own roots, ignore rules, scan generations, cancellation, refresh, and one reusable filename index
-- Browser, find-file, project search, and file references should consume the same published snapshot

--- Return the live core module when it is already loaded
-- @return table|nil core
local function get_core()
	return package.loaded["core"]
end

--- Compare two project file snapshots for changes
-- @param previous_files table previous file list
-- @param next_files table next file list
-- @return boolean changed
local function diff_files(previous_files, next_files)
	if #previous_files ~= #next_files then
		return true
	end

	for index, entry in ipairs(previous_files) do
		if next_files[index].filename ~= entry.filename or next_files[index].modified ~= entry.modified then
			return true
		end
	end

	return false
end

--- Recompute project-scan enabled state from active reasons
function project.update_scan_enabled()
	local core = get_core()
	core.project_scan_enabled = next(core.project_scan_reasons) ~= nil
end

--- Enable background project scanning for one reason
-- @param reason string|nil reason key
-- @param immediate boolean|nil force an immediate scan when not false
function project.enable_scan(reason, immediate)
	local core = get_core()
	reason = reason or "manual"
	core.project_scan_reasons[reason] = true
	project.update_scan_enabled()

	if immediate ~= false then
		core.project_scan_force = true
	end
end

--- Disable background project scanning for one reason or all reasons
-- @param reason string|nil reason key
function project.disable_scan(reason)
	local core = get_core()

	if reason then
		core.project_scan_reasons[reason] = nil
	else
		for key in pairs(core.project_scan_reasons) do
			core.project_scan_reasons[key] = nil
		end
	end

	project.update_scan_enabled()
end

--- Toggle project scanning through the legacy boolean API
-- @param enabled boolean enable when true
-- @param immediate boolean|nil force an immediate scan when enabling
function project.set_scan_enabled(enabled, immediate)
	if enabled then
		project.enable_scan("manual", immediate)
	else
		project.disable_scan("manual")
	end
end

--- Request project scanning on demand
-- @param immediate boolean|nil force an immediate scan when not false
function project.request_scan(immediate)
	project.enable_scan("manual", immediate ~= false)
end

--- Scan project files incrementally and refresh cached file metadata
function project.scan_coroutine()
	local core = get_core()

	-- TODO: Coroutine yielding only spreads work across UI frames and does not move filesystem work off the UI thread

	while true do
		if core.project_scan_enabled then
			-- get project files and replace previous table if the new table is different
			local start_time = system.get_time()
			local files = fileops.get_files(".")
			local dt_ms = (system.get_time() - start_time) * 1000

			-- DEBUG: project background scan timing/count telemetry
			local debug_scan = config.project_scan_debug == true
			local warn_ms = math.max(1, tonumber(config.project_scan_warn_ms) or 120)

			if debug_scan or dt_ms >= warn_ms then
				local reasons = {}

				for key in pairs(core.project_scan_reasons or {}) do
					reasons[#reasons + 1] = tostring(key)
				end

				table.sort(reasons)
				core.log_quiet(
					"[Debug] Project scanned; dt: %.1fms, files: %d, enabled: %s reasons: %s",
					dt_ms,
					#files,
					tostring(core.project_scan_enabled),
					(#reasons > 0 and table.concat(reasons, ",") or "-")
				)
			end

			if diff_files(core.project_files, files) then
				core.project_files = files
				core.redraw = true
			end

			if core.project_scan_force then
				core.project_scan_force = false
				coroutine.yield()
			else
				coroutine.yield(config.project_scan_rate)
			end
		else
			coroutine.yield(1)
		end
	end
end

--- Initialize project scan state for the current session
function project.init_state()
	local core = get_core()
	core.project_files = {}
	core.project_scan_reasons = {}

	if config.project_scan_enabled == true then
		core.project_scan_reasons["config"] = true
	end

	project.update_scan_enabled()
	core.project_scan_force = core.project_scan_enabled
end

--- Start the background project scan coroutine
function project.add_scan_coroutine()
	local core = get_core()
	core.add_coroutine(project.scan_coroutine)
end

--- Load a project-local startup module when present
-- @return boolean loaded
function project.load_module()
	local core = get_core()
	local filename = ".lxproj.lua"

	if system.is_file(filename) then
		return core.try(function()
			local fn, err = loadfile(filename)

			if not fn then
				error("Error when loading project module:\n\t" .. err)
			end

			fn()
			core.log_quiet("Loaded project module")
		end)
	end

	return true
end

return project
