--% Kale Ewasiuk (kalekje@gmail.com)
--% 2025-02-14
--% Copyright (C) 2025 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.


local luatbls = {}

luatbls._luakeys = require'luakeys'()

luatbls._rec_tbl = ''
luatbls._rec_tbl_opts = {}

luatbls._xysep = '%s+' -- spaces separate x-y coordinates
luatbls._tblv = '<v>'
luatbls._tblk = '<k>'
luatbls._cstemp = 'ltbl<t><k>'
luatbls._debug = false

function luatbls._dprint(s1, s2)
    if luatbls._debug then
        penlight.wrth(s1, s1)
    end
end


setmetatable(luatbls, {__call=function(t,s)return t._get_tbl_item(s) end})

function luatbls._get_tbl_name(s)
    s = s:strip()
    if s == '' then
        return luatbls._rec_tbl
    end
    for _, delim in ipairs{'.', '/', '|'} do
        s = s:split(delim)[1] -- if tbl reference had a . | or /, an indexer was used
    end
    if luatbls[s] == nil then
        local validtbls = penlight.List(penlight.tablex.kkeys(luatbls)):filter(function(s) return not s:startswith('_')  end):join(', ')
        penlight.tex.pkgerror('luatbls', 'Tried to access undefined tbl: "'..s..'".   Valid tbls are: '..validtbls)
        return luatbls._rec_tbl
    end
    return s
end

function luatbls._get_tbl(s)
    s = luatbls._get_tbl_name(s)
    return luatbls[s]
end

function luatbls._get_tbl_index(s, undec)
    undec = undec or false -- flag for allowing undeclared indexing
    local tbl = ''
    local key = ''
    local s_raw = s
    if s:find('%.') then
        local tt = s:split('.')
        tbl = tt[1]
        key = tt[2]
    elseif s:find('/') then
        local tt = s:split('/')
        tbl = tt[1]
        if tbl == '' then tbl = luatbls._rec_tbl end
        key = tonumber(tonumber(tt[2]))
        if key < 0 then key = #luatbls[tbl]+1+key end
    else
        tbl = luatbls._rec_tbl
        key = tonumber(s) or s
        if type(key) == 'number' and key < 0 then key = #luatbls[tbl]+1+key end
    end
    if tbl == '' then tbl = luatbls._rec_tbl end

    if (luatbls[tbl] == nil) or ((not undec) and (luatbls[tbl][key] == nil)) then
        penlight.tex.pkgerror('luatbls',  'Invalid tbl index attempt using: "'..s_raw..'". We tried to use tbl="' ..tbl..'" and key="'..key..'"'..
                'Note that "|" is forbidden here. The recent table is:  '..luatbls._rec_tbl)
    end
    return tbl, key
end

function luatbls._get_tbl_seq(s)
    local tblseq = s:split('|')
    local tbl = nil
    local seq = nil
    if #tblseq == 1 then
        tbl = luatbls._get_tbl_name('')
        seq = tblseq[1]
        if seq == '' then seq = ':,*' end
    elseif #tblseq == 2 then
        tbl = luatbls._get_tbl_name(tblseq[1])
        seq = tblseq[2]
        if seq == '' then seq = ':,*' end
    end
    return tbl, seq
end



function luatbls._get_tbl_item(s, p) -- get item with string, p means print value
  p = p or false
  local tbl, key = luatbls._get_tbl_index(s)
  local itm = luatbls[tbl][key]
  if p then
    tex.sprint(tostring(itm))
  end
  return itm
end


function luatbls._set_tbl_item(s, v)
  tbl, key = luatbls._get_tbl_index(s)
  luatbls[tbl][key] = v
end

