-- lib/supple/request.lua
--
-- Sandbox (for) Untrusted Procedure Partitioning (in) Lua Engine
--
-- Contextual object storage and identification.  Wrapping and unwrapping.
--
-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
--
-- For licence terms, see COPYING
--

local gc = collectgarbage

local capi = require 'supple.capi'
local track = require 'supple.track'

local my_objects_by_obj = {}
local my_objects_by_tag = {}
local my_objects_expn_by_tag = {}
local my_name = "UNSET"
local my_counter = 0
local their_objects_by_tag = setmetatable({}, { __mode="v" })
local their_objects_by_obj = setmetatable({}, { __mode="k" })
local their_objects_refcnt = {}
local proc_call = nil

local type = capi.rawtype

local function clean_down(call_other_end)
   track.enter("clean_down")
   -- And force a full GC
   gc "collect"
   gc "collect"
   gc "collect"
   -- Call the other end if needed
   if call_other_end then
      proc_call("supple:clean_down", "__call")
   end
   -- And forget all our local objects
   my_objects_by_obj = {}
   my_objects_by_tag = {}
   my_objects_expn_by_tag = {}
   track.leave("clean_down")
end

local function set_name(newname)
   my_name = newname
end

local function get_name()
   return my_name
end

local function set_proc_call(pc)
   proc_call = pc
end

local integral = {
   ["nil"] = true,
   boolean = true,
   string = true,
   number = true,
}

local function new_tag(special_tag)
   local retstr = special_tag
   if not retstr then
      if not my_name:match("%%d") then
	 retstr = ("%s:%d"):format(my_name, my_counter)
      else
	 retstr = my_name:format(my_counter)
      end
      my_counter = my_counter + 1
   end
   return retstr
end

local function give(obj, special_tag)
   -- If the object is integral, return it directly
   if integral[type(obj)] then
      return obj
   end
   -- If the passed object is one of their objects then "unwrap" it by
   -- returning their tag.
   track.enter("give", tostring(obj), tostring(special_tag))
   local tag = their_objects_by_obj[obj];
   if tag then
      track.leave("give", "theirs", tag)
      return { tag = tag }
   end
   -- If it's one of our objects which has already been wrapped, return our
   -- tag
   local tag = my_objects_by_obj[obj]
   if tag then
      track.leave("give", "ours", tag)
      return my_objects_expn_by_tag[tag]
   end
   -- otherwise wrap it freshly for us and return that.
   local tag = new_tag(special_tag)
   local expn = capi.explain(obj, tag)
   my_objects_by_obj[obj] = tag
   my_objects_by_tag[tag] = obj
   my_objects_expn_by_tag[tag] = expn
   track.leave("give", "ours, new", tag)
   return expn
end

local function receive(obj)
   -- If the object is integral, return it directly
   if integral[type(obj)] then
      return obj
   end
   -- It's not integral, so it must be a tagged object
   assert(type(obj) == "table")
   assert(type(obj.tag) == "string")
   local tag = obj.tag
   track.enter("receive", tag)
   -- First up, is it one of our objects?
   local ret = my_objects_by_tag[tag]
   if ret then
      track.leave("receive", "ours", tostring(ret))
      return ret
   end
   -- Now is it a known one of their objects?
   ret = their_objects_by_tag[tag]
   if ret then
      track.leave("receive", "theirs", tostring(ret))
      return ret
   end
   -- Okay, prepare a proxy?
   assert(type(obj.type) == "string", "Type string expected, got " .. type(obj.type) .. " preparing proxy for " .. obj.tag)
   local proxy, mt = capi.new_proxy(obj.type)
   assert(capi.type(proxy) == obj.type)
   their_objects_by_tag[tag] = proxy
   their_objects_by_obj[proxy] = tag
   their_objects_refcnt[tag] = (their_objects_refcnt[tag] or 0) + 1
   -- Fill out the metatable
   obj.methods = obj.methods or {}
   if obj.type == "function" then
      obj.methods[#obj.methods+1] = "__call"
   end
   if obj.type == "table" then
      obj.methods[#obj.methods+1] = "__len"
      obj.methods[#obj.methods+1] = "__index"
      obj.methods[#obj.methods+1] = "__newindex"
      obj.methods[#obj.methods+1] = "__next"
   end
   for _, name in ipairs(obj.methods or {}) do
      local function meta_func(mobj, ...)
	 track.enter("meta_func", their_objects_by_obj[mobj], tag, name)
	 local ret = {proc_call(tag, name, ...)}
	 track.leave("meta_func", their_objects_by_obj[mobj], tag, name)
	 return unpack(ret)
      end
      mt[name] = meta_func
   end
   function mt:__gc()
      their_objects_refcnt[tag] = their_objects_refcnt[tag] - 1
      if their_objects_refcnt[tag] == 0 then
	 their_objects_refcnt[tag] = nil
	 proc_call(tag, "__gc")
      end
   end
   -- And return the proxy object
   track.leave("receive", "theirs, new", tostring(proxy))
   return proxy
end

local function forget_mine(tag)
   track.enter("forget_mine", tag)
   local obj = my_objects_by_tag[tag]
   assert(obj, "Trying to forget nil object: " .. tag)
   my_objects_by_tag[tag] = nil
   my_objects_by_obj[obj] = nil
   my_objects_expn_by_tag[tag] = nil
   track.leave("forget_mine", tag)
end

local function find_tag(obj)
   if my_objects_by_obj[obj] then
      return "my", my_objects_by_obj[obj]
   end
   if their_objects_by_obj[obj] then
      return "their", their_objects_by_obj[obj]
   end
end

local function get_object_anchor()
   local ret = {}
   for obj in pairs(their_objects_by_obj) do
      ret[obj] = true
   end
   for obj in pairs(my_objects_by_obj) do
      ret[obj] = true
   end
   return ret
end

return {
   set_name = set_name,
   get_name = get_name,
   set_proc_call = set_proc_call,
   give = give,
   receive = receive,
   clean_down = clean_down,
   forget_mine = forget_mine,
   find_tag = find_tag,
   get_object_anchor = get_object_anchor,
}
