#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 = "127.0.0.1",

  -- TCP port number
  server_port = 40404,
}

--****************************************************************************************************************
--****************************************************************************************************************
--*****													     *****
--***** Client side message processing									     *****
--*****													     *****
--****************************************************************************************************************
--****************************************************************************************************************

function client_message_send(msg)
  local msgh = client.msgh
  if (msgh) then
    dbg("[\033[34mClient\033[0m] SEND: %s", tostring(msg))
    msgh:send(msg)
  end
end

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

local function client_message_received(msg)
  local tags  = mtd16.lutTag
  local stati = mtd16.lutStatusCode

  local code = msg:messageCode()
  dbg("[\033[34mClient\033[0m] RECEIVE: %s", 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 handler = client.message_dispatch[code]
  if (handler) then
    handler(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 client_message_send(ans) end
end

------------------------------------------------------------------------------------------------------------------
--
-- This is called regularily on a timer, to tell the server that the client is alive
--
local function client_message_ping()
  local client = client

  if (client.ping_sent) then
    client_close("Ping timeout")
  else
    client.ping_sent = true
    local msg = mtd16.new("Ping")
    client_message_send(msg)
    -- Server gets ten seconds to answer that ping
    client.ping_timer:start(10000)
  end

end

------------------------------------------------------------------------------------------------------------------
--
-- The TCP/IP code calls this function when it has established a connection to the server.
-- No data has been passed around yet.
--
function client_message_connected()
  local client = client

  -- The client will send a ping message to the server every eight seconds. Should the server fail
  -- to see a ping message after ten seconds, the client is automatically disconnected.
  -- 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(client_message_ping)
  client.ping_timer = ping_timer

  -- Cause sending a ping
  client.ping_sent = false
  client.ping_timer:start(1)

  -- Send a hello world message for demo purposes
  -- See server.lua for an example on how to dissect the message.
  local msg = mtd16.new("Hello")
  msg:append("Text", "Hello Server")
  msg:append("Item", 42)
  client_message_send(msg)
end

------------------------------------------------------------------------------------------------------------------
--
-- The TCP/IP code calls this function whenever the client gets disconnected, for whatever reason.
--
function client_message_disconnected()
  if (client.ping_timer) then
    client.ping_timer:close()
    client.ping_timer = nil
  end
end

------------------------------------------------------------------------------------------------------------------
--
-- Processes the server's response to our ping message.
-- Will restart the ping timer, so another ping goes out after eight seconds.
--
-- As soon as the first response is seen from the server,
-- we assume the connection is good and tell the TCP/IP code.
--
local function processPong(msg, ans)
  if (client.ping_sent) then
    client.ping_sent = false
    client.ping_timer:start(8000)
    client_connect_ok()
  end
end

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

  client.message_dispatch = {
    [tags.Pong]		= processPong,
  }

end

--****************************************************************************************************************
--****************************************************************************************************************
--*****													     *****
--***** TCP/IP connection handling									     *****
--*****													     *****
--****************************************************************************************************************
--****************************************************************************************************************
--
-- 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("[\033[34mClient\033[0m] 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.msgh) then
    client.msgh:close()
    client.msgh = 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("[\033[34mClient\033[0m] 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("[\033[34mClient\033[0m] Disconnect from %s:%u", client_config.server_address, client_config.server_port)
  else
    dbg("[\033[34mClient\033[0m] Disconnect from %s:%u (%s)", client_config.server_address, client_config.server_port, text)
  end

  client_message_disconnected()

  client_retry()
end

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

------------------------------------------------------------------------------------------------------------------
--
-- Callback from the MTD16 decoder whenever it has received another message
-- This is passed to the client side message handler code for processing
--
local function client_receive(msgh, msg)
  client_message_received(msg)
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

------------------------------------------------------------------------------------------------------------------
--
-- 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("[\033[34mClient\033[0m] Connect to %s:%u failed: %s", client_config.server_address, client_config.server_port, errtxt)
    client_retry()
    return
  end

  -- Connection established
  client.connected = true

  -- Start received message processing
  local msgh  = mtd16.attach(app, client.socket)
  client.msgh = msgh
  msgh.event  = client_receive
  msgh.eof    = client_eof

  -- Tell mainloop we are up
  dbg("[\033[34mClient\033[0m] Connection established")

  -- Begin message processing
  client_message_connected()
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("[\033[34mClient\033[0m] 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("[\033[34mClient\033[0m] 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),
  }

  -- It's a good idea to delay the first connection attempt for a bit.
  -- DHCP based systems need a few seconds before the network is available.
  -- Starting too early would just produce an ugly error message.
  client.timer:start(2000, true)

  -- Initialize message processor, too
  client_message_start()
end

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