-- lx-bundle:true
-- module: reg-files

local core = require("core")
local command = require("core.command")
local syntax = require("core.syntax")

local APP_NAME = "Lx"
local PROGID_PREFIX = "Lx.File"
local EXCLUDED_FILE_FORMATS = {
	".cmd",
	".bat",
	".ps1",
	".vbs",
	".js",
	".jse",
	".wsf",
	".reg",
	".msi",
	".exe",
	".scr",
	".com",
}

local excluded_file_format_set = {}
for _, ext in ipairs(EXCLUDED_FILE_FORMATS) do
	ext = tostring(ext or ""):lower():gsub("^%.", "")
	if ext ~= "" then
		excluded_file_format_set[ext] = true
	end
end

local function notify(msg)
	if core.status_view and core.status_view.show_notice then
		core.status_view:show_notice("reg-files: " .. msg)
	else
		core.log("reg-files: %s", msg)
	end
end

local function win_path(path)
	return tostring(path or ""):gsub("/", "\\")
end

local function basename(path)
	return tostring(path or ""):match("([^/\\]+)$") or "lx.exe"
end

local function join_path(a, b)
	if system and system.join_path then
		return system.join_path(a, b)
	end
	return tostring(a):gsub("[/\\]$", "") .. PATHSEP .. b
end

local function reg_escape(value)
	return tostring(value or ""):gsub("\\", "\\\\"):gsub('"', '\\"')
end

local function sanitize_reg_key_name(name)
	local safe = tostring(name or "")
	-- Registry key path separators cannot be part of one key segment.
	-- Keep extension punctuation that is valid and useful in ProgIDs: dots, dashes, plus signs.
	safe = safe:gsub("[/\\]", "_")
	safe = safe:gsub("[%z\1-\31]", "_")
	safe = safe:gsub("^%s+", ""):gsub("%s+$", "")
	if safe == "" then
		safe = "file"
	end
	return safe
end

local function is_excluded_file_format(ext)
	ext = tostring(ext or ""):lower():gsub("^%.", "")
	return excluded_file_format_set[ext] == true
end

local function write_file(path, text)
	local fp, err = io.open(path, "wb")
	if not fp then
		return nil, err
	end
	fp:write(text)
	fp:close()
	return true
end

