--% Kale Ewasiuk (kalekje@gmail.com)
--% 2022-11-06
--% Copyright (C) 2021-2022 Kale Ewasiuk
--%
--% Permission is hereby granted, free of charge, to any person obtaining a copy
--% of this software and associated documentation files (the "Software"), to deal
--% in the Software without restriction, including without limitation the rights
--% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
--% copies of the Software, and to permit persons to whom the Software is
--% furnished to do so, subject to the following conditions:
--%
--% The above copyright notice and this permission notice shall be included in
--% all copies or substantial portions of the Software.
--%
--% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
--% ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
--% TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
--% PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT
--% SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
--% ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
--% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
--% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
--% OR OTHER DEALINGS IN THE SOFTWARE.


-- tinyyaml license
--MIT License
--
--Copyright (c) 2017 peposso
--
--Permission is hereby granted, free of charge, to any person obtaining a copy
--of this software and associated documentation files (the "Software"), to deal
--in the Software without restriction, including without limitation the rights
--to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
--copies of the Software, and to permit persons to whom the Software is
--furnished to do so, subject to the following conditions:
--
--The above copyright notice and this permission notice shall be included in all
--copies or substantial portions of the Software.
--
--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
--OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
--SOFTWARE.

YAMLvars = {} -- self table

YAMLvars.yaml = require'markdown-tinyyaml' -- note: YAMLvars.sty will have checked existence of this already

local pl = _G['penlight'] or _G['pl'] -- penlight for this namespace is pl
if (__PL_EXTRAS__ == nil) or  (__PENLIGHT__ == nil) then
    tex.sprint('\\PackageError{yamlvars}{penlight package with extras (or extrasnoglobals) option must be loaded before this package}{}')
end

YAMLvars.xfm = {}
YAMLvars.prc = {}
YAMLvars.dec = {} -- table of declare function

YAMLvars.varsvals = {}
YAMLvars.varspecs = {}
YAMLvars.varslowcase = pl.List()

YAMLvars.xfmDefault = {}
YAMLvars.prcDefault = 'gdef'
YAMLvars.dftDefault = nil

YAMLvars.allowUndeclared = false
YAMLvars.overwritedefs = false
YAMLvars.lowvasevarall = false

YAMLvars.valTemp = ''
YAMLvars.varTemp = ''

YAMLvars.tabmidrule = 'hline'

YAMLvars.debug = false




function YAMLvars.debugtalk(s, ss)
    if YAMLvars.debug then
        pl.tex.help_wrt(s, ss)
    end
end

function YAMLvars.pkgerr(m)
    pl.tex.pkgerror('yamlvars', m, '', true)
end




-- todo need distinction beyyween table and penlight list ???
    --val = pl.array2d.map_slice1(_1..'\\\\', val, 1,-2)
    --return val:join('')
    --return pl.tablex.reduce(_1.._2, val, '')

function YAMLvars.xfm.markdown(var, val)
     --return '\\begin{markdown} '..val..'\n \\end{markdown}'
     pl.tex.help_wrt(val, md)
     return [[begin markdown ..val..

     par end markdown]]
end




-- xfm functions (transforms) -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
function YAMLvars.xfm.addxspace(var, val)
    return val .. '\\xspace'
end

function YAMLvars.xfm.tab2arr(var, val)
     return pl.array2d.from_table(val)
end

function YAMLvars.xfm.arrsort2ZA(var, val)
    return pl.array2d.sortOP(val, pl.operator.strgt)
end

function YAMLvars.xfm.addrule2arr(var, val)
     return pl.array2d.map_slice2(_1..'\\\\\\'.. YAMLvars.tabmidrule..' ', val, 1,-1,-2,-1)
end

function YAMLvars.xfm.arr2tabular(var, val)
     return pl.array2d.toTeX(val)..'\\\\'
end

function YAMLvars.xfm.list2items(var, val)
     return pl.List(val):map('\\item '.._1):join(' ')
end
YAMLvars.xfm.arr2itemize = YAMLvars.xfm.list2items

