-- LuaSocket-like layer above luxio

local l = require "luxio"
local realsocket = require "socket"

local stream_mt, dgram_mt

local function new_stream(sock, sockaddr)
   return setmetatable({
     sock = sock,
     sockaddr = sockaddr,
     rdbytes = 0,
     wrbytes = 0,
     created = realsocket.gettime()
   }, stream_mt)
end

local function bind(address, service, backlog)
   backlog = backlog or 32
   service = tostring(service)

   assert(type(address) == "string", "address string expected at #1")
   assert(type(service) == "string", "service string expected at #2")

   local r, s = l.getaddrinfo(address, service, l.AI_PASSIVE, l.AF_UNSPEC, l.SOCK_STREAM, l.IPPROTO_TCP)

   if r ~= 0 then
      return nil, ("getaddrinfo: %s"):format(l.gai_strerror(r))
   end
   
   local sock, sockaddr, r, errno

   for _, ai in ipairs(s) do
      sock = l.socket(ai.ai_family, ai.ai_socktype, ai.ai_protocol)
      if sock >= 0 then
	 l.setsockopt(sock, l.SOL_SOCKET, l.SO_REUSEADDR, 1)
	 r, errno = l.bind(sock, ai.ai_addr)
	 if r == 0 then
	    -- success
	    sockaddr = ai.ai_addr
	    break
	 else
	    l.close(sock)
	    sock = nil
	 end
      end
   end

   if sock == nil then
      -- none of them were bindable; return the error from the last
      -- attempt given there's not much else we can return.
      return nil, ("bind: %s"):format(l.strerror(errno))
   end

   r, errno = l.listen(sock, backlog)
   if r < 0 then
      l.close(sock)
      return nil, ("listen: %s"):format(l.strerror(errno))
   end

   return new_stream(sock, sockaddr)
end

local function connect(address, service, locaddr, locport)
   service = tostring(service)

   assert(type(address) == "string", "address string expected at #1")
   assert(type(service) == "string", "service string expected at #1")

   local r, s = l.getaddrinfo(address, service, 0, l.AF_UNSPEC, l.SOCK_STREAM, l.IPPROTO_TCP)

   if r ~= 0 then
      return nil, ("getaddrinfo: %s"):format(l.gai_strerror(r))
   end
   
   local sock, sockaddr, r, errno

   for _, ai in ipairs(s) do
      sock = l.socket(ai.ai_family, ai.ai_socktype, ai.ai_protocol)
      if sock >= 0 then
	 r, errno = l.connect(sock, ai.ai_addr)
	 if r == 0 then
	    -- success
	    sockaddr = ai.ai_addr
	    break
	 else
	    l.close(sock)
	    sock = nil
	 end
      end
   end

   if sock == nil then
      -- none of them were connectable; return the error from the
      -- last attempt given there's not much else we can return.
      return nil, ("connect: %s"):format(l.strerror(errno))
   end

   return new_stream(sock, sockaddr)
end

local function tcp()
   local sock, err = l.socket(l.AF_INET, l.SOCK_STREAM, l.IPPROTO_TCP)
   if sock < 0 then
      return nil, ("socket: %s"):format(l.strerror(errno))
   end
   return new_stream(sock)
end

local function tcp6()
   local sock, err = l.socket(l.AF_INET6, l.SOCK_STREAM, l.IPPROTO_TCP)
   if sock < 0 then
      return nil, ("socket: %s"):format(l.strerror(errno))
   end
   return new_stream(sock)
end

local function stream_accept(s)
   local sock, addr = l.accept(s.sock)
   if sock < 0 then
      return nil, ("accept: %s"):format(l.strerror(addr))
   end
   return new_stream(sock, addr)
end

local function stream_bind(s, addr, serv)
   serv = tostring(serv)
   assert(type(addr) == "string", "address string expected at #2")
   assert(type(addr) == "string", "service string expected at #3")

   local r, s = l.getaddrinfo(address, service, l.AI_PASSIVE, l.AF_UNSPEC, l.SOCK_STREAM, l.IPPROTO_TCP)

   if r ~= 0 then
      return nil, ("getaddrinfo: %s"):format(l.gai_strerror(r))
   end

   local sock = s.sock
   local errno, success

   l.setsockopt(sock, l.SOL_SOCKET, l.SO_REUSEADDR, 1)
   for _, ai in ipairs(s) do
      r, errno = l.bind(sock, ai.ai_addr)
      if r == 0 then
	 -- success
	 s.sockaddr = ai.ai_addr
	 success = true
	 break;
      end
   end

   if success == false then
      return nil, ("bind: %s"):format(l.strerror(errno))
   end

   return 1
end

local function stream_listen(s, backlog)
   backlog = backlog or 32
   local r, errno = l.listen(s.sock, backlog)

   if r ~= 0 then
      return nil, ("listen: %s"):format(l.strerror(errno))
   end
   
   return 1
end

local function stream_receive(s, pattern, prefix)
   assert(type(pattern) == "number", "unimplemented pattern") -- FIXME
   
   local r, errno = l.read(s.sock, pattern)
   if #r < pattern then
      return nil, "timeout", r
   else
      return r
   end
end

local function stream_send(s, data, i, j)
   i = i or 1
   j = j or -1
   local r, errno = l.write(s.sock, data:sub(i, j))
   return i + r
end

local function stream_tostring(s)
   return ("socket: %s"):format(tostring(s.sockaddr or "unbound"))
end

stream_mt = {
   accept = stream_accept,
   bind = stream_bind,
   close = stream_close,
   connect = stream_connect,
   listen = stream_listen,

   receive = stream_receive,
   send = stream_send,

   getpeername = stream_getpeername,
   getsockname = stream_getsockname,
   getstats = stream_getstats,
   setoption = stream_setoption,
   setstats = stream_setstats,
   settimeout = stream_settimeout,
   shutdown = stream_shutdown,

   __tostring = stream_tostring
}

stream_mt.__index = stream_mt

dgram_mt = {

}

return {
    bind = bind,
    connect = connect,
    tcp = tcp,
    tcp6 = tcp6,

    _DEBUG = false,
    _VERSION = realsocket._VERSION,
    newtry = realsocket.newtry,
    protect = realsocket.protect,
    sink = realsocket.sink, 
    skip = realsocket.skip,
    sleep = realsocket.sleep,
    gettime = realsocket.gettime,
    try = realsocket.try,
}
