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

--- OpenDocument extraction commands
-- Keeps text, spreadsheet, and presentation readers unloaded until needed
-- @module plugins.odfutils

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

local readers = {}

local outputs = {
	text = {
		command = "extract-text",
		method = "extract_text",
		suffix = ".extracted.txt",
		label = "text",
	},
	markdown = {
		command = "extract-md",
		method = "extract_markdown",
		suffix = ".extracted.md",
		label = "Markdown",
	},
	tsv = {
		command = "extract-tsv",
		method = "extract_tsv",
		suffix = ".extracted.tsv",
		label = "TSV",
	},
}

local formats = {
	odt = {
		label = "ODT",
		reader = "plugins.odfutils.odt",
		outputs = { "text", "markdown" },
	},
	ods = {
		label = "ODS",
		reader = "plugins.odfutils.ods",
		outputs = { "text", "markdown", "tsv" },
		word_wrap = false,
		hide_markdown_outline = true,
	},
	odp = {
		label = "ODP",
		reader = "plugins.odfutils.odp",
		outputs = { "text", "markdown" },
	},
	odg = {
		label = "ODG",
		reader = "plugins.odfutils.odg",
		outputs = { "text", "markdown" },
	},
}

--- Load one OpenDocument reader only when extraction needs it
-- @param format table format definition
-- @return table reader_module
local function get_reader(format)
	local reader = readers[format.reader]
	if not reader then
		reader = require(format.reader)
		readers[format.reader] = reader
	end

	return reader
end

--- Return the active supported OpenDocument
-- @param format_name string expected office type
-- @return table|nil doc
local function active_document(format_name)
	local view = core.active_view
	local doc = view and view.doc
	if not doc or doc.is_binary_placeholder ~= true then
		return nil
	end

	local declared_type = type(doc.format) == "table" and doc.format.office_type or nil
	local extension = type(doc.filename) == "string" and doc.filename:lower():match("%.([^./\\]+)$") or nil

	return (declared_type == format_name or declared_type == nil and extension == format_name) and doc or nil
end

--- Open or replace one extracted OpenDocument result
-- @param source_doc table source document
-- @param content string extracted content
-- @param output table output definition
-- @param format table format definition
-- @param context table|nil command display options
-- @return boolean opened
local function open_output_document(source_doc, content, output, format, context)
	local doc = Doc.create((source_doc.filename or "untitled") .. output.suffix)
	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)
	doc.save_as_required = true
	doc:clean()

	-- Spreadsheet outlines usually contain only sheet names and waste preview space
	if output == outputs.markdown and format.hide_markdown_outline then
		doc.markdown_outline_visible = false
	end

	local options = {
		open_mode = context and context.open_mode or nil,
		word_wrap = format.word_wrap,
	}
	local view
	if context and context.replace_view then
		view = core.workspace_view:replace_doc_view(context.replace_view, doc, options)
	else
		view = core.workspace_view:open_doc(doc, options)
	end

	return view ~= nil
end

--- Extract one active OpenDocument into a generated document
-- @param format_name string source format name
-- @param output table output definition
-- @param context table|nil command display options
-- @return boolean extracted
local function extract(format_name, output, context)
	local format = formats[format_name]
	local doc = active_document(format_name)
	if not doc then
		core.error("%s extraction requires an active binary %s document", format.label, format.label)
		return false
	end

	local content, err = get_reader(format)[output.method](doc.filename)
	if not content then
		core.error("%s extraction failed: %s", format.label, tostring(err))
		return false
	end
	if not open_output_document(doc, content, output, format, context) then
		core.error("%s extraction succeeded, but its document could not be displayed", format.label)
		return false
	end

	core.log('Extracted "%s" as %s', doc.filename, output.label)

	return true
end

local commands = {}

for format_name, format in pairs(formats) do
	for _, output_name in ipairs(format.outputs) do
		local output = outputs[output_name]
		commands[format_name .. ":" .. output.command] = {
			predicate = function()
				return active_document(format_name) ~= nil
			end,
			perform = function(context)
				return extract(format_name, output, context)
			end,
		}
	end
end

command.register(commands)

return {
	formats = formats,
	get_reader = get_reader,
}
