local M = {}
local mkutils = require "mkutils"
local lfs     = require "lfs"
local os      = require "os"
local kpse    = require "kpse"
local filter  = require "make4ht-filter"
local domfilter  = require "make4ht-domfilter"
local domobject  = require "luaxml-domobject"
local xtpipeslib = require "make4ht-xtpipes"
local log = logging.new "odt"


function M.prepare_parameters(settings, extensions)
 settings.tex4ht_sty_par = settings.tex4ht_sty_par ..",ooffice"
 settings.tex4ht_par = settings.tex4ht_par .. " ooffice/! -cmozhtf"
 -- settings.t4ht_par = settings.t4ht_par .. " -cooxtpipes -coo "
 -- settings.t4ht_par = settings.t4ht_par .. " -cooxtpipes "
 settings = mkutils.extensions_prepare_parameters(extensions, settings)
 return settings
end

-- object for working with the ODT file
local Odtfile = {}
Odtfile.__index = Odtfile

Odtfile.new = function(archivename)
 local self = setmetatable({}, Odtfile)
 -- create a temporary file
 local tmpname = os.tmpname()
 -- remove a temporary file, we are interested only in the unique file name
 os.remove(tmpname)
 -- get the unique dir name
 tmpname = tmpname:match("([a-zA-Z0-9_%-%.]+)$")
 local status, msg = lfs.mkdir(tmpname)
 if not status then return nil, msg end
 -- make picture dir
 lfs.mkdir(tmpname .. "/Pictures")
 self.archivelocation = tmpname
 self.name = archivename
 return self
end

function Odtfile:copy(src, dest)
 mkutils.cp(src, self.archivelocation .. "/" .. dest)
end

function Odtfile:move(src, dest)
 mkutils.mv(src, self.archivelocation .. "/" .. dest)
end

function Odtfile:create_dir(dir)
 local currentdir = lfs.currentdir()
 lfs.chdir(self.archivelocation)
 lfs.mkdir(dir)
 lfs.chdir(currentdir)
end

function Odtfile:make_mimetype()
 self.mimetypename = "mimetype"
 local m, msg = io.open(self.mimetypename, "w")
 if not m then
   log:error(msg)
   return nil, msg
 end
 m:write("application/vnd.oasis.opendocument.text")
 m:close()
end

function Odtfile:remove_mimetype()
 os.remove(self.mimetypename)
end


function Odtfile:pack()
 local currentdir = lfs.currentdir()
 local zip_command = mkutils.find_zip()
 lfs.chdir(self.archivelocation)
 -- make temporary mime type file
 self:make_mimetype()
 mkutils.execute(zip_command .. ' -q0X "' .. self.name .. '" ' .. self.mimetypename)
 -- remove it, so the next command doesn't overwrite it
 self:remove_mimetype()
 mkutils.execute(zip_command ..' -r "' .. self.name .. '" *')
 lfs.chdir(currentdir)
 mkutils.cp(self.archivelocation .. "/" .. self.name, mkutils.file_in_builddir(self.name, Make.params))
 mkutils.delete_dir(self.archivelocation)
end

--- *************************
--  *** fix picture sizes ***
--  *************************
--
local function add_points(dimen)
 if type(dimen) ~= "string" then return dimen end
 -- convert SVG dimensions to points if only number is provided
 if dimen:match("[0-9]$") then return dimen .. "pt" end
 return dimen
end

local function get_svg_dimensions(filename)
 local width, height
 if mkutils.file_exists(filename) then
   for line in io.lines(filename) do
     width = line:match("width%s*=%s*[\"'](.-)[\"']") or width
     height = line:match("height%s*=%s*[\"'](.-)[\"']") or height
     -- stop parsing once we get both width and height
     if width and height then break end
   end
 end
 width = add_points(width)
 height = add_points(height)
 return width, height
end

local function get_xbb_dimensions(filename)
 local f = io.popen("ebb -x -O " .. filename)
 if f then
   local content = f:read("*all")
   local width, height = content:match("%%BoundingBox: %d+ %d+ (%d+) (%d+)")
   return add_points(width), add_points(height)
 end
 return nil
