-- Package for standard handling of named arguments
-- (c) 2006 Markus Nentwig
-- This file is in the public domain (may be used freely)
-- No warranty is given or implied.
local mnoo=require("mnoo_current/mnoo.lua")

-- Report the error at the caller's level
-- because the function was misused
local codeerror=2

-- Report the error at the level of the caller's caller.
-- Reason: The subroutine that used the get() function got invalid data
local usererror=3

local cl=mnoo.newclass()
cl:declareObjectmethod("get", "all_done")
cl:declareObjectdata("contents", "done")

local eheader="mnoo/args_cl:"
local function ERROR_missing_argument(sym)
   return eheader.."argument '"..sym.."' not found"
end

local function ERROR_unprocessed_arguments(sym_ilist)
   return eheader.."unprocessed arguments: "..table.concat(sym_ilist, ", ")
end

local function ERROR_object_expected(sym)
   return eheader.."argument '"..sym.."': Object expected"
end

local function ERROR_class_expected(sym)
   return eheader.."class arg for '"..sym.."': Class expected"
end

local function ERROR_not_of_class(sym)
   return eheader.."argument '"..sym.."' is not of the requested class!"
end

local function ERROR_cannot_convert(sym, rtype)
   return "cannot convert argument "..sym.." to desired type "..(tostring(rtype) or "???")
end

local function ERROR_arg_handled_twice(sym)
   return "argument handled twice: '"..sym.."'"
end

local superclass_new=cl:getClassfunction("new")
cl.new=
   function(c, t)
      
      if type(t) ~= "table" then error("Expecting table with arguments. Use obj{...} calling convention!", usererror) end

      -- got already an args_cl object? Then simply return it, 
      -- This is used for call chains, where every level strips off the args it needs.
      if mnoo.isObject(t) then 
	 assert(t:objectIsA(cl))
	 return t 
      end

      local self=superclass_new(c)
      self.contents=t
      self.done={}
      return self
   end

cl.get=
   function(self, t)
      if not mnoo.isObject(self) or not self:objectIsA(cl) then error("argument must be argument class object!", codeerror) end
      local name=tostring(t.name) if not name then error("name argument missing / invalid", codeerror) end
      if t.class then
	 if t.type then
	    error("cannot give class and type at the same time!", codeerror)
	 end
	 t.type="object" 
	 if not mnoo.isClass(t.class) then error(ERROR_class_expected(name), codeerror) end
      end
      
      local rtype=tostring(t.type) if not rtype then error("type argument missing / invalid", codeerror) end
      local default=t.default
      local nilOK=t.nilOK

      if self.done[name] then
	 error(ERROR_arg_handled_twice(name), codeerror)
      end

      -- mark as handled
      self.done[name]=true
      
      local res=self.contents[name]
      if res==nil then
	 if default ~= nil then 
	    -- omit type check
	    return default
	 end
      end
      
      if res==nil and nilOK then
	 -- omit type check
	 return nil
      end

      if res==nil then
	 error(ERROR_missing_argument(name), usererror)
      end

      if rtype == "number" then
	 res=tonumber(res)
      elseif rtype == "function" then
	 if type(res) ~= "function" then error("function argument required", usererror) end
      elseif rtype == "string" then
	 res=tostring(res)
      elseif rtype == "table" then
	 if type(res) ~= "table" then res=nil end
      elseif rtype == "boolean" then
	 if type(res) ~= "boolean" then res=nil end
      elseif rtype == "object" then
	 if not mnoo.isObject(res) then
	    error(ERROR_object_expected(name), usererror) 
	 end
	 if t.class then
	    if not res:objectIsA(t.class) then	       
	       error(ERROR_not_of_class(name), usererror) 
	    end	   
	 end
      elseif rtype == "any" then
      else
	 error("unknown type "..(tostring(rtype) or "???"), codeerror)
      end
      
      if res==nil then error(ERROR_cannot_convert(name, tostring(rtype) or "???"), usererror) end
      return res
   end