function YAMLvars.xfm.arrsortAZ(var, val)
     return pl.List(val):sort(pl.operator.strlt)
end

function YAMLvars.xfm.arrsortZA(var, val)
     return pl.List(val):sort(pl.operator.strgt)
end

local function complastname(a, b)
    a = a:split(' ')
    b = b:split(' ')
    a = a[#a]
    b = b[#b]
    return a < b
end

function YAMLvars.xfm.arrsortlastnameAZ(var, val)
    val = pl.List(val):sort(complastname)
    return val
end

function YAMLvars.xfm.list2nl(var, val)
    if type(val) == 'string' then
        return val
    end
    return pl.List(val):join('\\\\ ')
end

function YAMLvars.xfm.list2and(var, val) -- for doc vars like author, publisher
    if type(val) == 'string' then
        return val
    end
    return pl.List(val):join('\\and ')
end


function YAMLvars.xfm.lb2nl(var, val) --linebreak in text 2 newline \\
    val, _ = val:gsub('\n','\\\\ ')
    return val
end

function YAMLvars.xfm.lb2newline(var, val) --linebreak in text 2 newline \\
    val, _ = val:gsub('\n','\\newline ')
    return val
end

function YAMLvars.xfm.lb2par(var, val) --linebreak in text 2 new l
    val, _ = val:gsub('\n%s*\n','\\par ')
    return val
end

function YAMLvars.xfm.lowercase(var, val)
    return val:lower()
end


-- dec laration functions, -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

function YAMLvars.dec.gdef(var, dft)
            YAMLvars.deccmd(var, dft)
end

function YAMLvars.dec.yvdef(var, dft)
        YAMLvars.deccmd('yv--'..var, dft)
end

function YAMLvars.dec.toggle(var, dft)
        tex.print('\\global\\newtoggle{'..var..'}')
        YAMLvars.prc.toggle(var, dft)
end

function YAMLvars.dec.length(var, dft)
        tex.print('\\global\\newlength{\\'..var..'}')
        YAMLvars.prc.length(var, dft)
end



-- prc functions (processing) -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

function YAMLvars.prc.gdef(var, val)
    --token.set_macro(var, val, 'global') -- old way, don't do as it will cause issues if val contains undef'd macros
    pl.tex.defcmd(var, val)
    YAMLvars.debugtalk(var..' = '..val, 'prc gdef')
end

function YAMLvars.prc.yvdef(var, val)
    pl.tex.defmacro('yv--'..var, val)
    YAMLvars.debugtalk('yv--'..var..' = '..val, 'prc yvdef')
end

function YAMLvars.prc.toggle(t, v) -- requires penlight extras
    local s = ''
    if pl.hasval(v) then
        s = '\\global\\toggletrue{'..t..'}'
    else
        s = '\\global\\togglefalse{'..t..'}'
    end
    tex.print(s)
    YAMLvars.debugtalk(s, 'prc toggle')
end

function YAMLvars.prc.length(t, v)
    v = v or '0pt'
    local s = '\\global\\setlength{\\global\\'..t..'}{'..v..'}'
    tex.print(s)
    YAMLvars.debugtalk(s, 'prc length')
end



function YAMLvars.prc.setATvar(var, val) -- set a @var directly: eg \gdef\@title{val}
    pl.tex.defcmdAT('@'..var, val)
end


function YAMLvars.prc.setdocvar(var, val) -- call a document var \var{val} = \title{val}
    -- YAML syntax options
    -- k: v -> \k{v}
    -- k:
    --   v1: v2      -> \k[v2]{v1}
    -- k: [v1, v2]   -> \k[v2]{v1}
    -- k: [v1]       -> \k{v1}
    if type(val) ~= 'table' then
        tex.sprint('\\'..var..'{'..val..'}')
    elseif #val == 0 then  -- assume single k,v passed
        for k,v in pairs(val) do
            tex.sprint('\\'..var..'['..v..']{'..k..'}')
        end
    elseif #val == 1 then
        tex.sprint('\\'..var..'{'..val[1]..'}')
    else
        tex.sprint('\\'..var..'['..val[2]..']{'..val[1]..'}')
    end
end


function YAMLvars.prc.setPDFdata(var, val)
    --update pdf meta data table (via penlight), uses pdfx xmpdata
    -- requires a table input
    for k, v in pairs(val) do
        if type(v) == 'table' then
            v = pl.List(v):join('\\sep ')
        end
        pl.tex.updatePDFtable(k, v, true)
    end
end

-- with hyperref package
function YAMLvars.prc.PDFtitle(var, val)
    tex.print('\\hypersetup{pdftitle={'..val..'}}')
end

function YAMLvars.prc.PDFauthor(var, val)
    tex.print('\\hypersetup{pdfauthor={'..val..'}}')
end

-- --


-- ??
--token.set_macro('yv--'..var, val, 'global') -- todo fix with csname hack?



-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

function YAMLvars.makecmd(cs, val) -- provide command via lua
   if token.is_defined(cs) and (not YAMLvars.overwritedefs) then
        YAMLvars.pkgerr('Variable '..cs..' already defined, could not declare')
    else
        pl.tex.defcmd(cs, val)
        --token.set_macro(cs, val, 'global') -- issues if val contains undefined macro
    end
end

function YAMLvars.deccmd(cs, def)
    if def == nil then
        YAMLvars.makecmd(cs, '\\PackageError{YAMLvars}{Variable "'..cs..'" was declared and used but, not set}{}')
    else
        YAMLvars.makecmd(cs, def)
    end
end

-- -- -- -- -- --

local function getYAMLfile(y)
    local f = io.open(y,"r")
    if f ~= nil then
        y = f:read('*a')
        io.close(f)
        return y
    else
        YAMLvars.pkgerr('YAML file "'..y..'" not found')
        return 'YAMLvars: FileNotFound'
    end
end


function YAMLvars.declareYAMLvarsStr(y)
    local t = YAMLvars.yaml.parse(y)
    for var, specs in pairs(t) do
        if pl.hasval(specs['lowcasevar']) or YAMLvars.lowvasevarall then
            var = var:lower()
            YAMLvars.varslowcase:append(var)
        end
        YAMLvars.varspecs[var] = {xfm=YAMLvars.xfmDefault,prc=YAMLvars.prcDefault,dft=YAMLvars.dftDefault}
        if type(specs) == 'string' then
            specs = {xfm={specs}}
        end
        if specs['xfm'] == nil then specs['xfm'] = {} end
        for s, p in pairs(specs) do
            if s == 'xfm' and type(p) ~= 'table' then p = {p} end
            YAMLvars.varspecs[var][s] = p -- set property of var
        end
        if YAMLvars.dec[YAMLvars.varspecs[var].prc] ~= nil then
            YAMLvars.dec[YAMLvars.varspecs[var].prc](var, YAMLvars.varspecs[var].dft)
        --else -- actually don't a dec function for all
        --    -- -- -- tex.print('\\PackageError{YAMLvars}{Declaration function for '..YAMLvarspecs[var].prc..'not found}{}')
        end
    end
    YAMLvars.debugtalk(YAMLvars.varspecs, 'declared YAML vars, varspecs')
end



function YAMLvars.declareYAMLvarsFile(y)
    YAMLvars.declareYAMLvarsStr(getYAMLfile(y))
end


local  function check_def(var, val)
    if YAMLvars.allowUndeclared then
        if YAMLvars.prcDefault == 'yvdef' then
            YAMLvars.prc.yvdef(var, val)
            YAMLvars.debugtalk(var..' = '..val,'yvdef made (undeclared)')
        else
            YAMLvars.makecmd(var, val)
            YAMLvars.debugtalk(var..' = '..val,'gdef made (undeclared)')
        end
     else
        --tex.print('\\PackageError{YAMLvars}{Variable "'..var..'" set but not declared}{}')
        YAMLvars.pkgerr('Variable "'..var..'" set but not declared')
    end
end

local function sub_lua_var(s, v1, v2)
    return s:gsub('([%A?%-?])('..v1..')([%W?%-?])', '%1'..v2..'%3') -- replace x variables
end

local _YV_invalid_expression = '\1 invalid expression'
local _YV_no_return = '\2 no return val'

local function expr_err(var, val)
    --tex.print('\\PackageError{YAMLvars}{xfm with "= or /" error on var "'..var..'"}{}') -- todo make program stop
    YAMLvars.pkgerr('xfm with "= or /" error on var "'..var..'"}{}') -- todo make program stop
end

local function eval_expr(func, var, val)
    local s, c = func:gsub('^[=/]', {['/'] = '\2', ['='] = 'YAMLvars.valTemp = '}, 1) -- / is run code, = sets val = code
    if c == 0 then
        return _YV_invalid_expression
    else
        --pl.tex.help_wrt(s, var)
        --pl.tex.help_wrt(val, var)
        YAMLvars.valTemp = val
        YAMLvars.varTemp = var
        --pl.tex.help_wrt(s, var)
        s, c = s:gsub('\2', '') -- strip \2 that might have appeared if / was applied
        s = sub_lua_var(' '..s, 'x', 'YAMLvars.valTemp')
        s = sub_lua_var(s, 'v', 'YAMLvars.varTemp')
        --pl.tex.help_wrt(s, var)
        local f, err = pcall(loadstring(s))
        if not f then
            --tex.print('\\PackageError{YAMLvars}{xfm with "= or /" error on var "'..var..'"}{}') --
            YAMLvars.pkgerr('xfm with "= or /" error on var "'..var) --
        end
        if c > 0 then
            expr_err(var)
            return _YV_no_return
        end
        return YAMLvars.valTemp
    end
end

local function transform_and_prc(var, val)
    for _, func in pairs(YAMLvars.varspecs[var]['xfm']) do --apply cleaning functions
        local f = YAMLvars.xfm[func]
        if f == nil then
            local val2 =  eval_expr(func, var, val)
            if val2 == _YV_invalid_expression then
                --tex.print('\\PackageError{YAMLvars}{xfm function "'..func..'" not defined or invalid expression passed on var "'..var..'"}{}')
                YAMLvars.pkgerr('xfm function "'..func..'" not defined or invalid expression passed on var "'..var)
            elseif val == _YV_no_return then
            else
                val = val2
            end
        else
            val = f(var, val)
        end
    end
    f = YAMLvars.prc[YAMLvars.varspecs[var]['prc']]
    if f == nil then
        --tex.print('\\PackageError{YAMLvars}{prc function "'..YAMLvars.varspecs[var]['prc']..'" on var "'..var..'" not defined}{}')
        YAMLvars.pkgerr('prc function "'..YAMLvars.varspecs[var]['prc']..'" on var "'..var..'" not defined')
    end
    f(var, val) -- prc the value of the variable
end

function YAMLvars.parseYAMLvarsStr(y)
    YAMLvars.varsvals = YAMLvars.yaml.parse(y)
    for var, val in pairs(YAMLvars.varsvals) do
        if YAMLvars.varslowcase:contains(var:lower()) then
            var = var:lower()
        end
        if YAMLvars.varspecs[var] == nil then
            check_def(var, val) -- if not declared
            -- todo consider free form parse declaring
            -- variable name: {xfm:, dec:, prc:, val: }
            -- definitely doable here
        else
            transform_and_prc(var, val)
        end
    end
end

function YAMLvars.parseYAMLvarsFile(y)
    YAMLvars.parseYAMLvarsStr(getYAMLfile(y))
end



function YAMLvars.print_varspecs()
    local pretty = require('pl.pretty')
    texio.write_nl('VVVVVV Var specifications:')
    texio.write_nl(pretty.write(YAMLvars.varspecs))
end



-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --




function YAMLvars.doYAMLfiles(t)
    if #t == 2 then
        YAMLvars.declareYAMLvarsFile(t[1])
        YAMLvars.parseYAMLvarsFile(t[2])
    elseif #t == 1 then
        YAMLvars.parseYAMLvarsFile(t[1])
    else
        tex.print('\\PackageWarning{YAMLvars}{No .yaml files found in CLI args"}{}')
    end
end

function YAMLvars.getYAMLcli()
    local t = {}
    if arg then
      for i,v in pairs(arg) do
          if v:find('.*%.yaml$') then
              t[#t+1] = v
          end
      end
    end
    --pl.tex.help_wrt(t)
    return t
 end



YAMLvars.yaml2PDFmetadata = function(ytext) -- parse a YAML file and update the pdfmetadata table
      __PDFmetadata__ = __PDFmetadata__ or {} -- existing metadata
      if ytext ~= nil then
        local pdfmetadata_yaml = YAMLvars.yaml.parse(ytext) -- new metadata
        local t = {}
        for k,v in pairs(pdfmetadata_yaml) do  -- ensure first character is capital letter
            t[k:upfirst()] = v
        end
        __PDFmetadata__ = table.update(__PDFmetadata__, t)
      end
    end







-- graveyard


function YAMLvars.prc.setheader(val, rl)
    local _, count = string.gsub(val, '\\\\', '')
    if count == 0 then
        val = '{\\ }\\\\'..val
    end
    val = '\\setstretch{0.8}'..val
    tex.print('\\'..rl..'ohead{'..val..'}')
end


function YAMLvars.prc.setheader(val, rl)
    local _, count = string.gsub(val, '\\\\', '')
    if count == 0 then
        val = '{\\ }\\\\'..val
    end
    val = '\\setstretch{0.8}'..val
    tex.print('\\'..rl..'ohead{'..val..'}')
end

function YAMLvars.prc.rhead(var, val)
    YAMLvars.prc.setheader(val, 'r')
end

function YAMLvars.prc.lhead(var, val)
    YAMLvars.prc.setheader(val, 'l')
end



function YAMLvars.prc.memoTo(var, val)
    for k, v in pairs(v) do
        pl.tex.defcmd('@memoTo', val)
        v = YAMLvars.xfm.list2nl(var, v)
        pl.tex.defcmd('@memoTo', val)
    end
end
function YAMLvars.prc.memoFr(var, val)
    for k, v in pairs(v) do
        pl.tex.defcmd('@memoFr', val)
        v = YAMLvars.xfm.list2nl(var, v)
        pl.tex.defcmd('@memoFrAddr', val)

    end
end
-- produce \Var[val[2]\\val[3].....\val[n]]{val[1]}
--function YAMLvars.prc.setdocvarOpt(var, val)
--       if type(val) ~= 'table' then
--            val = {val}
--        end
--        local s = '\\'..var..'{'..tostring(val[1])..'}'
--        s = s..'['..pl.List(v):map_slice1():join()..']' -- what does this do?
--        tex.print(s)
--end
--
--
--function YAMLvars.prc.setdocvarOpts(var, val)
--        if type(val) ~= 'table' then
--            val = {val}
--        end
--        local s = '\\'..var..'{'..tostring(val[1])..'}'
--        for k, v in pairs(val) do
--            if k > 1 then
--                s = s..'['..tostring(v)..']'
--            end
--        end
--        tex.print(s)
--end

return YAMLvars









     --token.set_macro('@memoFr', k, 'global')
        --token.set_macro('@memoFrAddr', v, 'global')
        --token.set_macro('@memoTo', k, 'global')
        --token.set_macro('@memoToAddr', v, 'global')
        --help_wrt(var,val)
        --        token.set_macro('@'..var, val, 'global')

--function YAMLvars.prc.title(var, val)
--        YAMLvars.prc.setdocvar('title', val)
--end
--
--
--function YAMLvars.prc.author(var, val)
--        YAMLvars.prc.setdocvar('author', val)
--end
--
--function YAMLvars.prc.date(var, val)
--        YAMLvars.prc.setdocvar('date', val)
--end


  --clean = clean or true
    --if clean then -- clean first part of yaml string
    --    y = clean_tex_spaces(y)
    --end
--local function clean_tex_spaces(s)
--    pl.tex.help_wrt(s)
--    if s:sub(1,2) == '%s' then
--        s, _ = s:gsub('%s+','',1)
--    end
--    s, _ = s:gsub('\\par ','\n\n')
--    return s
--end

