--------------------------------------------------------------------------------
--- Serialize arbitrary Lua data to Lua code
-- @module lua-nucleo.tserialize
-- This file is a part of lua-nucleo library
-- @copyright lua-nucleo authors (see file `COPYRIGHT` for the license)
--------------------------------------------------------------------------------

-- Serializes arbitrary lua tables to lua code
-- that can be loaded back via loadstring()
-- Functions, threads, userdata are not supported
-- Metatables are ignored
-- Usage:
--     str = tserialize(explist) --> to serialize data
--     =(loadstring(str)()) --> to load it back

local tserialize
do
  local pairs, type, tostring, select
      = pairs, type, tostring, select
  local table_concat, table_remove = table.concat, table.remove
  local string_format, string_match = string.format, string.match

  local lua_keywords = import 'lua-nucleo/language.lua' { 'lua_keywords' }
  local serialize_number = import 'lua-nucleo/string.lua' { 'serialize_number' }

  local cur_buf
  local cat = function(v)
    cur_buf[#cur_buf + 1] = v
  end

  local function general_iterator(tbl, i)
    i = i + 1
    local v = tbl[i]
    if v ~= nil then
      return i, v
    end
  end

  local function metatable_iterator(tbl, i)
    i = i + 1
    local v = rawget(tbl, i)
    if v ~= nil then
      return i, v
    end
  end

  -- Starting from Lua 5.3, size of the array tables with holes is different
  -- that in older versions and is really undetermined as the Lua manual
  -- always been said. This affects native `ipair` iterator and breaks
  -- serialization of some tables with holes in Lua >= 5.3. To resolve it,
  -- we introduce modified ipairs iterator that gets rid of that uncertainty.
  -- `ipairs_raw` iterates the table starting from the first element until
  -- the `nil` element is found. Access is made via `rawget` if the input table
  -- has metatable, and via table key accessor otherwise.
  local function ipairs_raw(list)
    if getmetatable(list) then
      return metatable_iterator, list, 0
    end

    return general_iterator, list, 0
  end

  local function explode_rec(t, add, vis, added)
    local t_type = type(t)
    if t_type == "table" then
      if not (added[t] or vis[t]) then
        vis[t] = true
        for k,v in pairs(t) do
          explode_rec(k, add, vis, added)
          explode_rec(v, add, vis, added)
        end
      else
        if not added[t] and vis[t] then
          add[#add+1] = t
          added[t] = { name = "_["..#add.."]", num = #add}
        end
      end
    end
  end

  local parse_rec
  do
    local started

    local function impl(t, added, rec_info)
      local t_type = type(t)
      local rec = false
      if t_type == "table" then
        if not added[t]or not started then
          started = true
          for k, v in pairs(t) do
            if impl(k, added, rec_info) or impl(v, added, rec_info) then
              rec = true
              if type(k) == "table" then
                rec_info[k] = true
              end
              if type(v) == "table" then
                rec_info[v] = true
              end
            end
          end
        else
          return true
        end
      end
      return rec
    end

    parse_rec = function(t, added, rec_info)
      started = false
      rec_info[t] = true
      impl(t, added, rec_info)
    end

  end
  local function recursive_proceed(t, added, rec_info, after, started)
    local t_type = type(t)
    if t_type == "table" then
      if not started or not added[t] then
	local started = true
        cat("{")
        -- Serialize numeric indices
        local next_i = 0
        for i, v in ipairs_raw(t) do
          next_i = i
          if not (rec_info[i] or rec_info[v]) then
            if i ~= 1 then cat(",") end
            recursive_proceed(v, added, rec_info, after, started)
          else
            next_i = i - 1
            break
          end
        end
        next_i = next_i + 1
        -- Serialize hash part
        -- Skipping comma only at first element if there is no numeric part.
        local comma = (next_i > 1) and "," or ""
        for k, v in pairs(t) do
          local k_type = type(k)
          if not (rec_info[k] or rec_info[v]) then
          --that means, if the value does not contain a recursive link
          -- to the table itself
          --and the index does not contain a recursive link...
            if k_type == "string"  then
              cat(comma)
              comma = ","
              --check if we can use the short notation
              -- eg {a=3,b=5} istead of {["a"]=3,["b"]=5}
              if
                not lua_keywords[k] and string_match(k, "^[%a_][%a%d_]*$")
              then
                cat(k); cat("=")
              else
                cat(string_format("[%q]=", k))
              end
                recursive_proceed(v, added, rec_info, after, started)
            elseif
              k_type ~= "number" or -- non-string non-number
              k >= next_i or k < 1 or -- integer key in hash part of the table
              k % 1 ~= 0 -- non-integral key.
            then
              cat(comma)
              comma = ","
              cat("[")
              recursive_proceed(k, added, rec_info, after, started)
              cat("]")
              cat("=")
              recursive_proceed(v, added, rec_info, after, started)
            end
          else
            after[#after + 1] = {k,v}
          end
        end
        cat("}")
      else -- already visited!
        cat(added[t].name)
      end
    elseif t_type == "string" then
      cat(string_format("%q", t))
    elseif t_type == "number" then
      cat(serialize_number(t))
    elseif t_type == "boolean" then
      cat(tostring(t))
    elseif t == nil then
      cat("nil")
    else
      return nil
    end
    return true
  end

  local function recursive_proceed_simple(t, added)
    local t_type = type(t)
    if t_type == "table" then
      if not added[t] then
        cat("{")
        -- Serialize numeric indices
        local next_i = 0
        for i, v in ipairs_raw(t) do
          next_i = i
          if i ~= 1 then cat(",") end
          recursive_proceed_simple(v, added)
        end
        next_i = next_i + 1
        -- Serialize hash part
        -- Skipping comma only at first element if there is no numeric part.
        local comma = (next_i > 1) and "," or ""
        for k, v in pairs(t) do
          local k_type = type(k)
          if k_type == "string"  then
            cat(comma)
            comma = ","
            --check if we can use the short notation
            -- eg {a=3,b=5} istead of {["a"]=3,["b"]=5}
            if
              not lua_keywords[k] and string_match(k, "^[%a_][%a%d_]*$")
            then
              cat(k); cat("=")
            else
              cat(string_format("[%q]=", k))
            end
              recursive_proceed_simple(v, added)
          elseif
            k_type ~= "number" or -- non-string non-number
            k >= next_i or k < 1 or -- integer key in hash part of the table
            k % 1 ~= 0 -- non-integral key.
          then
            cat(comma)
            comma = ","
            cat("[")
            recursive_proceed_simple(k, added)
            cat("]")
            cat("=")
            recursive_proceed_simple(v, added)
          end
        end
        cat("}")
      else -- already visited!
        cat(added[t].name)
      end
    elseif t_type == "string" then
      cat(string_format("%q", t))
    elseif t_type == "number" then
      cat(serialize_number(t))
    elseif t_type == "boolean" then
      cat(tostring(t))
    elseif t == nil then
      cat("nil")
    else
      return nil
    end
    return true
  end



  local afterwork = function(k, v, buf, name, added)
    cur_buf = buf
    cat(" ")
    cat(name)
    cat("[")
    local after = buf.afterwork
    if not recursive_proceed_simple(k, added) then
      return false
    end
    cat("]=")
    if not recursive_proceed_simple(v, added) then
      return false
    end
    cat(" ")
    return true
  end

  tserialize = function (...)
  --===================================--
  --===========THE MAIN PART===========--
  --===================================--
    --PREPARATORY WORK: LOCATE THE RECURSIVE AND SHARED PARTS--
    local narg = select("#", ...)
    local visited = {}
    -- table, containing recursive parts of our variables
    local additional_vars = { }
    local added = {}
    for i = 1, narg do
      local v = select(i, ...)
      explode_rec(v, additional_vars, visited, added) -- discover recursive subtables
    end
    visited = nil -- no more needed
    local nadd = #additional_vars
    --SERIALIZE ADDITIONAL FIRST--
    local rec_info = {}

    for i = 1, nadd do
      local v = additional_vars[i]
      parse_rec(v, added, rec_info)
    end
    local buf = {}
    for i = 1, nadd do
      local v = additional_vars[i]
      buf[i] = {afterwork = {}}
      local after = buf[i].afterwork
      cur_buf = buf[i]
      if not recursive_proceed(v, added, rec_info, after) then
        return nil, "Unserializable data in parameter #" .. i
      end
    end

    rec_info = {}
    for i = 1, nadd do
      local v = additional_vars[i]
      buf[i].afterstart = #buf[i]
      for j = 1, #(buf[i].afterwork) do
        if not afterwork(
              buf[i].afterwork[j][1],
              buf[i].afterwork[j][2],
              buf[i],
              added[v].name,
	      added
            )
        then
          return nil, "Unserializable data in parameter #" .. i
        end
      end
    end

    --SERIALIZE GIVEN VARS--

    for i = 1, narg do
      local v = select(i, ...)
      buf[i + nadd] = {}
      cur_buf = buf[i + nadd]
      if not recursive_proceed_simple(v, added) then
        return nil, "Unserializable data in parameter #" .. i
      end
    end

    --DECLARE ADDITIONAL VARS--

    local prevbuf = {}
    for v, inf in pairs(added) do
      prevbuf[ #prevbuf + 1] =
	"[" .. inf.num .. "]=" .. table_concat(buf[inf.num], "", 1, buf[inf.num].afterstart)..";"
    end

    --CONCAT PARTS--
    for i = 1, nadd do
      buf[i] = table_concat(buf[i], "", buf[i].afterstart + 1)
    end
    for i = nadd + 1, nadd+narg do
      buf[i] = table_concat(buf[i])
    end

    --RETURN THE RESULT--

    if  nadd == 0 then
      return "return " .. table_concat(buf,",")
    else
      local rez = {
        "do local _={";
        table_concat(prevbuf, " ");
        '}';
        table_concat(buf, " ", 1, nadd);
        " return ";
        table_concat(buf, ",", nadd+1);
        " end";
      }
      return table_concat(rez)
    end
  end
end

return
{
  tserialize = tserialize;
}
