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

--- Optional Windows Explorer integration
-- @module plugins.windowsexplorer

-- Other systems should not load dependencies or register commands for this plugin
if not WINDOWS then
	return
end

local core = require("core")
local command = require("core.command")
local fileops = require("core.file.ops")
local format_registry = require("core.format")

local APP_NAME = "LxPortable"
local APPLICATION_KEY = APP_NAME .. ".exe"
local PROGID_PREFIX = APP_NAME .. ".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

--- Show one Windows Explorer integration message
-- @param msg string message text
local function notify(msg)
	if core.status_view and core.status_view.show_notice then
		core.status_view:show_notice("Windows Explorer: " .. msg)
	else
		core.log("Windows Explorer: %s", msg)
	end
end

--- Convert one path to Windows separators
-- @param path string filesystem path
-- @return string windows_path
local function win_path(path)
	return tostring(path or ""):gsub("/", "\\")
end

--- Escape one registry string value
-- @param value string raw value
-- @return string escaped_value
local function reg_escape(value)
	local escaped = tostring(value or "")
	-- Pattern breakdown: set replaces NUL and control bytes that can split registry lines
	escaped = escaped:gsub("[%z\1-\31]", "_")

	return escaped:gsub("\\", "\\\\"):gsub('"', '\\"')
end

--- Make one safe registry key segment
-- @param name string raw segment
-- @return string safe_name
local function sanitize_reg_key_name(name)
	local safe = tostring(name or "")
	-- Registry key path separators cannot be part of one key segment
	-- Keep useful extension punctuation such as dots, dashes, and 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

--- Return whether one extension should never be registered
-- @param ext string extension
-- @return boolean excluded
local function is_excluded_file_format(ext)
	ext = tostring(ext or ""):lower():gsub("^%.", "")
	return excluded_file_format_set[ext] == true
end

--- Collect safe file associations from the current format registry
-- @return table[] types
local function collect_types()
	format_registry.init_registry({ force_update = true })
	local entries = format_registry.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

