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

local core = require("core")
local command = require("core.command")
local Doc = require("core.doc")
local spreadsheet = require("core.lib.spreadsheet")
local utf8help = require("core.text.utf8help")

local function active_tsv_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("%.tsv$") then
		return doc
	end
	return nil
end

local function is_active_tsv()
	return active_tsv_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

--- Parses literal TSV fields while retaining the utility's LF-only line behavior
-- @param text string TSV source
-- @return table records
local function parse_tsv_records(text)
	-- Quotes stay ordinary data and carriage returns remain part of their cells
	return spreadsheet.parse_delimited(text, "\t", {
		quote = false,
		line_endings = "lf",
	}).rows
end

--- Align TSV text by displayed column width
-- @param text string TSV text
-- @return string aligned_text Space-aligned text
local function align_tsv_text(text)
	local records = parse_tsv_records(text)
	local column_widths = {}
	local padding_cache = {}
	local output_lines = {}

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

	for _, columns in ipairs(records) do
		local parts = {}
		for column_index = 1, #columns do
			local value = columns[column_index]
			if column_index < #columns then
				local pad = (column_widths[column_index] - utf8help.width(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_escape(value)
	return '"' .. value:gsub('"', '""') .. '"'
end

local function tsv_to_csv_text(text)
	local records = parse_tsv_records(text)
	local output_lines = {}
	for _, columns in ipairs(records) do
		for column_index = 1, #columns do
			columns[column_index] = csv_escape(columns[column_index])
		end
		output_lines[#output_lines + 1] = table.concat(columns, ",")
	end
	if text:sub(-1) == "\n" and output_lines[#output_lines] == '""' then
		output_lines[#output_lines] = nil
	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 TSV 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.register({
	["tsv:align"] = {
		requires = { "doc", "edit" },
		predicate = is_active_tsv,
		perform = function()
			local doc = active_tsv_doc()
			local input_text = get_target_text(doc)
			local aligned_text = align_tsv_text(input_text)
			open_generated_doc(doc, aligned_text, ".txt")
		end,
	},
	["tsv:align-inplace"] = {
		requires = { "doc", "edit" },
		predicate = is_active_tsv,
		perform = function()
			local doc = active_tsv_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_tsv_text(text)
				end)
				return
			end
			doc:replace(function(text)
				return align_tsv_text(text)
			end)
		end,
	},
	["tsv:convert-to-csv"] = {
		requires = { "doc", "edit" },
		predicate = is_active_tsv,
		perform = function()
			local doc = active_tsv_doc()
			local input_text = get_target_text(doc)
			local csv_text = tsv_to_csv_text(input_text)
			open_generated_doc(doc, csv_text, ".csv")
		end,
	},
})

return {
	align = align_tsv_text,
}