function luatbls._check_recent_tbl_undefault()
    local undefaults = {}
    if luatbls._rec_tbl_opts ~= nil then
        local defaults = penlight.tablex.union(
                luatbls._rec_tbl_opts.defs or {},
                luatbls._rec_tbl_opts.defaults or {}
        )
        for k, v in pairs(luatbls[luatbls._rec_tbl]) do
            if defaults[k] == nil then
                undefaults[#undefaults+1] = k
            end
        end
        if penlight.hasval(undefaults) then
            penlight.tex.pkgerror('luatbls',
                    'Invalid keys passed to tbl keyval:  ' .. (', '):join(undefaults) ..
                    ' ;   choices are:  ' .. (', '):join(penlight.tablex.keys(defaults))
            )
        end
    end
end

function luatbls._check_choices(k, csv)
 local csv = penlight.List(luatbls._luakeys.parse(csv,{naked_as_value=true}))
 local v = luatbls._get_tbl_item(k)
    if not csv:contains(v) then
        penlight.tex.pkgerror('luatbls', 'Invalid choice "'..v..'" given to tbl.key "'..k..'". Allowed choices are: '..
        (', '):join(csv))
    end
end


function luatbls._make_alpha_key(k)
     if tonumber(k) ~= nil then
         k = penlight.Char(tonumber(k))
     end
    return k
end

function luatbls._make_def_name(t, k, temp)
    if temp == penlight.tex.xNoValue then
        temp = luatbls._cstemp
    end
    k = luatbls._make_alpha_key(k)
    return temp:gsub('<t>',t):gsub('<k>',k)
end

function luatbls._def_tbl(ind, def, g)
  local _tbl, _key = luatbls._get_tbl_index(ind)
    def = luatbls._make_def_name(_tbl, _key, def)
    luatbls._def_tbl_one(luatbls[_tbl][_key], def, g)
end


function luatbls._def_tbl_some(Ind, def, g)
  for t, k, v in luatbls._iter_tbls_vals(Ind) do
        local newdef = luatbls._make_def_name(t, k, def)
         luatbls._def_tbl_one(v, newdef, g)
     end
end


function luatbls._def_tbl_one(v, cs, g)
    if type(v) == 'table' then
        for kk, vv in pairs(v) do
            token.set_macro(cs..luatbls._make_alpha_key(kk), tostring(vv), g)
        end
    else
        token.set_macro(cs, tostring(v), g)
    end
end

function luatbls._def_tbl_coords(ind, def)
    local tbl, key = luatbls._get_tbl_index(ind)
    local str = luatbls[tbl][key]
    def = luatbls._make_def_name(tbl, key, def)
    local x, y = str:strip():splitv(luatbls._xysep)
     if (not penlight.hasval(x)) or (not penlight.hasval(y))  then
       penlight.tex.pkgerror('luatbls', '_def_tbl_coords function could not parse coordiantes given as "'..str..'" ensure two numbers separated by space are given!', '', true)
     end
    token.set_macro(def..'x', tostring(x))
    token.set_macro(def..'y', tostring(y))
end



function luatbls._make_one_toggle(def, v, g)
    tex.sprint(g..'\\providetoggle{'..def..'}')
    tex.sprint(g..'\\toggle'..tostring(v)..'{'..def..'}')
end

function luatbls._make_toggle_tbl(ind, def, g)
    g = g or ''
    local t, k = luatbls._get_tbl_index(ind)
    local v = luatbls[t][k]
    def = luatbls._make_def_name(t, k, def)
    luatbls._make_one_toggle(def, penlight.hasval(v), g)
end

function luatbls._make_toggles_tbl(Ind, def, g)
    g = g or ''
    for t, k, v in luatbls._iter_tbls_vals(Ind) do
        if type(v) == 'boolean' then
            local newdef = luatbls._make_def_name(t, k, def)
            luatbls._make_one_toggle(newdef, v, g)
        end
    end
end


function luatbls._make_one_length(def, v, g)
    tex.sprint(g..'\\providenewlength{\\'..def..'}')
    tex.sprint(g..'\\deflength{\\'..def..'}{'..v..'}')
end

function luatbls._make_length_tbl(ind, def, g)
    g = g or ''
    local t, k = luatbls._get_tbl_index(ind)
    local v = luatbls[t][k]
    if type(v) == 'number' then
        v = tostring(v)..'sp'
    end
    local def = luatbls._make_def_name(t, k, def)
    luatbls._make_one_length(def, v, g)
end

function luatbls._make_lengths_tbl(Ind, def, g)
    g = g or ''
    for t, k, v in luatbls._iter_tbls_vals(Ind) do
        if type(v) == 'number' then
            v = tostring(v)..'sp'
        end
        if v:istexdim() then
            local newdef = luatbls._make_def_name(t, k, def)
            luatbls._make_one_length(newdef, v, g)
        end
    end
end



function luatbls._for_tbl_prt(k, v,cmd)
    local cmd_new = cmd:gsub(luatbls._tblv, tostring(v)):gsub(luatbls._tblk, tostring(k)):gsub('(\\%w+) ', '%1') -- for some reason a space gets added to \cs, maybe
    luatbls._dprint(cmd_new, '_for_tbl replacement')
    tex.sprint(cmd_new)
end

function luatbls._for_tbl(Ind, cmd)
  for t, k, v in luatbls._iter_tbls_vals(Ind) do
        luatbls._for_tbl_prt(k, v,cmd)
  end
end

function luatbls._for_tbl_e(tbl, cmd)
  for k, v in pairs(tbl) do
        luatbls._for_tbl_prt(k, v,cmd)
  end
end


function luatbls._iter_tbls_vals(s)
    if s:find('|') or ((s:find('%.') == nil) and (s:find('/') == nil))  then
        local tbl, seq = luatbls._get_tbl_seq(s)
        local keyval = {}
        for key, val in penlight.seq.tbltrain(luatbls._get_tbl(tbl), seq) do
         -- todo this should check validity of keys for sequences
            keyval[#keyval+1] = {key, val}
        end
        luatbls._dprint(keyval, 'luatbls._iter_tbls_vals is iterating through tbl: '..tbl..' with sequence:  '..seq)
        local i = 0
        return function()
            i = i + 1
            if i <= #keyval then
                return tbl, keyval[i][1], keyval[i][2]
            end
        end
    else
        local tbl, key = luatbls._get_tbl_index(s)
        local val = luatbls[tbl][key]
        luatbls._dprint(keyval, 'luatbls._iter_tbls_vals is using tbl: '..tbl..' with key: '..key)
        local i = 1
        return function()
            if i == 1 then
                i = i + 1 -- only return the single value
                return tbl, key, val
            end
        end
    end
end

function luatbls._make_args(s, key, val)
    local args = {val}
    if s == nil then
        return args
    end
    s = s:split(')')[1]
    args = luatbls._luakeys.parse(s, {naked_as_value=true})
    for i, v in ipairs(args) do
        if v == luatbls._tblv then
            args[i] = val
        elseif v == luatbls._tblk then
            args[i] = key
        end
    end
    return args
end

function luatbls._make_func(f, key, val)
    f = f:strip()
    fargs = f:split('(')
    f = fargs[1]
    args = luatbls._make_args(fargs[2], key, val)
    if f:startswith(':') then
        f = f:sub(2,-1)
        if type(val) == 'string' then
            f = string[f]
        elseif type(val) == 'table' then
            f = penlight.tablex[f]
        end
    else
        f = penlight._Gdot(f)
    end
    --luatbls._dprint() -- todo more debug printing
    return f, args
end



function luatbls._make_newtbl(tblind, newtbl)
    if tblind == '' then tblind = luatbls._rec_tbl..'|' end
    if newtbl ~= '' then -- determine if new tbl is needed
        local ogtbl = luatbls._get_tbl_name(tblind)
        luatbls[newtbl] = penlight.tablex.deepcopy(luatbls[ogtbl])
        luatbls._rec_tbl = newtbl
        tblind, _ = string.gsub(tblind, ogtbl, newtbl, 1)
    end
    return tblind
end

function luatbls._apply_tbl(tblind, func, newtbl)
    tblind = luatbls._make_newtbl(tblind, newtbl)
    for _, f in pairs(func:split('|')) do
        for tbl, key, val in luatbls._iter_tbls_vals(tblind) do
            local thefunc, args = luatbls._make_func(f, key, val)
            if thefunc == nil then
                penlight.tex.pkgerror('luatbls', 'Tried to apply function: "'..f..'" to tbl value. It yielded no function. Ensure : is used for self-methods')
            end
            luatbls[tbl][key] = thefunc(penlight.utils.unpack(args))
        end
    end
end

return luatbls