end
--
local function fix_picture_sizes(tmpdir)
 local filename = tmpdir .. "/content.xml"
 local f = io.open(filename, "r")
 if not f then
   log:warning("Cannot open ", filename, "for picture size fixes")
   return nil
 end
 local content = f:read("*all") or ""
 f:close()
 local status, dom= pcall(function()
   return domobject.parse(content)
 end)
 if not status then
   log:warning("Cannot parse DOM, the resulting ODT file will be most likely corrupted")
   return nil
 end
 for _, pic in ipairs(dom:query_selector("draw|image")) do
   local imagename = pic:get_attribute("xlink:href")
   -- update SVG images dimensions
   log:debug("image", imagename)
   local parent = pic:get_parent()
   local width =  parent:get_attribute("svg:width")
   local height = parent:get_attribute("svg:height")
   -- if width == "0.0pt" then width = nil end
   -- if height == "0.0pt" then height = nil end
   if not width or not height then
     local imgfilename = tmpdir .. "/" .. imagename
     if imagename:match("svg$") then
       width, height = get_svg_dimensions(imgfilename) --  or width, height
     elseif imagename:match("png$") or imagename:match("jpe?g$") then
       width, height = get_xbb_dimensions(imgfilename)
     end
   end
   log:debug("new dimensions", width, height)
   parent:set_attribute("svg:width", width)
   parent:set_attribute("svg:height", height)
   -- if
 end
 -- save the modified DOM again
 log:debug("Fixed picture sizes")
 local domcontent = dom:serialize()
 local f, msg = io.open(filename, "w")
 if not f then
   log:error(msg)
   return nil, msg
 end
 f:write(domcontent)
 f:close()
end

