-- lx-bundle:true

local core = require("core")
local command = require("core.command")
local keymap = require("core.keymap")
local config = require("core.config")
-- Text cursor translation helpers are scoped under core.text
local translate = require("core.text.translate")

-- Case conversion utilities for symbol and selected text transforms

-- Converts camel hump boundaries captured by gsub into snake segments
local function camel_to_snake(a, b)
	return a .. "_" .. string.lower(b)
end

-- Converts camelCase or TitleCase into snake_case
local function to_snake(symbol)
	-- Pattern breakdown:
	-- (.) captures the previous character
	-- ([A-Z]) captures an uppercase character that starts a new hump
	return symbol:gsub("(.)([A-Z])", camel_to_snake):lower()
end

-- Converts snake_case tokens into Title Case words without separators
local function to_title(symbol)
	local parts = {}
	-- Pattern breakdown:
	-- [^_]+ matches one or more non-underscore characters
	for part in symbol:gmatch("[^_]+") do
		parts[#parts + 1] = part:sub(1, 1):upper() .. part:sub(2):lower()
	end
	return table.concat(parts)
end

-- Converts snake_case into camelCase
local function to_camel_from_snake(symbol)
	local title = to_title(symbol)
	return title:sub(1, 1):lower() .. title:sub(2)
end

-- Cycles word style across snake_case camelCase and TitleCase
local function toggle(symbol)
	-- Pattern breakdown:
	-- [a-z] checks for at least one lowercase letter before transforming
	if not symbol:match("[a-z]") then
		return symbol
	-- Pattern breakdown:
	-- _ checks for snake_case separator presence
	elseif symbol:match("_") then
		-- snake_case -> camelCase
		return to_camel_from_snake(symbol)
	-- Pattern breakdown:
	-- ^ anchors at start
	-- [A-Z] checks for TitleCase leading uppercase
	elseif symbol:match("^[A-Z]") then
		-- TitleCase -> snake_case
		return to_snake(symbol)
	-- Pattern breakdown:
	-- ^ anchors at start
	-- [a-z] checks for camelCase leading lowercase
	elseif symbol:match("^[a-z]") then
		-- camelCase -> TitleCase
		return symbol:sub(1, 1):upper() .. symbol:sub(2)
	end
	return symbol
end

-- Title-cases each whitespace-delimited token in free text
local function titlecase_text(text)
	-- Pattern breakdown:
	-- (%S) captures first non-space character in each token
	-- (%S*) captures the rest of that token
	return text:gsub("(%S)(%S*)", function(a, b)
		return string.upper(a) .. string.lower(b)
	end)
end

-- Uppercases the first non-space character in the text
local function first_capital_text(text)
	-- Pattern breakdown:
	-- ^ anchors at start
	-- (%s*) captures leading whitespace
	-- (%S) captures first non-space character
	return (text:gsub("^(%s*)(%S)", function(ws, c)
		return ws .. string.upper(c)
	end))
end

-- Uses the current selection or resolves the current word span
local function get_range_or_word(doc)
	if doc:has_selection() then
		return doc:get_selection(true)
	end
	local line, col = doc:get_selection()
	local line1, col1 = doc:position_offset(line, col, translate.start_of_word)
	local line2, col2 = doc:position_offset(line, col, translate.end_of_word)
	return line1, col1, line2, col2
end

-- Applies transform function to selected text or current word on one line
local function apply_to_range(doc, fn)
	local line1, col1, line2, col2 = get_range_or_word(doc)
	if line1 ~= line2 then
		return
	end
	local text = doc:get_text(line1, col1, line2, col2)
	if text == "" then
		return
	end
	local next_text = fn(text)
	if not next_text or next_text == text then
		return
	end
	doc:remove(line1, col1, line2, col2)
	doc:insert(line1, col1, next_text)
end

command.add({
	["case:cameltitlesnake"] = {
		requires = { "doc", "edit" },
		perform = function()
		local active_view = core.active_view
		local doc = active_view.doc
		local line, col = doc:get_selection()
		local line1, col1 = doc:position_offset(line, col, translate.start_of_word)
		local line2, col2 = doc:position_offset(line, col, translate.end_of_word)
		if line1 ~= line2 then
			return
		end
		local text = doc:get_text(line1, col1, line2, col2)
		-- Pattern breakdown:
		-- ^ anchors at start
		-- config.symbol_pattern validates symbol-safe word content
		-- $ anchors at end
		if not text:match("^" .. config.symbol_pattern .. "$") then
			return
		end
		local toggled = toggle(text)
		if not toggled or toggled == text then
			return
		end
		local sel = { doc:get_selection() }
		local scroll_x, scroll_y = active_view.scroll.x, active_view.scroll.y
		-- Preserve selection and scroll offsets when replacing the symbol text
		doc:remove(line1, col1, line2, col2)
		doc:insert(line1, col1, toggled)
		doc:set_selection(table.unpack(sel))
		active_view.scroll.x, active_view.scroll.y = scroll_x, scroll_y
		end,
	},

	["case:lower"] = {
		requires = { "doc", "edit" },
		perform = function()
		apply_to_range(core.active_view.doc, string.lower)
		end,
	},

	["case:upper"] = {
		requires = { "doc", "edit" },
		perform = function()
		apply_to_range(core.active_view.doc, string.upper)
		end,
	},

	["case:title"] = {
		requires = { "doc", "edit" },
		perform = function()
		apply_to_range(core.active_view.doc, titlecase_text)
		end,
	},

	["case:capital"] = {
		requires = { "doc", "edit" },
		perform = function()
		apply_to_range(core.active_view.doc, first_capital_text)
		end,
	},
})

keymap.add({
	["ctrl+shift+l"] = "case:lower",
	["ctrl+shift+u"] = "case:upper",
	["ctrl+shift+t"] = "case:title",
	["f6"] = "case:cameltitlesnake",
})
