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

-- Splits one TSV line and keeps empty cells, including trailing empty cells
local function split_tsv(line)
	local columns = {}
	local start_pos = 1
	while true do
		local tab_pos = line:find("\t", start_pos, true)
		if not tab_pos then
			columns[#columns + 1] = line:sub(start_pos)
			break
		end
		columns[#columns + 1] = line:sub(start_pos, tab_pos - 1)
		start_pos = tab_pos + 1
	end
	return columns
end

-- Iterates logical lines from a text block without allocating an intermediate list
local function for_each_logical_line(text, fn)
	local start_pos = 1
	local len = #text
	if len == 0 then
		fn("")
		return
	end
	while true do
		local newline_pos = text:find("\n", start_pos, true)
		if not newline_pos then
			fn(text:sub(start_pos))
			break
		end
		fn(text:sub(start_pos, newline_pos - 1))
		start_pos = newline_pos + 1
		if start_pos > len then
			fn("")
			break
		end
	end
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 column_widths = {}
	local padding_cache = {}
	local output_lines = {}

	for_each_logical_line(text, function(line)
		local columns = split_tsv(line)
		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_each_logical_line(text, function(line)
		local columns = split_tsv(line)
		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 output_lines = {}
	for_each_logical_line(text, function(line)
		local columns = split_tsv(line)
		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,
}
