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

--- Microsoft Office extraction commands
-- Keeps format readers unloaded until one of their commands is used
-- @module plugins.msoutils

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 = {
	doc = {
		label = "DOC",
		extensions = { doc = true },
		types = { doc = true },
		reader = "plugins.msoutils.doc",
		outputs = { "text", "markdown" },
	},
	docx = {
		label = "DOCX",
		extensions = { docx = true },
		types = { docx = true },
		reader = "plugins.msoutils.docx",
		outputs = { "text", "markdown" },
	},
	xls = {
		label = "XLS",
		extensions = { xls = true },
		types = { xls = true },
		reader = "plugins.msoutils.xls",
		outputs = { "text", "markdown", "tsv" },
		word_wrap = false,
	},
	xlsx = {
		label = "XLSX",
		extensions = { xlsm = true, xlsx = true },
		types = { xlsm = true, xlsx = true },
		reader = "plugins.msoutils.xlsx",
		outputs = { "text", "markdown", "tsv" },
		word_wrap = false,
		hide_markdown_outline = true,
	},
}

--- Load one format 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 whether one filename has an extension handled by a format
-- @param filename string|nil source filename
-- @param extensions table supported extensions
-- @return boolean matches
local function filename_matches(filename, extensions)
	local extension = type(filename) == "string" and filename:lower():match("%.([^./\\]+)$") or nil

	return extension ~= nil and extensions[extension] == true
end

--- Return the active binary document for one Office format
-- @param format table format definition
-- @return table|nil doc
local function active_document(format)
	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 matches = declared_type and format.types[declared_type]
		or declared_type == nil and filename_matches(doc.filename, format.extensions)

	return matches and doc or nil
end

--- Open or replace one extracted Office document
-- @param source_doc table source Office 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()

	-- Workbook 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 view_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, view_options)
	else
		view = core.workspace_view:open_doc(doc, view_options)
	end

	return view ~= nil
end

--- Extract one active Office document into a generated document
-- @param format table format definition
-- @param output table output definition
-- @param context table|nil command display options
-- @return boolean extracted
local function extract(format, output, context)
	local doc = active_document(format)
	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 name, format in pairs(formats) do
	for _, output_name in ipairs(format.outputs) do
		local command_format = format
		local command_output = outputs[output_name]
		commands[name .. ":" .. command_output.command] = {
			predicate = function()
				return active_document(command_format) ~= nil
			end,
			perform = function(context)
				return extract(command_format, command_output, context)
			end,
		}
	end
end

command.register(commands)

return {
	formats = formats,
	get_reader = get_reader,
}
