--------------------------------------------------------------------------------
------------------------------  #####      ##     ------------------------------
------------------------------ ##   ##  #  ##     ------------------------------
------------------------------ ##   ## ##  ##     ------------------------------
------------------------------ ##   ##  #  ##     ------------------------------
------------------------------  #####  ### ###### ------------------------------
--------------------------------                --------------------------------
----------------------- An Object Request Broker in Lua ------------------------
--------------------------------------------------------------------------------
-- Project: OiL - ORB in Lua                                                  --
-- Release: 0.5                                                               --
-- Title  : Client-side CORBA GIOP Protocol specific to IIOP                  --
-- Authors: Renato Maia <maia@inf.puc-rio.br>                                 --
--------------------------------------------------------------------------------
-- Notes:                                                                     --
--   See section 15.7 of CORBA 3.0 specification.                             --
--   See section 13.6.10.3 of CORBA 3.0 specification for IIOP corbaloc.      --
--------------------------------------------------------------------------------
-- channels:Facet
-- 	channel:object retieve(configs:table, [probe:boolean])
-- 	channel:object dispose(configs:table)
-- 	configs:table default([configs:table])
-- 
-- sockets:Receptacle
-- 	socket:object tcp()
-- 	input:table, output:table select([input:table], [output:table], [timeout:number])
--------------------------------------------------------------------------------

local ipairs       = ipairs
local next         = next
local pairs        = pairs
local rawget       = rawget
local rawset       = rawset
local setmetatable = setmetatable
local type         = type

local math = require "math"

local tabop             = require "loop.table"
local ObjectCache       = require "loop.collection.ObjectCache"
local UnorderedArraySet = require "loop.collection.UnorderedArraySet"
local OrderedSet        = require "loop.collection.OrderedSet"
local Wrapper           = require "loop.object.Wrapper"

local oo        = require "oil.oo"
local Exception = require "oil.corba.giop.Exception"
local Channels  = require "oil.kernel.base.Channels"                            --[[VERBOSE]] local verbose = require "oil.verbose"

module "oil.kernel.base.Acceptor"

oo.class(_M, Channels)

--------------------------------------------------------------------------------
-- connection management

LuaSocketOps = tabop.copy(Channels.LuaSocketOps)
CoSocketOps = tabop.copy(Channels.CoSocketOps)

function LuaSocketOps:release()
	UnorderedArraySet.add(self.port, self.__object)
end

function CoSocketOps:release()
	UnorderedArraySet.add(self.port, self)
end

local list = {}
function LuaSocketOps:probe()
	list[1] = self.__object
	return self.sockets:select(list, nil, 0)[1] == list[1]
end

function CoSocketOps:probe()
	local list = { self }
	return self.sockets:select(list, nil, 0)[1] == list[1]
end

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

Port = oo.class()

function Port:__init(object)
	self = oo.rawnew(self, object)
	local factory = self.factory
	self.wrapped = ObjectCache()
	function self.wrapped.retrieve(_, socket)
		socket = factory:setupsocket(socket)
		socket.socket = factory.sockets
		socket.port = self
		return socket
	end
	
	UnorderedArraySet.add(self, self.__object)
	
	return self
end

function Port:accept(probe)                                                     --[[VERBOSE]] verbose:channels("accepting channel from port with ",#self," active channels")
	local except
	if OrderedSet.empty(self) then
		local selected = self.factory.sockets:select(self, nil, probe and 0)
		for _, channel in ipairs(selected) do
			if channel == self.__object then
				channel, except = channel:accept()
			else
				UnorderedArraySet.remove(self, channel)
			end
			OrderedSet.enqueue(self, channel)
		end
	end
	if probe then
		return not OrderedSet.empty(self)
	elseif not except then
		return self.wrapped[ OrderedSet.dequeue(self) ]
	else
		return nil, except
	end
end

--------------------------------------------------------------------------------
-- channel cache for reuse

function __init(self, object)
	self = oo.rawnew(self, object)
	--
	-- cache of active channels
	-- self.cache[host][port] == <channel accepted at host:port>
	--
	self.cache = setmetatable({}, {
		__index = function(hosts, host)
			local cache = ObjectCache()
			function cache.retrieve(_, port)
				local socket, errmsg = self.sockets:tcp()
				if socket then
					self:setupsocket(socket)
					_, errmsg = socket:bind(host, port)
					if _ then
						_, errmsg = socket:listen()
						if _ then                                                           --[[VERBOSE]] verbose:channels("new port binded to ",host,":",port)
							return Port{
								factory = self,
								__object = socket,
							}
						else
							self.except = Exception{ "NO_RESOURCES", minor_code_value = 0,
								message = "unable to listen to port of host",
								reason = "listen",
								error = errmsg,
								host = host, 
								port = port,
							}
						end
					else
						self.except = Exception{ "NO_RESOURCES", minor_code_value = 0,
							message = "unable to bind to port of host",
							reason = "bind",
							error = errmsg,
							host = host, 
							port = port,
						}
					end
				else
					self.except = Exception{ "NO_RESOURCES", minor_code_value = 0,
						message = "unable to create new socket due to error",
						reason = "socket",
						error = except,
					}
				end
			end
			rawset(hosts, host, cache)
			return cache
		end,
	})
	return self
end

--------------------------------------------------------------------------------
-- channel factory

function retrieve(self, profile, probe)                                         --[[VERBOSE]] verbose:channels("retrieve channel accepted from ",profile.host,":",profile.port)
	local port = self.cache[profile.host][profile.port]
	if port then
		return port:accept(probe)
	else
		return nil, self.except
	end
end

function dispose(self, profile)                                                 --[[VERBOSE]] verbose:channels("disposing channels accepted from ",profile.host,":",profile.port)
	local ports = rawget(self.cache, profile.host)
	local port = ports and rawget(ports, profile.port)
	if port then
		ports[profile.port] = nil
		if next(ports) == nil then
			self.cache[profile.host] = nil
		end
		local result, except = port.__object:close()
		if result then
			UnorderedArraySet.remove(port, port.__object)
			while not OrderedSet.empty(port) do
				UnorderedArraySet.add(port, OrderedSet.dequeue(port))
			end
			result = port
		end
		return result, except
	else
		return nil, "already disposed"
	end
end

local PortLowerBound = 2809 -- inclusive (never at first attempt)
local PortUpperBound = 9999 -- inclusive

function default(self, profile)
	local sockets = self.sockets
	profile = profile or {}
	
	-- find a network interface
	local host = profile.host
	if host == nil or host == "*" then
		profile.host = "*"
		host = sockets.dns.gethostname()
	end
	
	-- find a socket port
	if not profile.port then
		local ports = self.cache[profile.host]
		local start = PortLowerBound + math.random(PortUpperBound - PortLowerBound)
		local count = start
		local port
		repeat
			if rawget(ports, count) == nil then
				port = ports[count]
				if port then
					profile.port = count
				else
					local except = self.except
					if except.reason ~= "listen" and except.reason ~= "bind" then
						return nil, except
					end
				end
			end
			if count >= PortUpperBound
				then count = PortLowerBound
				else count = count + 1
			end
		until port or count == start
	end
	
	-- collect addresses
	host = profile.refhost or host
	local addr, info = sockets.dns.toip(host)
	if addr then
		addr = info.ip
		addr[#addr+1] = info.name
		for _, alias in ipairs(info.alias) do
			addr[#addr+1] = alias
		end
	else
		addr = {host}
	end
	for index, name in ipairs(addr) do
		addr[name] = index
	end
	profile.addresses = addr
	
	return profile
end