-- fix font records in the lg file that don't correct Font_Size record
local lg_fonts_processed=false
local patched_lg_fonts = {}
local function fix_lgfile_fonts(ignored_name, params)
 -- this function is called from file match. we must use the name of the .lg file
 local filename = mkutils.file_in_builddir(params.input .. ".lg", params)
 if not lg_fonts_processed then
   local lines = {}
   -- default font_size
   local font_size = "10"
   if mkutils.file_exists(filename) then
     --
     for line in io.lines(filename) do
       -- default font_size can be set in the .lg file
       if line:match("Font_Size") then
         font_size = line:match("Font_Size:%s*(%d+)")
       elseif line:match("Font%(") then
         -- match Font record
         local name, size, size2, size3 = line:match('Font%("([^"]+)","([%d]*)","([%d]+)","([%d]+)"')
         -- find if the first size is not set, and add the default font_size then
         if size == "" then
           line = string.format('Font("%s","%s","%s","%s")', name, font_size, size2, size3)
           -- we must also save the font name and size for later post-processing, because
           -- we will need to fix styles in content.xml too
           patched_lg_fonts[name .. "-" .. font_size] = true
         end
       end
       lines[#lines+1] = line
     end
     -- save changed lines to the lg file
     local f = io.open(filename, "w")
     for _,line in ipairs(lines) do
       f:write(line .. "\n")
     end
     f:close()
   end
   filter_settings "odtfonts" {patched_lg_fonts = patched_lg_fonts}
 end
 lg_fonts_processed=true
 return true
end

local move_matches = xtpipeslib.move_matches

local function insert_lgfile_fonts(make)
 local params = make.params
 local first_file = mkutils.file_in_builddir(params.input .. ".4oo", params)
 -- find the last file and escape it so it can be used
 -- in filename match
 make:match(first_file, fix_lgfile_fonts)
 move_matches(make)
end

-- escape string to be used in the gsub search
local function escape_file(filename)
 local quotepattern = '(['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..'])'
 return filename:gsub(quotepattern, "%%%1")
end


-- call xtpipes from Lua
local function call_xtpipes(make)
 -- we must find root of the TeX distribution
 local selfautoparent = xtpipeslib.get_selfautoparent()

 if selfautoparent then
   local matchfunction = xtpipeslib.get_xtpipes(selfautoparent)
   make:match("4oo", matchfunction)
   make:match("4om", matchfunction)
   -- move last match to a first place
   -- we need to move last two matches, for 4oo and 4om files
   move_matches(make)
   move_matches(make)
   -- fix font records in the lg file
   insert_lgfile_fonts(make)
 else
   log:warning "Cannot locate xtpipes. Try to set TEXMFROOT variable to a root directory of your TeX distribution"
 end
end

-- sort output files according to their extensions
local function prepare_output_files(lgfiles)
 local groups = {}
 for _, name in ipairs(lgfiles) do
   local basename, extension = name:match("(.-)%.([^%.]+)$")
   local group = groups[extension] or {}
   table.insert(group, basename)
   groups[extension] = group
   log:debug("prepare output file", basename, extension)
 end
 return groups
end

-- execute function on all files in the group
-- function fn takes current filename and table with various attributes
local function exec_group(groups, name, fn)
 for _, basename in ipairs(groups[name] or {}) do
   fn{basename = basename, extension=name, filename = basename .. "." .. name}
 end
end

-- remove <?xtpipes XML instructions, because they cause issues in some ODT processing
-- applications
local function remove_xtpipes(text)
 -- remove <?x
 return text:gsub("%<%?xtpipes.-%?%>", "")
end

function M.modify_build(make)
 local executed = false
 -- execute xtpipes from the build file, instead of t4ht. this fixes issues with wrong paths
 -- expanded in tex4ht.env in Miktex or Debian
 call_xtpipes(make)
 -- fix the image dimensions wrongly set by xtpipes
 local domfilters = domfilter({"t4htlinks", "odtpartable"}, "odtfilters")
 make:match("4oo$", domfilters)
 -- execute it before xtpipes, because we don't want xtpipes to mess with t4htlink elements
 move_matches(make)
 -- fixes for mathml
 local mathmldomfilters = domfilter({"joincharacters","mathmlfixes"}, "mathmlfilters")
 make:match("4om$", mathmldomfilters)
 -- DOM filters that should be executed after xtpipes
 local latedom = domfilter({"odtfonts"}, "lateodtfilters")
 make:match("4oo$", latedom)
 -- convert XML entities for Unicode characters produced by Xtpipes to characters
 local fixentities = filter {"entities-to-unicode", remove_xtpipes}
 make:match("4oo", fixentities)
 make:match("4om", fixentities)
 -- we must handle outdir. make4ht copies the ODT file before it was packed, so
 -- we will copy it again after packing later in this format file
 local outdir = make.params["outdir"]

 -- build the ODT file. This match must be executed as a last one
 -- this will be executed as a first match, just to find the last filename
 -- in the lgfile
 make:match(".*", function()
   -- execute it only once
   if not executed then
     -- this is list of processed files
     local lgfiles = make.lgfile.files
     for k,v in ipairs(lgfiles) do
       if v:match("odt$") then table.remove(lgfiles, k) end
     end
     -- find the last file and escape it so it can be used
     -- in filename match
     local lastfile = escape_file(lgfiles[#lgfiles]) .."$"
     -- make match for the last file
     -- odt packing will be done here
     make:match(lastfile, function(filename, par)
       local groups = prepare_output_files(make.lgfile.files)
       -- we must remove any path from the basename
       -- local basename = groups.odt[1]:match("([^/]+)$")
       local basename = make.params.input
       local odtname = basename .. ".odt"
       local odt,msg = Odtfile.new(odtname)
       if not odt then
         log:error("Cannot create ODT file: " .. msg)
       end
       -- helper function for simple file moving
       local function move_file(group, dest)
         exec_group(groups, group, function(par)
           odt:move("${filename}" % par, dest)
         end)
       end

       -- the document text
       exec_group(groups, "4oo", function(par)
         odt:move("${filename}" % par, "content.xml")
         odt:create_dir("Pictures")
       end)

       -- manifest
       exec_group(groups, "4of", function(par)
         odt:create_dir("META-INF")
         odt:move("${filename}" % par, "META-INF/manifest.xml")
       end)

       -- math
       exec_group(groups, "4om", function(par)
         odt:create_dir(par.basename)
         odt:move("${filename}" % par, "${basename}/content.xml" % par)
         -- copy the settings file to math subdir
         local settings = groups["4os"][1]
         odt:copy(settings .. ".4os", "${basename}/settings.xml" % par)
       end)

       -- these files are created only once, so it doesn't matter that they are
       -- copied to one file
       move_file("4os", "settings.xml")
       move_file("4ot", "meta.xml")
       move_file("4oy", "styles.xml")

       -- pictures
       exec_group(groups, "4og", function(par)
         -- add support for images in the TEXMF tree
         if not mkutils.file_exists(par.basename) then
           par.basename = kpse.find_file(par.basename, "graphic/figure")
           if not par.basename then return nil, "Cannot find picture" end
         end
         -- the Pictues dir is flat, without subdirs
         odt:copy("${basename}" % par, "Pictures")
       end)

       -- fix picture sizes in the content file
       fix_picture_sizes(odt.archivelocation)

       -- remove some spurious file
       exec_group(groups, "4od", function(par)
         os.remove(par.filename)
       end)

       odt:pack()
       local build_filename = mkutils.file_in_builddir(odt.name, make.params)
       if outdir and outdir ~= "" then
         local outfilename = outdir .. "/" .. odt.name
         log:info("Copying ODT file to the output dir: " .. outfilename)
         mkutils.copy(build_filename,outfilename)
       elseif build_filename ~= odt.name then
         mkutils.cp(build_filename, odt.name)
       end
     end)
   end
   executed = true
 end)
 return make
end
return M