local function collect_types()
	syntax.init_registry({ force_update = true })
	local entries = syntax.get_file_association_entries()
	local by_ext = {}

	for _, entry in ipairs(entries) do
		local name = type(entry.name) == "string" and entry.name ~= "" and entry.name or "File"
		for _, ext in ipairs(entry.extensions or {}) do
			ext = tostring(ext or ""):lower():gsub("^%.", "")
			if ext ~= ""
				and not is_excluded_file_format(ext)
				and not ext:find("[^a-z0-9_+%-.]")
				and not by_ext[ext]
			then
				by_ext[ext] = {
					ext = ext,
					name = name,
				}
			end
		end
	end

	local list = {}
	for _, info in pairs(by_ext) do
		list[#list + 1] = info
	end
	table.sort(list, function(a, b)
		return a.ext < b.ext
	end)

	local used = {}
	for _, info in ipairs(list) do
		local base = PROGID_PREFIX .. "." .. sanitize_reg_key_name(info.ext)
		local progid = base
		local n = 2
		while used[progid:lower()] do
			progid = base .. "_" .. n
			n = n + 1
		end
		used[progid:lower()] = true
		info.progid = progid
	end

	return list
end

local function append_file_associations(lines, types)
	for _, t in ipairs(types) do
		lines[#lines + 1] = '".' .. t.ext .. '"="' .. reg_escape(t.progid) .. '"'
	end
end

local function append_supported_types(lines, types)
	for _, t in ipairs(types) do
		lines[#lines + 1] = '".' .. t.ext .. '"=""'
	end
end

local function register_text(exe_file, types)
	local exe_path = win_path(exe_file)
	local exe_name = basename(exe_path)
	local open_command = '"' .. exe_path .. '" "%1"'
	local lines = {
		"Windows Registry Editor Version 5.00",
		"",
		"; Generated by Lx reg-files plugin.",
		"; Per-user registration: no administrator rights required.",
		"; This registers Lx as an available handler; Windows 10/11 still require the user to choose defaults.",
		"; Regenerate this file after moving lx.exe; .reg files cannot discover the portable executable path.",
		"; Import with: reg import lx-register.reg",
		"",
		"[HKEY_CURRENT_USER\\Software\\RegisteredApplications]",
		'"' .. APP_NAME .. '"="Software\\\\' .. APP_NAME .. '\\\\Capabilities"',
		"",
		"[HKEY_CURRENT_USER\\Software\\" .. APP_NAME .. "\\Capabilities]",
		'"ApplicationName"="' .. APP_NAME .. '"',
		'"ApplicationDescription"="Lx text, code, and file viewer"',
		"",
		"[HKEY_CURRENT_USER\\Software\\" .. APP_NAME .. "\\Capabilities\\FileAssociations]",
	}

	append_file_associations(lines, types)

	lines[#lines + 1] = ""
	lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\" .. reg_escape(exe_name) .. "]"
	lines[#lines + 1] = '"FriendlyAppName"="' .. APP_NAME .. '"'
	lines[#lines + 1] = ""
	lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\" .. reg_escape(exe_name) .. "\\DefaultIcon]"
	lines[#lines + 1] = '@="' .. reg_escape(exe_path) .. ',0"'
	lines[#lines + 1] = ""
	lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\" .. reg_escape(exe_name) .. "\\shell\\open\\command]"
	lines[#lines + 1] = '@="' .. reg_escape(open_command) .. '"'
	lines[#lines + 1] = ""
	lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\" .. reg_escape(exe_name) .. "\\Capabilities]"
	lines[#lines + 1] = '"ApplicationName"="' .. APP_NAME .. '"'
	lines[#lines + 1] = '"ApplicationDescription"="Lx text, code, and file viewer"'
	lines[#lines + 1] = ""
	lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\" .. reg_escape(exe_name) .. "\\Capabilities\\FileAssociations]"
	append_file_associations(lines, types)
	lines[#lines + 1] = ""
	lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\" .. reg_escape(exe_name) .. "\\SupportedTypes]"
	append_supported_types(lines, types)

	for _, t in ipairs(types) do
		local desc = t.name .. " (" .. APP_NAME .. ")"
		lines[#lines + 1] = ""
		lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\" .. reg_escape(t.progid) .. "]"
		lines[#lines + 1] = '@="' .. reg_escape(desc) .. '"'
		lines[#lines + 1] = '"FriendlyTypeName"="' .. reg_escape(desc) .. '"'
		lines[#lines + 1] = ""
		lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\" .. reg_escape(t.progid) .. "\\DefaultIcon]"
		lines[#lines + 1] = '@="' .. reg_escape(exe_path) .. ',0"'
		lines[#lines + 1] = ""
		lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\" .. reg_escape(t.progid) .. "\\shell\\open\\command]"
		lines[#lines + 1] = '@="' .. reg_escape(open_command) .. '"'
		lines[#lines + 1] = ""
		lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\." .. reg_escape(t.ext) .. "\\OpenWithProgids]"
		lines[#lines + 1] = '"' .. reg_escape(t.progid) .. '"=hex(0):'
	end

	return table.concat(lines, "\r\n") .. "\r\n"
end

local function unregister_text(exe_file, types)
	local exe_name = basename(win_path(exe_file))
	local lines = {
		"Windows Registry Editor Version 5.00",
		"",
		"; Generated by Lx reg-files plugin.",
		"; Removes only the Lx portable registration values/keys emitted by lx-register.reg.",
		"; Import with: reg import lx-unregister.reg",
		"",
		"[HKEY_CURRENT_USER\\Software\\RegisteredApplications]",
		'"' .. APP_NAME .. '"=-',
	}

	for _, t in ipairs(types) do
		lines[#lines + 1] = ""
		lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\." .. reg_escape(t.ext) .. "\\OpenWithProgids]"
		lines[#lines + 1] = '"' .. reg_escape(t.progid) .. '"=-'
	end

	for _, t in ipairs(types) do
		lines[#lines + 1] = ""
		lines[#lines + 1] = "[-HKEY_CURRENT_USER\\Software\\Classes\\" .. reg_escape(t.progid) .. "]"
	end

	lines[#lines + 1] = ""
	lines[#lines + 1] = "[-HKEY_CURRENT_USER\\Software\\Classes\\Applications\\" .. reg_escape(exe_name) .. "]"
	lines[#lines + 1] = ""
	lines[#lines + 1] = "[-HKEY_CURRENT_USER\\Software\\" .. APP_NAME .. "]"

	return table.concat(lines, "\r\n") .. "\r\n"
end

local function generate()
	local types = collect_types()
	if #types == 0 then
		core.error("reg-files: no syntax extensions found")
		return
	end

	local register_path = join_path(EXE_DIR, "lx-register.reg")
	local unregister_path = join_path(EXE_DIR, "lx-unregister.reg")

	local ok, err = write_file(register_path, register_text(EXE_FILE, types))
	if not ok then
		core.error("reg-files: failed to write %s: %s", register_path, tostring(err))
		return
	end

	ok, err = write_file(unregister_path, unregister_text(EXE_FILE, types))
	if not ok then
		core.error("reg-files: failed to write %s: %s", unregister_path, tostring(err))
		return
	end

	core.log("reg-files: generated %s", register_path)
	core.log("reg-files: generated %s", unregister_path)
	notify(string.format("generated register/unregister files for %d extensions", #types))
end

command.add({
	["reg-files:generate"] = {
		perform = generate,
	},
})
