#include <lua/fcntl.lh>
#include <lua/errno.lh>

------------------------------------------------------------------------------------------------------------------
--
-- Configuration of the server
--
server_config = {

  -- Listening to address 0.0.0.0 will accept incoming connections from anyone
  listen_address = "0.0.0.0", -- ANY

  -- TCP port number
  listen_port = 40404,

  -- Limits the number of simultaneously connected clients
  maxconnections = 5,

}

------------------------------------------------------------------------------------------------------------------
--
-- Main application event handler
--
app = event.new()
shell = app:shell()

------------------------------------------------------------------------------------------------------------------
--
-- Initialize graphics display for printing information messages, if available.
-- All messages are also printed to the debug serial port.
--
fb = gfx.open()
if (fb) then
  fb:cls()
  fb:font(gfx.font("/usr/lib/fonts/6x8.lfn"))
  dbgtrm = fb:terminal()
  dbgtrm:write("\033[20h", "\033%G", "\033[?97l")  -- Autofeed, UTF8 mode, enable updates and redraw
end

function dbg(fmt, ...)
  local ok,txt = pcall(string.format, fmt, ...)
  if (dbgtrm) then dbgtrm:write(txt, "\n") end
  print(txt)
end

------------------------------------------------------------------------------------------------------------------
--
-- Whenever a connection closes, for whatever reason, this function
-- cleans up all resources and removes the connection from the active list.
--
function server_close(client, text)
  -- Remove this client from the list of active connections
  server.clients[client.id] = nil

  -- Must close event handler and file descriptor
  -- We cannot leave this to the Lua garbage collector, as it needs to be closed NOW!
  client.rxhnd:close()
  client.rxhnd = nil

  client.conn:close()
  client.conn = nil

  -- Print reason of disconnection
  if (text) then
    dbg("[Server:%u] Disconnect from %s:%u (%s)", client.id, client.addr, client.port, text)
  else
    dbg("[Server:%u] Disconnect from %s:%u", client.id, client.addr, client.port)
  end

end

------------------------------------------------------------------------------------------------------------------
--
-- Called whenever there was data received from the peer
--
local function server_receive(app, fd)

  -- Find the client in connection table
  local client = server.clients[fd]

  -- This should not happen, but it is always a good idea to program defensively
  if (not client) then return end

  -- Read the data.
  -- It is important to continue reading until there is no more data.
  -- The event callback will trigger only once until all buffered data has been processed.

  local conn = client.conn

  while true do
    local data = conn:read()
    if (not data) then break end

    -- For demonstration, the data is simply echoed back to the client
    conn:write(data)

    dbg("[Server:%u] %s", client.id, data:quoted())
  end

  -- When read() has indicated it cannot read any more data, we must check for an error condition
  local text, errno = conn:lastError()

  -- TCP/IP is based on the BSD sockets API which is a bit weird here:
  --   - No error indicates end-of-file, the peer has closed the connection
  --   - EAGAIN is not an error per-se, but simply indicates that the receive buffer is empty
  --   - Any other error is reported and the socket closed

  if (errno ~= EAGAIN) then
    server_close(client, text)
  end

end

------------------------------------------------------------------------------------------------------------------
--
-- Accept another connection and set up an entry in the active list
--
local function server_accept()
  local server = server
  local clients = server.clients

  -- Accept connection
  local conn,addr,port = server.socket:accept()
  if (not conn) then return end

  -- We're using event driven programming, therefore a read() call must never block
  conn:blocking(false)

  -- Do I like you?
  -- Limit the number of accepted connections to avoid using up all resources.
  local maxconn = server_config.maxconnections
  if (maxconn) then
    num = 0
    for x,y in pairs(clients) do num = num + 1 end
    if (num >= maxconn) then
      conn:close()
      dbg("[Server] Reject from %s:%u", addr, port)
      return
    end
  end

  -- Use file descriptor to identify this connection
  local id = conn:fd()

  -- Register a callback that triggers whenever new data arrives
  local rxhnd = app:add(conn, server_receive)

  -- Remember some client properties
  local client = {
    conn = conn,
    rxhnd = rxhnd,
    addr = addr,
    port = port,
    id   = id,
  }

  -- Remember this client
  clients[id] = client

  dbg("[Server:%u] Connect from %s:%u", id, addr, port)
end

------------------------------------------------------------------------------------------------------------------
--
-- Start the TCP server
--
function server_start()

  -- Get a TCP socket handle and make sure it will never block program execution
  local socket = rawio.socket("tcp")
  socket:blocking(false)

  -- Register a callback that triggers whenever a new connection arrives
  local lsnhnd = app:add(socket, server_accept)

  -- Start listening to incoming connections
  local addr = server_config.listen_address
  local port = server_config.listen_port

  socket:bind(addr, port)
  socket:listen()

  -- Set up a data structure for managing connected clients
  server = {
    clients = { },
    socket = socket,
    lsnhnd = lsnhnd,
  }

  dbg("[Server] Listening on %s:%u", addr, port)
end

------------------------------------------------------------------------------------------------------------------
--
-- Main loop
--

server_start()

while true do app:poll() end

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