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

-- String quote command plugin:
-- - provides 'string:quote'
-- - provides 'string:unquote'
-- - provides 'string:toggle-quote'
-- - resolves 'string:quote:<profile-id>' dynamically at execution time
-- - delegates profile/strategy logic to core.text.quotes
local core = require("core")
local command = require("core.command")
local keymap = require("core.keymap")
local config = require("core.config")
local quotes = require("core.text.quotes")


-- Unified user-facing status/log notifier for this plugin
local function notify(msg)
	if core.status_view and core.status_view.show_notice then
		core.status_view:show_notice("string: " .. msg)
	else
		core.log("string: %s", msg)
	end
end

-- Syntax-aware missing-profile message in comments.lua style
local function notify_missing_quote_support(doc, profile_id)
	local format_name = (doc and doc.format and doc.format._module_name) or "current format"
	local id = tostring(profile_id or "primary")
	core.log('No "%s" quote support configured for %s', id, format_name)
end

-- Applies a text transform based on selection policy:
-- - with selection: transform selected text
-- - without selection:
--   - "none" => notify
--   - otherwise transform current logical line
local function apply_transform(doc, transform_fn)
	-- Empty-selection policy comes from config
	local mode = config.quote_empty_selection_mode or "line"
	if doc:has_selection() then
		-- Selection path: replace selection with transformed selection text
		doc:replace(function(text)
			local changed, err = transform_fn(text)
			if not changed then
				notify(err)
				return text
			end
			return changed
		end)
		return
	end

	if mode == "none" then
		-- Explicitly require a selection for this mode
		notify("No selection")
		return
	end

	-- Line path: operate on the full logical line including optional trailing LF
	local line, col = doc:get_selection()
	local logical, had_lf = doc:get_logical_line(line)
	local changed, err = transform_fn(logical)
	if not changed then
		notify(err)
		return
	end
	local line_end_col = #logical + 1
	if had_lf then
		line_end_col = line_end_col + 1
	end
	-- Replace exact line range to avoid accidental extra blank line insertion
	doc:replace_range(line, 1, line, line_end_col, function()
		return changed
	end)
	-- Keep cursor stable for line-mode transforms:
	-- preserve original column and clamp only when the line became shorter
	local new_line_end_col = #changed + 1
	local target_col = math.min(col, new_line_end_col)
	doc:set_selection(line, target_col)
end

-- Applies wrapping via syntax primary/secondary profile resolution
local function apply_wrap(doc, profile_id)
	-- Resolve profile once so both validation and downstream calls use same id
	local profile, id = quotes.resolve_profile(doc, profile_id)
	if not profile then
		notify_missing_quote_support(doc, id)
		return
	end

	apply_transform(doc, function(text)
		return quotes.wrap_text(doc, text, id)
	end)
end

-- Applies unquote using syntax auto-detection
local function apply_unquote(doc)
	apply_transform(doc, function(text)
		return quotes.unquote_text(doc, text, nil)
	end)
end

-- Toggles quote state:
-- - if text can be unquoted, unquote it
-- - otherwise quote it using syntax primary profile
local function apply_toggle_quote(doc, quote_profile_id)
	apply_transform(doc, function(text)
		local unquoted = quotes.unquote_text(doc, text, nil)
		if unquoted then
			return unquoted, nil
		end
		return quotes.wrap_text(doc, text, quote_profile_id or "primary")
	end)
end

-- Static generic command:
-- - uses syntax primary profile resolution
command.register({
	["string:quote"] = {
		requires = { "doc", "edit" },
		perform = function()
			apply_wrap(core.active_view.doc, "primary")
		end,
	},
	["string:unquote"] = {
		requires = { "doc", "edit" },
		perform = function()
			apply_unquote(core.active_view.doc)
		end,
	},
	["string:toggle-quote"] = {
		requires = { "doc", "edit" },
		perform = function()
			apply_toggle_quote(core.active_view.doc)
		end,
	},
})

-- Dynamic command provider:
-- - resolves 'string:quote:<id>' at execution time
-- - lists currently available profile commands for command palette
command.register_dynamic(nil, {
	requires = { "doc", "edit" },
	-- Resolve command name to a runnable handler
	resolve = function(name)
		if type(name) ~= "string" then
			return nil
		end
		local quote_prefix = "string:quote:"
		if name:sub(1, #quote_prefix) == quote_prefix then
			local profile_id = name:sub(#quote_prefix + 1)
			if profile_id == "" then
				return nil
			end
			return function()
				apply_wrap(core.active_view.doc, profile_id)
			end
		end
		local toggle_prefix = "string:toggle-quote:"
		if name:sub(1, #toggle_prefix) == toggle_prefix then
			local profile_id = name:sub(#toggle_prefix + 1)
			if profile_id == "" then
				return nil
			end
			return function()
				apply_toggle_quote(core.active_view.doc, profile_id)
			end
		end
		return nil
	end,
	-- Enumerate command names for current active doc syntax
	list = function()
		local doc = core.active_view and core.active_view.doc or nil
		local ids = quotes.list_profile_ids(doc)
		local out = {}
		local seen = {}
		local function add(name)
			if not seen[name] then
				out[#out + 1] = name
				seen[name] = true
			end
		end
		local function add_alias(alias)
			local p = select(1, quotes.resolve_profile(doc, alias))
			if p then
				add("string:quote:" .. alias)
				add("string:toggle-quote:" .. alias)
			end
		end
		add_alias("primary")
		add_alias("secondary")
		for i = 1, #ids do
			add("string:quote:" .. ids[i])
			add("string:toggle-quote:" .. ids[i])
		end
		return out
	end,
})

-- Default keybinding entrypoint
keymap.bind("plugin", {
	text_editor = {
		["alt+q"] = "string:toggle-quote:primary",
		["ctrl+alt+q"] = "string:toggle-quote:secondary",
	},
})