--- Append extension-to-handler mappings
-- @param lines string[] registry lines
-- @param types table[] file association types
local function append_file_associations(lines, types)
	for _, t in ipairs(types) do
		lines[#lines + 1] = '".' .. t.ext .. '"="' .. reg_escape(t.progid) .. '"'
	end
end

--- Append extensions supported by the executable
-- @param lines string[] registry lines
-- @param types table[] file association types
local function append_supported_types(lines, types)
	for _, t in ipairs(types) do
		lines[#lines + 1] = '".' .. t.ext .. '"=""'
	end
end

--- Build the Windows Explorer installation script
-- @param exe_file string executable path
-- @param types table[] file association types
-- @return string registry_text
local function install_text(exe_file, types)
	local exe_path = win_path(exe_file)
	local open_command = '"' .. exe_path .. '" "%1"'
	local lines = {
		"Windows Registry Editor Version 5.00",
		"",
		"; Generated by the Lx Windows Explorer plugin.",
		"; Per-user registration: no administrator rights required.",
		"; This registers LxPortable 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.",
		"; Open this file with Registry Editor to install the integration.",
		"",
		"[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\\" .. APPLICATION_KEY .. "]"
	lines[#lines + 1] = '"FriendlyAppName"="' .. APP_NAME .. '"'
	lines[#lines + 1] = ""
	lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\" .. APPLICATION_KEY .. "\\DefaultIcon]"
	lines[#lines + 1] = '@="' .. reg_escape(exe_path) .. ',0"'
	lines[#lines + 1] = ""
	lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\" .. APPLICATION_KEY .. "\\shell\\open\\command]"
	lines[#lines + 1] = '@="' .. reg_escape(open_command) .. '"'
	lines[#lines + 1] = ""
	lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\" .. APPLICATION_KEY .. "\\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\\" .. APPLICATION_KEY .. "\\Capabilities\\FileAssociations]"
	append_file_associations(lines, types)
	lines[#lines + 1] = ""
	lines[#lines + 1] = "[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\" .. APPLICATION_KEY .. "\\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

--- Build the Windows Explorer removal script
-- @param types table[] file association types
-- @return string registry_text
local function uninstall_text(types)
	local lines = {
		"Windows Registry Editor Version 5.00",
		"",
		"; Generated by the Lx Windows Explorer plugin.",
		"; Removes only the LxPortable integration installed by this plugin.",
		"; Open this file with Registry Editor to uninstall the integration.",
		"",
		"[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\\" .. APPLICATION_KEY .. "]"
	lines[#lines + 1] = ""
	lines[#lines + 1] = "[-HKEY_CURRENT_USER\\Software\\" .. APP_NAME .. "]"

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

--- Build and write matching install and uninstall registry files
-- @param install_path string install file path
-- @param uninstall_path string uninstall file path
-- @return number|nil extension_count
-- @return string|nil error_message
local function write_reg_files(install_path, uninstall_path)
	local types = collect_types()
	if #types == 0 then
		return nil, "no file extensions found"
	end

	local ok, err = system.write_file_atomic(install_path, install_text(EXE_FILE, types))
	if not ok then
		return nil, string.format("failed to write %s: %s", install_path, tostring(err))
	end

	ok, err = system.write_file_atomic(uninstall_path, uninstall_text(types))
	if not ok then
		os.remove(install_path)
		return nil, string.format("failed to write %s: %s", uninstall_path, tostring(err))
	end

	return #types
end

--- Generate permanent registry files beside the executable
local function generate_reg_files()
	local install_path = system.join_path(EXE_DIR, "lx-explorer-install.reg")
	local uninstall_path = system.join_path(EXE_DIR, "lx-explorer-uninstall.reg")
	local extension_count, write_error = write_reg_files(install_path, uninstall_path)

	if not extension_count then
		core.error("Windows Explorer: %s", write_error)
		return
	end

	core.log("Windows Explorer: generated %s", install_path)
	core.log("Windows Explorer: generated %s", uninstall_path)
	notify(string.format("generated install and uninstall files for %d extensions", extension_count))
end

--- Quote one path for the Windows command processor
-- @param path string filesystem path
-- @return string quoted_path
local function quote_command_path(path)
	return '"' .. tostring(path):gsub('"', '""') .. '"'
end

--- Open one temporary registry file and remove it after Registry Editor closes
-- @param path string temporary registry file
-- @return boolean|nil launched
-- @return string|nil error_message
local function import_reg_file(path)
	local quoted_path = quote_command_path(path)
	local launch_command = 'start "" /wait regedit.exe '
		.. quoted_path
		.. " & del /f /q "
		.. quoted_path

	return system.exec(launch_command)
end

--- Create and open one temporary registry script
-- @param uninstall boolean create the uninstall script
local function run_integration(uninstall)
	local types = collect_types()
	if #types == 0 then
		core.error("Windows Explorer: no file extensions found")
		return
	end

	local filename = uninstall and "lx-explorer-uninstall.reg" or "lx-explorer-install.reg"
	local path, path_error = fileops.temp_path(filename)
	if not path then
		core.error("Windows Explorer: failed to create a temporary path: %s", tostring(path_error))
		return
	end

	local text = uninstall and uninstall_text(types) or install_text(EXE_FILE, types)
	local written, write_error = system.write_file_atomic(path, text)

	if not written then
		core.error("Windows Explorer: failed to write %s: %s", path, tostring(write_error))
		return
	end

	local launched, launch_error = import_reg_file(path)
	if not launched then
		os.remove(path)
		core.error("Windows Explorer: failed to open Registry Editor: %s", tostring(launch_error))
		return
	end

	notify(uninstall and "opened the uninstall confirmation" or "opened the install confirmation")
end

command.register({
	["windows-explorer:generate-reg-files"] = {
		perform = generate_reg_files,
	},
	["windows-explorer:install"] = {
		perform = function()
			run_integration(false)
		end,
	},
	["windows-explorer:uninstall"] = {
		perform = function()
			run_integration(true)
		end,
	},
})