cl.all_done=
   function(self, t)
      local remaining
      for key, val in pairs(self.contents) do
	 if not self.done[key] then
	    if not remaining then remaining={} end
	    table.insert(remaining, key)
	 end
      end
      if remaining then
	 return false, ERROR_unprocessed_arguments(remaining)
      else
	 return true
      end
   end


-- ########################################################################################
-- # Built-in self test (optional)
-- ########################################################################################
if _SELFTEST then
   local args_cl=cl
   local test=
      function(testfun, check)
	 local flag, result=pcall(testfun)
	 result=result or "OK"
	 assert(string.find(result, check, 0, true), "***"..result.."*** instead of ***"..check.."***")
      end

   -- ########################################################################################
   -- # Normal operation and standard errors
   -- ########################################################################################
   for testcase, result in pairs({
				    tc1="OK", 
				    tc2=ERROR_missing_argument("one"),
				    tc3=ERROR_unprocessed_arguments({"extra1", "extra2"}),
				    tc4=ERROR_object_expected("obj"),
				    tc5=ERROR_not_of_class("obj"),
				    tc6=ERROR_class_expected("obj"),
				    tc7=ERROR_cannot_convert("one", "number"),
				    tc8=ERROR_arg_handled_twice("one")
				 }) do
      test(
	   function()
	      local c=mnoo.newclass()
	      local o=c:new()
	      
	      local c2=mnoo.newclass()
	      local o2=c2:new()
	      
	      local testclass=c
	      local t={one="1", two=2, obj=o, obj2=o2}
	      if testcase=="tc2" then
		 t.one=nil
	      elseif testcase=="tc3" then
		 t.extra1="x"
		 t.extra2="y"
	      elseif testcase=="tc4" then
		 t.obj=1
	      elseif testcase=="tc5" then
		 t.obj=o2
	      elseif testcase=="tc6" then
		 testclass=1
	      elseif testcase=="tc7" then
		 t.one="abc"
	      end

	      local a=args_cl:new(t)
	      
	      local r1=a:get{name="one", type="number"}
	      assert(type(r1)=="number")
	      assert(tostring(r1)=="1")
	      
	      assert(a:get{name="two", type="string"}=="2")
	      assert(a:get{name="obj", class=testclass}==o)
	      assert(a:get{name="obj2", type="object"}==o2) -- no class check		  
	      
	      if testcase=="tc8" then
		 a:get{name="one", type="number"}
	      end

	      assert(a:all_done())
	   end, 
	   result)
   end -- for testcase
   
   -- ########################################################################################
   -- # Constructor chain
   -- ########################################################################################
   test(
	function()
	   local c=mnoo.newclass()
	   
	   local super_new=c:getClassfunction("new")
	   c.new=
	      function(actual_class, t)
		 t=args_cl:new(t)
		 t:get{name="one", type="string"}
		 assert(t:all_done())

		 local self=super_new(actual_class) -- base constructor without args		 
		 return self
	      end
	   
	   -- derive class and overload new
	   c=mnoo.newclass(c)
	   local super_new=c:getClassfunction("new")
	   c.new=
	      function(actual_class, t)
		 t=args_cl:new(t) -- here t is already args_cl, although we wouldn't know it
		 t:get{name="two", type="string"}

		 local self=super_new(actual_class, t)
		 -- we are aware, that we are passing our args_cl argument to the parent class
		 -- constructor. Therefore check all_done() only after the call.
		 assert(t:all_done())

		 return self
	      end

	   -- derive class and overload new
	   c=mnoo.newclass(c)
	   local super_new=c:getClassfunction("new")
	   c.new=
	      function(actual_class, t)
		 t=args_cl:new(t) -- here t is already args_cl, although we wouldn't know it
		 t:get{name="three", type="string"}

		 local self=super_new(actual_class, t)
		 -- we are aware, that we are passing our args_cl argument to the parent class
		 -- constructor. Therefore check all_done() only after the call.
		 assert(t:all_done())

		 return self
	      end

	   local o=c:new{one=1, two=2, three=3}
	   
	end,
	"OK")
   
   io.stderr:write("mnoo/args_cl: self test passed\n")
end

-- Store evaluation result so that require() looks it up when
-- precompiled libraries are being used
_LOADED["mnoo_current/args_cl.lua"]=cl
return cl