-- 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.

local common = require("core.common")

local hook = {
	_subs = {
		["doc.save"] = {},
		["doc.open"] = {},
		["doc.new"] = {},
		["doc.close"] = {},
		["doc.monitor"] = {},
		["doc.flash_line"] = {},
		["core.log"] = {},
		["core.event"] = {},
		["core.line_change"] = {},
	},
	_next_token = 1,
	_next_order = 1,
}

hook.path = {}

--- Normalize path values for stable cross-platform comparisons
-- @param path string|nil raw filesystem path
-- @return string|nil normalized_path comparison path with forward separators
function hook.path.normalize(path)
	if type(path) ~= "string" or path == "" then
		return nil
	end

	local absolute_path = system.absolute_path(path) or path

	return system.fix_slashes(system.path_key(absolute_path), "/")
end

--- Compare two paths after normalization
-- @param path_a string|nil first path
-- @param path_b string|nil second path
-- @return boolean equal true when both paths identify the same file
function hook.path.eq(path_a, path_b)
	local a = hook.path.normalize(path_a)
	local b = hook.path.normalize(path_b)
	return a ~= nil and b ~= nil and a == b
end

--- Match a normalized path against a Lua pattern
-- @param path string|nil filesystem path
-- @param glob string Lua pattern kept for the existing hook API
-- @return boolean matches true when the pattern matches
function hook.path.match(path, glob)
	local normalized = hook.path.normalize(path)
	if not normalized or type(glob) ~= "string" or glob == "" then
		return false
	end
	return not not common.match_pattern(normalized, glob)
end

local function get_core()
	return package.loaded["core"]
end

local function sort_subscribers(list)
	table.sort(list, function(a, b)
		if a.priority ~= b.priority then
			return a.priority < b.priority
		end
		return a.order < b.order
	end)
end

local event_handlers = {
	["doc.save"] = {
		before = true,
		after = true,
		error = true,
	},
	["doc.open"] = {
		before = true,
		after = true,
		error = true,
	},
	["doc.new"] = {
		before = true,
		after = true,
		error = true,
	},
	["doc.close"] = {
		before = true,
		after = true,
	},
	["doc.monitor"] = {
		changed = true,
		deleted = true,
		restored = true,
		reloaded = true,
		prompted = true,
		toggle = true,
	},
	["doc.flash_line"] = {
		after = true,
	},
	["core.log"] = {
		after = true,
	},
	["core.event"] = {
		windowmode = true,
		textinput = true,
		keydown = true,
		keyup = true,
		mousemove = true,
		mouseleave = true,
		mousedown = true,
		mouseup = true,
		mousewheel = true,
		filedrop = true,
		quit = true,
	},
	["core.line_change"] = {
		dispatch = true,
	},
}

local function validate_handlers(event_name, spec)
	local allowed = event_handlers[event_name]
	assert(allowed ~= nil, "hook.add: unsupported event name")

	local has_any = false

	for stage_name in pairs(allowed) do
		local handler = spec[stage_name]
		if handler ~= nil then
			assert(type(handler) == "function", "hook.add: handler fields must be functions")
			has_any = true
		end
	end

	assert(has_any, "hook.add: spec requires at least one valid handler")
	return allowed
end

-- Register one grouped event subscriber and return a token for removal
function hook.add(event_name, spec)
	assert(type(event_name) == "string" and event_name ~= "", "hook.add: event name must be a non-empty string")
	assert(type(spec) == "table", "hook.add: spec table expected")

	local allowed = validate_handlers(event_name, spec)
	local priority = tonumber(spec.priority)

	if priority then
		priority = math.floor(priority)
	else
		priority = 100
	end

	local token = hook._next_token
	hook._next_token = hook._next_token + 1

	local entry = {
		token = token,
		id = spec.id,
		once = spec.once == true,
		priority = priority,
		order = hook._next_order,
	}
	for stage_name in pairs(allowed) do
		entry[stage_name] = spec[stage_name]
	end
	hook._next_order = hook._next_order + 1

	local list = hook._subs[event_name]

	list[#list + 1] = entry
	sort_subscribers(list)
	return token
end

-- Remove one subscriber by token
function hook.remove(token)
	if type(token) ~= "number" then
		return false
	end

	for _, list in pairs(hook._subs) do
		for i = 1, #list do
			if list[i].token == token then
				table.remove(list, i)
				return true
			end
		end
	end

	return false
end

--- Emit one grouped event stage to all subscribers in priority order
-- @param event_name string event name
-- @param stage string event stage
-- @param payload table event data
-- @return table result handled and failed callback counts
function hook.emit(event_name, stage, payload)
	local allowed = event_handlers[event_name]

	if not allowed then
		return { handled = 0, failed = 0, }
	end

	if not allowed[stage] then
		return { handled = 0, failed = 0, }
	end

	local list = hook._subs[event_name]

	if not list or #list == 0 then
		return { handled = 0, failed = 0, }
	end

	local handled = 0
	local failed = 0
	local to_remove = {}
	local traceback = (_G.debug and _G.debug.traceback) or tostring
	local dispatch = {}

	-- Freeze this pass so callback changes apply to the next emit
	for i = 1, #list do
		dispatch[i] = list[i]
	end

	for i = 1, #dispatch do
		local entry = dispatch[i]
		local handler = entry[stage]

		if type(handler) == "function" then
			local ok, err = xpcall(function()
				return handler(payload)
			end, traceback)

			if ok then
				handled = handled + 1
				if entry.once then
					to_remove[#to_remove + 1] = entry.token
				end
			else
				failed = failed + 1
				local core = get_core()

				if core and type(core.error_with_trace) == "function" then
					local traceback_text = tostring(err)
					local first_line = traceback_text:match("^[^\r\n]+") or traceback_text
					local summary = first_line:match("^.-:%d+:%s*(.*)$") or first_line

					-- Report the failing subscriber frame instead of the hook dispatcher frame
					core.error_with_trace(
						traceback_text,
						"Hook error in event %q stage %q%s: %s",
						tostring(event_name),
						tostring(stage),
						entry.id and (" [" .. tostring(entry.id) .. "]") or "",
						tostring(summary)
					)
				elseif core then
					core.error(
						"Hook error in event %q stage %q%s: %s",
						tostring(event_name),
						tostring(stage),
						entry.id and (" [" .. tostring(entry.id) .. "]") or "",
						tostring(err)
					)
				end
			end
		end
	end

	for i = 1, #to_remove do
		hook.remove(to_remove[i])
	end

	return { handled = handled, failed = failed, }
end

-- Clear all subscribers for one event or for all events
function hook.clear(event_name)
	if event_name == nil then
		for name in pairs(hook._subs) do
			hook._subs[name] = {}
		end
		return
	end

	if hook._subs[event_name] then
		hook._subs[event_name] = {}
	end
end

return hook
