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
