% Package odsfile. Author Michal Hoftich <michal.h21@gmail.com>
% This package is subject of LPPL license, version 1.3c
\ProvidesPackage{odsfile}[2024/10/10 v0.9 odsfile package to select cells from ODS sheets and
      typeset them as LaTeX tables]
\RequirePackage{luacode,xkeyval,xparse}

%keyval keys

\define@key{includespread}{file}{\loadodsfile{#1}}%
\define@key{includespread}{sheet}{\luaexec{sheetname = "\luatexluaescapestring{#1}"}}%
\define@key{includespread}{range}{\luaexec{range="\luatexluaescapestring{#1}"}}%
\define@key{includespread}{template}{\luaexec{currenttemplate="\luatexluaescapestring{#1}"}}%
\define@key{includespread}{rowtemplate}{\luaexec{rowtemplate="\luatexluaescapestring{\detokenize{#1}}"}}%
\define@key{includespread}{celltemplate}{\luaexec{celltpl="\luatexluaescapestring{\detokenize{#1}}"}}%
\define@key{includespread}{multicoltemplate}{\luaexec{multicoltpl="\luatexluaescapestring{\unexpanded{#1}}"}}%
\define@key{includespread}{escape}{\luaexec{latexescape="\luatexluaescapestring{\unexpanded{#1}}"}}%

\newcommand\OdsNl{\\}
\newcommand\OdsLastNl{\\}

\define@choicekey*+{includespread}{rowseparator}[\val\nr]{tableline,hline,newline}[tableline]%
{%
\ifcase\nr\relax%
  \luaexec{rowseparator=""}%
\or%
  \luaexec{rowseparator="\\hline "}%
\or%
\luaexec{%
rowseparator=[[\\n]]
}
\fi%
}{%
	\luaexec{rowseparator="\luatexluaescapestring{#1}"}
}
\define@key{includespread}{columnbreak}{%
  \luaexec{columnbreak="\luatexluaescapestring{\unexpanded{#1}}{}"}%
}

\define@key{includespread}{save}{%
  \luaexec{odssave="\luatexluaescapestring{#1}"}%
}

\define@key{includespread}{debug}{% use either true or false
  \luaexec{odsdebug=\luatexluaescapestring{#1}}%
}

\define@key{includespread}{coltypes}{%
  \luaexec{coltypes="\luatexluaescapestring{\unexpanded{#1}}"}%
}
\define@choicekey*+{includespread}{columns}[\val\nr]{head,top,none}{%
\ifcase\nr\relax%
  \luaexec{columns=1}%
\or%
  \luaexec{columns=2}%
\fi%
}{%
\luaexec{%
local function split(s,sep)
        local sep, fields = sep or ":", {}
        local pattern = string.format("([^\%s]+)", sep)
        s:gsub(pattern, function(c) fields[\#fields+1] = c end)
        return fields
end
local s="\luatexluaescapestring{#1}"
columns = split(s,",")
}%
}{}%

\define@key{includespread}{newline}{%
  \luaexec{odsnl="\luatexluaescapestring{\unexpanded{#1}}"}%
}

\define@key{includespread}{lastnewline}{%
  \luaexec{odslastnl="\luatexluaescapestring{\unexpanded{#1}}"}%
}



% Variable initialization and helper functions
\begin{luacode*}
odsreader   = require("odsfile")
odsfile     = nil
sheetname   = nil
range       = {nil,nil,nil,nil}
columns     = nil
templates   = {}
row         = {}
odssave     = nil
odsdebug    = nil
body        = nil
odsfilename = ""
currenttemplate = nil
rowtemplate = nil
celltpl   = "-{value}"
multicoltpl = "\\multicolumn{-{count}}{l}{-{value}}"
latexescape = "true"
odsnl       = "\\OdsNl"
odslastnl   = "\\OdsLastNl"
\end{luacode*}

\newcommand\loadodsfile[2][]{%
  \setkeys{includespread}{#1}%
  \luaexec{%
    odsfilename     = "\luatexluaescapestring{#2}"%
    local ods   = odsreader.load(odsfilename)%
    odsfile, e  = ods:loadContent()%
  }%
}

\newcommand\tabletemplate[2]{%
  \luaexec{%
    templates["#1"]="\luatexluaescapestring{\unexpanded{#2}}"%
  }
}

\NewDocumentCommand\includespread{s o}{%
  \IfBooleanTF#1%
  {\ods@includespread@star{#2}}%
  {\ods@includespread@unstar{#2}}%
}

\newcommand\ods@includespread@star[1]{%
  \let\ods@tmp@lastNL\OdsLastNl%
  \renewcommand\OdsLastNl{}%
  \ods@includespread@unstar{#1}%
  \let\OdsLastNl\ods@tmp@lastNL%
}

\newcommand\ods@includespread@unstar[1]{%
  \luaexec{%
    range = nil
    rowseparator = ""
    columns=nil
    currenttemplate = nil 
    odssave = nil
    odsdebug = false
    rowtemplate = nil
    celltpl = "-{value}"
    columnbreak = "\\linebreak{}"
    coltypes    = nil
    latexescape = "true"
  }%
  \setkeys{includespread}{#1}%
  \luaexec{%
    body   = odsreader.getTable(odsfile,sheetname)
    local ranges = odsreader.getRange(range)
    local values = {}
    for _, real_range in ipairs(ranges) do
      values = odsreader.join(values, odsreader.tableValues(body,real_range[1],real_range[2],real_range[3],real_range[4]))
    end
    %-- Conversion of odsfile table values to LaTeX tabular  
    local concatParagraphs = function(column)
      % -- second returned value signalize whether cell contain paragraph, or not 
		  local getCell = odsreader.get_cell
      if type(column) =="table" then 
        return getCell(column, columnbreak), true
      end
      return getCell(column,""), false
    end
    local rowValues = function(row, headings)
      local headings = headings or {}
      local t={} 
      local i = 1
      for _,column in pairs(row) do 
        local attr = column.attr or {}
        local value, br  = concatParagraphs(column.value) 
        value = value or ""
        local x = attr["table:number-columns-spanned"] or "1"
        x = tonumber(x)
        if x > 1 then 
           value = odsreader.interp(multicoltpl, {value = value, count = x})
        else
          value = odsreader.interp(celltpl, {value = value})
        end
        % table.insert(t,value)
        t[i] = value
        headings[i] = br
        i = i + x
      end
      return t, headings
    end
    local makeColtypes = function(h)
      local maxsize = tex.hsize / 65536
      local h = h or {}
      local p = 0
      for _, c in pairs(h) do
        if c then 
          p = p + 1 
        end 
      end
      if p > 0 then
        local j = {}
        local size = tostring(math.floor(maxsize / \#h)) .. "pt"
        for _, c in pairs(h) do 
          local k = "l"
          if c then k = "p{"..size.."}" end
          j[\#j+1]= k
        end
        return table.concat(j)
      else 
        return string.rep("l",\#h)
      end
    end
    if rowtemplate == nil then
      local headings = {}
      local currow = {}
      currenttemplate = currenttemplate or "default"
      content = {}   
      for i,row in pairs(values) do
        currow, headings = rowValues(row, headings)
      	table.insert(content,table.concat(currow," & "))
      end
      %-- Column headings handling
      local colheading=""
      if type(columns) == "number" and columns == 1 then
      	columns = rowValues(values[1])
      	content = odsreader.table_slice(content,2,nil)
      elseif type(columns) == "number" and columns == 2 then
        local t = {}
        for _, real_range in ipairs(ranges) do
          t = odsreader.join(t, odsreader.tableValues(body,real_range[1],1,real_range[3],2))
        end
      	columns = rowValues(t[1])
      end  
      if type(columns) == "table" then colheading = table.concat(columns," & ") .. odsnl .. " " end
      % coltypes = ""
      if type(content)== "table" then 
        % coltypes= string.rep("l",\#content[1]) 
        if not coltypes then
          coltypes = makeColtypes(headings)
        end
      end
      content = table.concat(content, odsnl .. " " ..rowseparator) .. odslastnl
      local result = odsreader.interp(templates[currenttemplate],{content=content,coltypes=coltypes,colheading=colheading,rowsep=rowseparator})
      if odsdebug then
        print(result)
      end
      if odssave then
        odsreader.save(odssave, result)
      else
        tex.sprint(result)
      end
    else
      local content = {}  
      currenttemplate = currenttemplate or "empty"
      for _,row in pairs(values) do
        table.insert(content,odsreader.interp(rowtemplate,rowValues(row)))
      end
      content = table.concat(content,rowseparator) 
      local result = odsreader.interp(templates[currenttemplate],{content=content,coltypes=coltypes,colheading=colheading,rowsep=rowseparator})
      if odsdebug then
        print(result)
      end
      if odssave then
        odsreader.save(odssave, result)
      else
        tex.sprint(result)
      end
    end
  }%
}%

\tabletemplate{empty}{-{content}}

\tabletemplate{default}{-{colheading}-{rowsep}-{content}}

\tabletemplate{booktabs}{%
\begin{tabular}{-{coltypes}}
\toprule
-{colheading}
\midrule
-{content}
\bottomrule
\end{tabular}
}

% Interface for adding of new rows

\newenvironment{AddRow}[1][]{%
\def\AddString##1##2{% 
\luaexec{%
local pos = "\luatexluaescapestring{##2}"%
if pos == "" then pos = nil end; row:addString("\luatexluaescapestring{\unexpanded{##1}}",nil,pos)%
}%
}%
\def\AddNumber##1##2{%
\luaexec{%
local pos = "\luatexluaescapestring{##2}"%
if pos == "" then pos = nil end; row:addFloat("\luatexluaescapestring{##1}",nil,pos)%
}%
}%
\luaexec{%
pos = "\luatexluaescapestring{#1}"%
if pos == "" then pos = nil end; row = odsreader.newRow()%
}%
}{%
\luaexec{%
body = body or odsreader.getTable(odsfile)
row:insert(body,pos)%
% we must save the updated table to the original table
odsfile.root["office:document-content"]["office:body"]["office:spreadsheet"]["table:table"] = body%
}%
}

% Interface for saving the spreadsheet

\newcommand\savespreadsheet{%
\luaexec{%
 local xml = require("luaxml-mod-xml")
 f = io.open("content.xml","w")%
 f:write(xml.serialize(odsfile.root))%
 f:close()%
 odsreader.updateZip(odsfilename,"content.xml")%
}%
}

% support for hyperlinks in cells

\newcommand\odslink[2]{\texttt{#2}}

\AtBeginDocument{%
	\@ifpackageloaded{hyperref}{%
		\renewcommand\odslink[2]{\href{#1}{#2}}%
	}{}
}
\endinput
