#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,

}

--****************************************************************************************************************
--****************************************************************************************************************
--*****													     *****
--***** Server side message processing									     *****
--*****													     *****
--****************************************************************************************************************
--****************************************************************************************************************
--
-- Sends a message to a client.
-- If client is nil, the message is sent to all clients.
--
function server_message_send(client, msg)

  if (not client) then return server_message_broadcast(msg) end

  local msgh = client.msgh
  if (msgh) then
    dbg("[\033[31mServer:%u\033[0m] SEND: %s", client.id, tostring(msg))
    msgh:send(msg)
  end
end

------------------------------------------------------------------------------------------------------------------
--
-- Send a message to all connected clients.
--
-- The argument 'not_this_client' can be used to exclude a single client from the broadcast.
-- This is used when the message originated from that client and it makes no sense to send it back.
--
function server_message_broadcast(msg, not_this_client)

  dbg("[\033[31mServer\033[0m] BCAST: %s", tostring(msg))

  for _,peer in pairs(server.clients) do
    local client = peer.client
    if (client and (client ~= not_this_client)) then
      local msgh = client.msgh
      if (msgh) then
	msgh:send(msg)
      end
    end
  end
end

------------------------------------------------------------------------------------------------------------------
--
-- Callback from the MTD16 decoder whenever a new message arrives
--
local function server_message_received(client, msg)
  local tags  = mtd16.lutTag
  local stati = mtd16.lutStatusCode

  dbg("[\033[31mServer:%u\033[0m] RECEIVE: %s", client.id, tostring(msg))

  -- Collects the answer message
  local ans = mtd16.new()

  -- A lookup table provides a handler function for each message code that can be processed
  -- All other messages are returned with a status of "NotImplemented"
  local code = msg:messageCode()
  local handler = server.message_dispatch[code]
  if (handler) then
    handler(client, msg, ans)
  else
    local anscode = mtd16.answerCode(code)
    if (anscode) then
      ans:appendMessageCode(anscode)
      ans:append(tags.StatusCode, stati.NotImplemented)
    end
  end

  -- If the handler placed something into the answer buffer, send it back to client
  if (not ans:isEmpty()) then server_message_send(client, ans) end
end

------------------------------------------------------------------------------------------------------------------
--
-- Called when the client fails to respond to a ping message.
--
local function server_message_ping(client)
  server_close(client, "Ping timeout")
end

------------------------------------------------------------------------------------------------------------------
--
-- The TCP/IP code calls this function when it has accepted a new client connection
-- No data has been passed around yet.
--
local function server_message_connected(client)
  local client = client

  -- The client gets a timer that will expire after 10 seconds, unless the client sends regular
  -- ping messages to the server. This is done to detect clients that are switched off by surprise
  -- without being able to do a clean disconnect. You must always expect this kind of misbehaviour.
  local ping_timer = app:timer(function() server_message_ping(client) end)
  client.ping_timer = ping_timer
  ping_timer:start(10000)

end

------------------------------------------------------------------------------------------------------------------
--
-- The TCP/IP code calls this function whenever a client gets disconnected, for whatever reason.
--
local function server_message_disconnected(client)
  -- Stop and remove the ping timer.
  if (client.ping_timer) then
    client.ping_timer:close()
    client.ping_timer = nil
  end
end

------------------------------------------------------------------------------------------------------------------
--
-- Process a ping message
--
local function processPing(client, msg, ans)
  -- It's alive! ALIVE!!! It's aliiive...
  client.ping_timer:start(10000)

  -- Reply in kind
  ans:appendMessageCode("Pong")
end

------------------------------------------------------------------------------------------------------------------
--
-- Process a hello message
--
-- Note that when processing a message, the code must always assume that the data it is looking for is not
-- present in the message. To avoid errors, this example uses the 'or' keyword to provide default data,
-- should the getXXX() function return nil.
--
local function processHello(client, msg, ans)
  local text = msg:getString("Text") or ""
  local nr   = msg:getNumber("Item") or 0

  dbg("[\033[31mServer:%u\033[0m] HELLO -- text=%s nr=%u", client.id, text:quoted(), nr)

  -- Let's send a hello back
  ans:appendMessageCode("Hello")
  ans:append("Text", "Hello Client")
end

------------------------------------------------------------------------------------------------------------------
--
-- Initialize the jumptable for message processing
--
local function server_message_start()
  local tags = mtd16.lutTag

  server.message_dispatch = {
    [tags.Ping]		 = processPing,
    [tags.Hello]	 = processHello,
  }

end

--****************************************************************************************************************
--****************************************************************************************************************
--*****													     *****
--***** TCP/IP connection handling									     *****
--*****													     *****
--****************************************************************************************************************
--****************************************************************************************************************
--
-- 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 message handler and file descriptor
  -- We cannot leave this to the Lua garbage collector, as it needs to be closed NOW!
  client.msgh:close()
  client.msgh = nil

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

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

  -- Inform server side message handler code about lost connection
  server_message_disconnected(client)
end

------------------------------------------------------------------------------------------------------------------
--
-- Callback from the MTD16 decoder when it detects a closed connection.
-- Simply call the server close code.
--
local function server_eof(msgh, text)
  server_close(msgh.client, text)
end

------------------------------------------------------------------------------------------------------------------
--
-- Callback from the MTD16 decoder whenever it has received another message
-- This is passed to the server side message handler code for processing
--
local function server_receive(msgh, msg)
  server_message_received(msgh.client, msg)
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("[\033[31mServer\033[0m] Reject from %s:%u", addr, port)
      return
    end
  end

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

  -- Set up for MTD16 mode
  local msgh = mtd16.attach(app, conn)

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

  -- Register callbacks
  msgh.event = server_receive
  msgh.eof   = server_eof

  -- Remember this client
  clients[id] = client

  dbg("[\033[31mServer:%u\033[0m] Connect from %s:%u", id, addr, port)

  -- Inform server side message handler code about new connection
  server_message_connected(client)
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("[\033[31mServer\033[0m] Listening on %s:%u", addr, port)

  -- Initialize message processor, too
  server_message_start()
end

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