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

------------------------------------------------------------------------------------------------------------------
--
-- Configuration of the client
--
client_config = {

  -- The address of the server this client wants to connect to
  server_address = "192.168.200.38",

  -- TCP port number
  server_port = 40404,
}

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

------------------------------------------------------------------------------------------------------------------
--
-- Clean up any resources that where used by the last connection attempt.
-- This is used whenever a connection attempt fails or a connection closes.
--
function client_free()
  local client = client

  if (client.connected) then
    dbg("[Client] Closing connection to %s:%u", client_config.server_address, client_config.server_port)
    client.connected = nil
  end

  if (client.socket) then
    client.socket:close()
    client.socket = nil
  end

  if (client.cnh) then
    client.cnh:close()
    client.cnh = nil
  end

  if (client.rxh) then
    client.rxh:close()
    client.rxh = nil
  end

  client.timer:stop()
end

------------------------------------------------------------------------------------------------------------------
--
-- While not connected, the client tries to connect to the server.
--
-- There is a delay between the connection attempts that increases with a small random
-- component after each failed attempt. This helps spreading the network load if there are
-- many clients trying to connect at the same time.
--
function client_retry()
  local client = client

  client_free()

  if (not client.retry_timeout) then
    client.retry_timeout = 500
  elseif (client.retry_timeout < 5000) then
    client.retry_timeout = client.retry_timeout + 250 + (cipher.random() % 250)
  end

  dbg("[Client] Will try again in %ums", client.retry_timeout)

  client.timer:start(client.retry_timeout)
end

------------------------------------------------------------------------------------------------------------------
--
-- Called whenever a connection was closed
--
function client_close(text)

  client.connected = nil

  if (not text) then
    dbg("[Client] Disconnect from %s:%u", client_config.server_address, client_config.server_port)
  else
    dbg("[Client] Disconnect from %s:%u (%s)", client_config.server_address, client_config.server_port, text)
  end

  client_retry()
end

------------------------------------------------------------------------------------------------------------------
--
-- When the code detects that a connection attempt was successful, it resets the retry counter.
-- This will cause the next connection attempt to start quickly, should the current connection fail.
--
function client_connect_ok()
  client.retry_timeout = nil
end

------------------------------------------------------------------------------------------------------------------
--
-- Called whenever there was data received from the peer
--
local function client_receive()

  -- 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 socket = client.socket

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

    dbg("[Client] %s", data:quoted())
  end

  -- When read() has indicated it cannot read any more data, we must check for an error condition
  local text, errno = socket: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
    client_close(text)
  end

end

------------------------------------------------------------------------------------------------------------------
--
-- The current connection attempt has finished.
-- Either we now have a working connection or an error to process.
--
-- This is basically the BSD sockets weirdness at work.
--
function client_connect_done()
  local client = client

  -- Remove the event callback
  client.cnh:close()
  client.cnh = nil

  -- Check result of the connection attempt
  local err = client.socket:getsockopt(SOL_SOCKET, SO_ERROR)
  if (err ~= 0) then
    client.socket:setError(err)
    local errtxt = client.socket:lastError()
    dbg("[Client] Connect to %s:%u failed: %s", client_config.server_address, client_config.server_port, errtxt)
    client_retry()
    return
  end

  -- Connection established
  client.connected = true

  -- Register a callback that triggers whenever new data arrives
  local rxh = app:add(client.socket, client_receive)

  -- Save state
  client.rxh = rxh

  -- We assume the connection is ok.
  -- In a real world application, the next thing to do would be to exchange some data with the server
  -- and check that the server is functional. Only if that test succeeds, client_connect_ok() should be called.
  client_connect_ok()

  -- Tell mainloop we are up
  dbg("[Client] Connection established")

  -- Let's send some test data
  client.socket:write("Hello World!\r\n")
end

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

local function client_connect()
  local client = client

  client_free()

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

  -- Debugging
  dbg("[Client] Connecting to %s:%u", client_config.server_address, client_config.server_port)

  -- Start connection to client
  client.socket:connect(client_config.server_address, client_config.server_port)

  -- Check for success
  -- The normal result for the connect() call is to report EINPROGRESS, which means that the
  -- TCP protocol is now busy trying to reach the server.
  -- Any other result is an error and will trigger a retry.
  local errtxt,err = client.socket:lastError()
  if (err ~= EINPROGRESS) then
    local errtxt = client.socket:lastError()
    dbg("[Client] Connect to %s:%u failed: %s", client_config.server_address, client_config.server_port, errtxt)
    client_retry()
    return
  end

  -- Set up a callback that is called when the connection attempt is done (successful or not)
  client.cnh = app:add(client.socket, client_connect_done, POLLOUT)
end

------------------------------------------------------------------------------------------------------------------
--
-- The client initialization code will just set up some data structures and leave the actual connecting
-- to a timer callback.
--
function client_start()

  client = {
    retry_timeout = nil,
    timer = app:timer(client_connect),
  }

  client.timer:start(2000, true)
end

------------------------------------------------------------------------------------------------------------------
--
-- Main loop
--
client_start()

while true do app:poll() end

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