local _NAME = ...
local _ENV = {}
package.loaded[_NAME] = _ENV

local assert = assert
local string = string
local table = table
local pairs = pairs
local type = type
local error = error
local setmetatable = setmetatable
local next = next
local require = require
local json = require "json"
local tonumber, tostring = tonumber, tostring
local struct = require "struct"
local DumpDataInHex = require "luanode.utils".DumpDataInHex
local inspect = require "luanode.utils".inspect
local print = print
local os = os

--module("LnChatClient.Protocol", package.seeall)

-- no more external access after this point
setfenv(1, _ENV)

-- allows me to force json4lua to encode empty tables as objects instead of arrays
local json_force_object_mt = { __toJson = function() return "{}" end }
local function json_force_encode_as_object (t)
	setmetatable(t, json_force_object_mt)
end

local helpers = require "LnChatClient.helpers"

local Protocol = {}
Protocol.__index = Protocol
Protocol.__type = "LnChatClient.Protocol"


local function GetNextRequestId (protocol)
	protocol.next_request_id = protocol.next_request_id + 1
	return protocol.next_request_id
end

---
-- A dumb-ass parser to recognize messages in the form:
-- xxxx: 4 bytes with payload length
-- n bytes: the actual payload
-- When you receive data, call the Parse function with it. When a message has been decoded, your callback function 
-- will be called with the message.
-- @param on_message is a function to be called whenever we succesfully parsed a message
-- @return a table with a function "Parse".
local function makeParser (on_message)
	assert(type(on_message) == "function")
	
	-- sospecho que este parser me quedo mas complicado de lo que deberia ser.
	-- Retorno una tabla con un par de funciones. Cada una representa un estado (esperando el header, esperando el 
	-- payload). 
	local buffer = {}
	local header = ""
	local payload_length = 0
	local received_length = 0
	local current_state = nil
	
	local states = {
		waiting_for_header = function(self, data)
			if #data >= 4 then
				local tail_header_len = 4 - #header
				header = header .. data:sub(1, tail_header_len)
				payload_length = struct.unpack("i4", header)
				header = ""
				if #data > 4 then
					local tail = data:sub(1 + tail_header_len)
					return self:waiting_for_payload(tail)
				end
				return self.waiting_for_payload
			else
				header = header .. data
				return self.waiting_for_header
			end
		end,

		waiting_for_payload = function(self, data)
			if received_length + #data >= payload_length then
				local diff = data:sub(1, payload_length - received_length)
				buffer[#buffer + 1] = diff
				--console.warn("GOT MESSAGE")
				on_message(table.concat(buffer))
				buffer = {}
				payload_length = 0
				received_length = 0
				if #data > #diff then
					return self:waiting_for_header(data:sub(#diff + 1))
				else
					return self.waiting_for_header
				end
			else
				received_length = received_length + #data
				buffer[#buffer + 1] = data
			end
			return self.waiting_for_payload
		end,
		
		Parse = function(self, data)
			--console.info(DumpDataInHex(data))
			self.current_state = self:current_state(data)
		end
	}
	
	states.current_state = states.waiting_for_header
	return states
end


--- 
-- Validates an incoming message. Checks that the required fields are present.
local function CheckMessage (message) 
	if type(message["action"]) ~= "string" then return false end
	
	if message.action ~= "request" and message.action ~= "response" and message.action ~= "event" then return false end
	if message.action ~= "event" then
		if type(message["transaction_id"]) ~= "number" then return false end
	end

	if message.action == "response" then
		if type(message["result"]) ~= "string" and type(message["result"]) ~= "number" and type(message["result"]) ~= "boolean" then
			return false
		end
	
	elseif action == "request" then
		if type(message["command"]) ~= "string" then return false end
	end

	return true
end

---
-- Returns a function that handles the chat protocol. It is intended to be used as the "data" callback of a Net 
-- connection. So the function is of the form F(connection, data_received)
function Protocol:MakeConnectionHandler (client, socket)

	-- if the socket closes, cancel any pending requests
	socket:on("end", function()
		self:cancel_requests()
	end)
	
	local parser = makeParser(function(data)
		local message = json.decode(data)
		
		if not CheckMessage(message) then
			client.m_server_connection:destroy()
			client.m_logger.error("Got malformed message: '%s'", data)
			return
		end
		
		if message.action == "response" then
			
			local handler = self.requests[message.transaction_id]
			if not handler then
				client.m_logger.debug("No handler for response. Transaction id = %d, Message = %s", message.transaction_id, inspect(message))
				return
			end
			handler(message.result, message.reason)
			self.requests[message.transaction_id] = nil
			
		elseif message.action == "event" then
		
			message.values.session_id = message.session_id;	-- el chatserver no siempre me manda la sesi�n en los valores
			
			message.command = message.command:lower()
			if message.command == "onconnect" then
				local user = client.m_users_by_token[message.token]
				if user then
					user.handlers.on_connect(user, message.session_id, message.values)
				end
				
			elseif message.command == "ondisconnect" then
				local user = client.m_users_by_token[message.token]
				if user then
					user.handlers.on_disconnect(user, message.session_id, message.values)
				end
				
			elseif message.command == "onnewmessage" then
				local user = client.m_users_by_token[message.token]
				if user then
					user.handlers.on_new_message(user, message.session_id, message.values)
				end
				
			elseif message.command == "ontransfer" then
			
			elseif message.command == "onchangesessionproperties" then
				local user = client.m_users_by_token[message.token]
				if user then
					user.handlers.on_change_session_properties(user, message.session_id, message.values)
				end
			end
		
		elseif action == "request" then
			-- not implemented yet
		end
		
	end)
	
	return function(connection, data)
		parser:Parse(data)
	end
end

---
-- Private
local function WaitForTransaction (protocol, client, params, callback)
	assert(type(params) == "table")
	
	local requestId = GetNextRequestId(protocol)
	params.action = "request"
	params.transaction_id = requestId
	
	local message = json.encode(params)
	
	protocol.requests[requestId] = callback

	--console.warn("WaitForTransaction", "params", inspect(params), "json", message)
	
	-- little endian, int (4 bytes), null terminated string
	-- http://www.inf.puc-rio.br/~roberto/struct/
	local data = struct.pack("<i4 c0",  #message, message)
	client.m_server_connection:write(data)
end

---
--
function Protocol:cancel_requests ()
	self.cancelled = true
	for _, callback in pairs(self.requests) do
		helpers.TryCall(callback, false, { code = "E_REQUEST_CANCELED", message = "request cancelled" })
	end
	self.requests = {}
	self.cancelled = true
end

---
--
function Protocol:ConnectUserToSession (connection, userId, sessionId, role, callback)
	local params = { command = "connectusertosession", user_id = userId, session_id = sessionId, role = role }
	
	WaitForTransaction(self, connection, params, function(status, err)
		helpers.TryCall(callback, status, err)
	end)
end

---
--
function Protocol:ConnectCoachToSession (connection, userId, sessionId, coachedUser, callback)
	local params = { command = "connectusertosession", user_id = userId, session_id = sessionId, role = "coach", coached_user = coachedUser }
	
	WaitForTransaction(self, connection, params, function(status, err)
		helpers.TryCall(callback, status, err)
	end)
end

---
--
function Protocol:ChangeUserRoleInSession (connection, userId, sessionId, new_role, callback)
	local params = { command = "changeuserroleinsession", user_id = userId, session_id = sessionId, new_role = new_role }
	
	WaitForTransaction(self, connection, params, function(status, err)
		helpers.TryCall(callback, status, err)
	end)
end

---
--
function Protocol:BecomeCoachInSession (connection, userId, sessionId, coachedUser, callback)
	local params = { command = "changeuserroleinsession", user_id = userId, session_id = sessionId, new_role = "coach",
					 coached_user = coachedUser }
	
	WaitForTransaction(self, connection, params, function(status, err)
		helpers.TryCall(callback, status, err)
	end)
end

---
--
function Protocol:DisconnectUserFromSession (connection, userId, sessionId, values, callback)
	local params = { command = "disconnectuserfromsession", user_id = userId, session_id = sessionId, values = values }
	
	WaitForTransaction(self, connection, params, function(status, err)
		helpers.TryCall(callback, status, err)
	end)
end

---
--
function Protocol:CreateSession (connection, sessionId, sessionArgs, callback)
	local params = { command = "createsession", session_id = sessionId, values = sessionArgs }

	if not next(params.values) then
		json_force_encode_as_object(params.values)
	end
	
	WaitForTransaction(self, connection, params, function(status, err)
		helpers.TryCall(callback, status, err)
	end)
end

---
--
function Protocol:DeleteSession (connection, sessionId, callback)
	local params = { command = "deletesession", session_id = sessionId }
	
	WaitForTransaction(self, connection, params, function(status, err)
		helpers.TryCall(callback, status, err)
	end)
end

---
--
function Protocol:SetSessionProperties (connection, sessionId, sessionArgs, callback)
	local params = { command = "setpropertiestosession", session_id = sessionId, values = sessionArgs }
	
	WaitForTransaction(self, connection, params, function(status, err)
		helpers.TryCall(callback, status, err)
	end)
end

---
--
function Protocol:GetUsersInSession (connection, sessionId, callback)
	local params = { command = "getusersinsession", session_id = sessionId }
	
	WaitForTransaction(self, connection, params, function(status, err)
		if status then
			helpers.TryCall(callback, json.decode(status))
		else
			helpers.TryCall(callback, false, assert(err) )
		end
	end)
end

---
--
function Protocol:GetTextFromSession (connection, sessionId, callback)
	local params = { command = "gettextfromsession", session_id = sessionId }

	WaitForTransaction(self, connection, params, function(status, err)
		if status then
			helpers.TryCall(callback, json.decode(status))
		else
			helpers.TryCall(callback, false, assert(err) )
		end
	end)
end

---
--
function Protocol:SendMessage (connection, token, sessionId, message, callback)
	local params = { command = "send", token = token, session_id = sessionId, values = message }
	
	WaitForTransaction(self, connection, params, function(status, err)
		helpers.TryCall(callback, status, err)
	end)
end

---
--
function Protocol:SendInternalMessage (connection, token, sessionId, message, callback)
	local params = { command = "send_internal", token = token, session_id = sessionId, values = message }
	
	WaitForTransaction(self, connection, params, function(status, err)
		helpers.TryCall(callback, status, err)
	end)
end

---
--
function Protocol:LoginUser (connection, userId, query, callback)
	local params = { command = "loginuser", channel_id = "TCP", user_id = userId, values = query }
	
	WaitForTransaction(self, connection, params, function(token)
		helpers.TryCall(callback, token)
	end)
end

---
-- Checks if the user is logged in
function Protocol:IsLoggedIn (connection, userAtVcc, callback)
	local params = { command = "isconnected", user_id = userAtVcc }	
	
	WaitForTransaction(self, connection, params, function(result, err)
		helpers.TryCall(callback, result, err)
	end)
end

---
--
function Protocol:RemoveUser (connection, token, values, callback)
	local params = { command = "rmvuser", token = token, values = values }
	
	WaitForTransaction(self, connection, params, function(status, err)
		helpers.TryCall(callback, status, err)
	end)
end

---
--
function Protocol:GetUsers (connection, callback)
	local params = { command = "getusers" }

	WaitForTransaction(self, connection, params, function(result, err)
		if result then
			helpers.TryCall(callback, true, json.decode(result))
		else
			helpers.TryCall(callback, false, result, err)
		end
	end)
end

---
--
function Protocol:GetSessions (connection, callback)
	local params = { command = "getsessions" }

	WaitForTransaction(self, connection, params, function(result, err)
		if result then
			helpers.TryCall(callback, true, json.decode(result))
		else
			helpers.TryCall(callback, false, result, err)
		end
	end)
end

---
--
function Protocol:AddTextToSession (connection, session_id, message, callback)
	local params = { command = "addtexttosession", session_id = session_id, values = message }

	WaitForTransaction(self, connection, params, function(status, err)
		helpers.TryCall(callback, status, err)
	end)
end

---
--
function Protocol:SendSystemMessage (connection, session_id, message, callback)
	local params = { command = "sendsystemmessage", session_id = session_id, values = message }

	WaitForTransaction(self, connection, params, function(status, err)
		helpers.TryCall(callback, status, err)
	end)
end

--function LogoutUser(connection, token, callback)
--end

return function ()
	local prot = {
		next_request_id = 0,
		requests = {}
	}

	return setmetatable(prot, Protocol)
end
