-- lx-bundle:true

local core = require("core")
local command = require("core.command")
local Doc = require("core.doc")

local function active_csv_doc()
	local view = core.active_view
	local doc = view and view.doc
	local filename = doc and doc.filename
	if type(filename) == "string" and filename:lower():match("%.csv$") then
		return doc
	end
	return nil
end

local function is_active_csv()
	return active_csv_doc() ~= nil
end

-- Keeps trailing-empty-line selection semantics consistent with other line commands
local function get_target_line_range(doc)
	if doc:has_selection() then
		local line1, _, line2, col2 = doc:get_selection(true)
		if line2 > line1 and col2 == 1 then
			line2 = line2 - 1
		end
		if line2 < line1 then
			line2 = line1
		end
		return line1, line2, true
	end
	return 1, #doc.lines, false
end

local function detect_csv_enclosure(text)
	if text:find('"', 1, true) then
		return '"'
	end
	if text:find("'", 1, true) then
		return "'"
	end
	return '"'
end

-- Parses CSV text into unquoted cell values while respecting quoted commas
local function parse_csv_records(text)
	local records = {}
	local record = {}
	local len = #text
	local field = ""
	local i = 1
	local in_quotes = false
	local enclosure = detect_csv_enclosure(text)

	if len == 0 then
		return { { "" } }
	end

	while i <= len do
		local ch = text:sub(i, i)
		if ch == enclosure then
			if in_quotes and text:sub(i + 1, i + 1) == enclosure then
				field = field .. enclosure
				i = i + 2
			else
				in_quotes = not in_quotes
				i = i + 1
			end
		elseif ch == "," and not in_quotes then
			record[#record + 1] = field
			field = ""
			i = i + 1
		elseif (ch == "\n" or ch == "\r") and not in_quotes then
			record[#record + 1] = field
			records[#records + 1] = record
			record = {}
			field = ""
			if ch == "\r" and text:sub(i + 1, i + 1) == "\n" then
				i = i + 2
			else
				i = i + 1
			end
		else
			field = field .. ch
			i = i + 1
		end
	end

	record[#record + 1] = field
	records[#records + 1] = record
	return records
end

-- Aligns CSV text by computing max width per column and padding with spaces
local function align_csv_text(text)
	local records = parse_csv_records(text)
	local column_widths = {}
	local padding_cache = {}
	local output_lines = {}

	for _, record in ipairs(records) do
		for column_index = 1, #record do
			local value = record[column_index]
			local value_len = #value
			local width = column_widths[column_index] or 0
			if value_len > width then
				column_widths[column_index] = value_len
			end
		end
	end

	for _, record in ipairs(records) do
		local parts = {}
		for column_index = 1, #record do
			local value = record[column_index]
			if column_index < #record then
				local pad = (column_widths[column_index] - #value) + 2
				local spaces = padding_cache[pad]
				if not spaces then
					spaces = string.rep(" ", pad)
					padding_cache[pad] = spaces
				end
				parts[#parts + 1] = value .. spaces
			else
				parts[#parts + 1] = value
			end
		end
		output_lines[#output_lines + 1] = table.concat(parts)
	end

	return table.concat(output_lines, "\n")
end

local function csv_to_tsv_text(text)
	local records = parse_csv_records(text)
	local output_lines = {}
	for _, record in ipairs(records) do
		output_lines[#output_lines + 1] = table.concat(record, "\t")
	end
	return table.concat(output_lines, "\n")
end

local function replace_extension(filename, extension)
	local base = filename or "untitled"
	local slash = base:match("^.*()[/\\]")
	local dot = base:match("^.*()%.")
	if dot and (not slash or dot > slash) then
		return base:sub(1, dot - 1) .. extension
	end
	return base .. extension
end

-- Opens a new document tab and fills it with provided content
local function open_generated_doc(source_doc, content, extension)
	local doc = Doc.create(replace_extension(source_doc.filename, extension))
	core.workspace_view:open_doc(doc)
	local last_line = #doc.lines
	local last_col = #(doc.lines[last_line] or "") + 1
	doc:remove(1, 1, last_line, last_col)
	doc:insert(1, 1, content)
	doc:set_selection(1, 1)
end

-- Returns selected text or whole-doc text for CSV alignment
local function get_target_text(doc)
	local line1, line2, has_selection = get_target_line_range(doc)
	if has_selection then
		return doc:get_text(line1, 1, line2, math.huge)
	end
	return doc:get_text(1, 1, #doc.lines, #doc.lines[#doc.lines])
end

command.add({
	["csv:align"] = {
		requires = { "doc", "edit" },
		predicate = is_active_csv,
		perform = function()
			local doc = active_csv_doc()
			local input_text = get_target_text(doc)
			local aligned_text = align_csv_text(input_text)
			open_generated_doc(doc, aligned_text, ".txt")
		end,
	},
	["csv:align-inplace"] = {
		requires = { "doc" },
		predicate = is_active_csv,
		perform = function()
			local doc = active_csv_doc()
			local line1, line2, has_selection = get_target_line_range(doc)
			if has_selection then
				doc:replace_range(line1, 1, line2, math.huge, function(text)
					return align_csv_text(text)
				end)
				return
			end
			doc:replace(function(text)
				return align_csv_text(text)
			end)
		end,
	},
	["csv:convert-to-tsv"] = {
		requires = { "doc", "edit" },
		predicate = is_active_csv,
		perform = function()
			local doc = active_csv_doc()
			local input_text = get_target_text(doc)
			local tsv_text = csv_to_tsv_text(input_text)
			open_generated_doc(doc, tsv_text, ".tsv")
		end,
	},
})
