local net = require "luanode.net"
local EventEmitter = require "luanode.event_emitter"
local Class = require "luanode.class"
local console = console

local LnChatClient = {}
package.loaded[select(1, ...)] = LnChatClient

local helpers = require "LnChatClient.helpers"
local User = require "LnChatClient.User"
local Protocol = require "LnChatClient.Protocol"

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


local m_keepAlive = false
local m_keepAliveInterval = 0
local m_keepAliveTime = 0

local Client = Class.InheritsFrom(EventEmitter)
Client.__type = "LnChatClient.Client"

---
-- Constructor. Can take a table with options.
-- @field logger is a table with the functions to be used for logging (usually, a lualiblogger instance)
function Client:__init (args)
	local client = Class.construct(Client)
	
	client.m_users_by_token = {}
	client.m_users = {}
	client.m_sessions = {}

	client.m_protocol = Protocol()
	
	if args then
		if args.logger then
			client.m_logger = args.logger
		end
	end
	
	client.m_logger = client.m_logger or {
		debug = console.debug,
		info = console.info,
		warn = console.warn,
		error = console.error,
		fatal = console.fatal,
		profile = console.info
	}
	
	return client
end


---
-- Connects to the Chat server, asynchronously.
-- @param args is a table with 'server' and 'port' fields
-- Emits the 'connect' event when it connected.
function LnChatClient.Connect (args)

	local client = Client(args)
	
	client.m_server = args.server or "127.0.0.1"
	client.m_port = args.port or 1866
	client.m_timeout = args.timeout or 15000
	
	if args.keep_alive then
		client.m_keepAlive = true
		client.m_keepAliveInterval = args.keep_alive.interval or 3000
		client.m_keepAliveTime = args.keep_alive.time or 0
	end
	
	client:reconnect()
	
	client:on("session_added", function(self, session)
		local s = self.m_sessions[session.id]
		if not s then
			self.m_sessions[session.id] = { session = session, count = 1 }
		else
			s.count = s.count + 1
		end
	end)
	
	client:on("session_removed", function(self, session)
		local s = self.m_sessions[session.id]
		if s then
			s.count = s.count - 1
			if s.count == 0 then
				self.m_sessions[session.id] = nil
			end
		end
	end)
	
	return client
end

--
-- utility functions
function Client:_FindUser (user_key)
	return self.m_users[user_key]
end

function Client:_RemoveUser (key)
	if type(key) == "string" then
		local user = self.m_users[key]
		self.m_users[user] = nil
		self.m_users[key] = nil
		self.m_users_by_token[user.token] = nil
	else
		local user_id = key.user_id
		self.m_users[key] = nil
		self.m_users[user_id] = nil
		self.m_users_by_token[key.token] = nil
	end
end

---
-- Reconnects to the current chat server.
function Client:reconnect ()
	self.m_server_connection = net.createConnection(self.m_port, self.m_server)
	
	self.m_server_connection:on("connect", function(cnx)
		self:emit("connect")
	end)
	
	self.m_server_connection:on("error", function(cnx, msg)
		self:emit("connectError", msg)
	end)
	
	self.m_server_connection:on("data", self.m_protocol:MakeConnectionHandler(self, self.m_server_connection))
end

---
-- Logs a user in to the chat server
function Client:Login (user_name, vcc, options, callback)
	local user_key, display_name
	local user_type = "webclient"
	
	if type(user_name) == "table" then
		if type(vcc) == "function" then
			callback = vcc
			vcc = nil
		end
		local params = user_name
		if type(params.user_id) == "string" then
			user_key = params.user_id
			user_name, vcc = params.user_id:match("(.+)@(.+)")
		else
			user_name = assert(params.user_name)
			vcc = assert(params.vcc)
			
			user_key = user_name.."@"..vcc
		end
		options = params
	else
		if type(options) == "function" then
			callback = options
			options = nil
		end
		if type(vcc) ~= "string" then
			error("vcc is missing")
		end
		user_key = user_name .. "@" .. vcc
	end
	
	if type(options) == "table" then
		display_name = options.display_name or user_name
		if type(display_name) ~= "string" then
			error("display_name field must be a string")
		end
		
		user_type = options.user_type or user_type
		if type(user_type) ~= "string" then
			error("user_type field must be a string")
		end
		if user_type == "coach" then
			local user = options.coached_user
			options.coached_user = user .. "@" .. vcc
		end
	else
		-- defaults
		if user_name ~= "" then
			display_name = user_name
		else
			display_name = user_key:match("([^@]+)")
		end
		options = {}
	end
	
	-- now we have the means to identify a user
	if self:_FindUser(user_key) then
		process.nextTick(function()
			callback(false, ("user '%s' is already logged in"):format(user_key))
		end)
		return
	end
	
	local query = {
		user_id = user_key,
		user_name = user_name,
		display_name = display_name,
		vcc = vcc,
		user_type = user_type
	}
	
	for k,v in pairs(options) do
		if type(k) == "string" and type(v) == "string" and not helpers.IsExcludedKey(k) then
			query[k] = v
		end
	end
	
	self.m_protocol:LoginUser(self, user_key, query, function(token, err)
		if token then
			local user = User(self, user_key, token, query)
			self.m_users_by_token[token] = user
			self.m_users[user_key] = user
			callback(true, user)
		else
			callback(false, err or "Login failed")
		end
	end)
end

---
-- Returns a user by its key or nil of not found
function Client:BindUser (user_key)
	return self:_FindUser(user_key)
end

---
-- Logs out a user from the chat server
function Client:Logout (user_name, vcc, callback)
	local user_id

	if not callback and type(vcc) == "function" then
		callback = vcc
		vcc = nil
	end

	if type(user_name) == "table" then
		if user_name.user_id then
			user_id = user_name.user_id
		else
			local params = user_name
			user_name = assert(params.user_name)
			vcc = assert(params.vcc)
			user_id = user_name.."@"..vcc
		end
	else
		user_id = user_name.."@"..vcc
	end
	
	-- now we have the means to identify a user
	local user = self:_FindUser(user_id)
	if not user then
		local err = self.m_logger.warn( ("user '%s' is not logged in"):format(user_id) )
		helpers.TryAsyncCall(callback, false, err)
		return false, err
	end
	
	user:Logout(callback)
end

---
-- Checks if the user is logged in
function Client:IsLoggedIn (user_name, vcc, callback)
	assert(type(user_name) == "string", "You must supply a user_name")
	assert(type(vcc) == "string", "You must supply a vcc")

	local userAtVcc = ("%s@%s"):format(user_name, vcc)
	self.m_protocol:IsLoggedIn(self, userAtVcc, callback)
end

---
-- 
function Client:Disconnect ()
	self.m_server_connection:finish()
end

---
--
function Client:GetUsers (callback)
	self.m_protocol:GetUsers(self, callback)
end

---
--
function Client:GetSessions (callback)
	self.m_protocol:GetSessions(self, callback)
end

---
-- Sends a message
-- @param message is a table with the message options
-- @field text holds the message to be sent
--
function Client:SendMessage (token, session_id, message, callback)
	self.m_protocol:SendMessage(self, token, session_id, message, callback)
end

---
-- Sends a message
-- @param message is a table with the message options
-- @field text holds the message to be sent
--
function Client:AddTextToSession (session_id, message, callback)
	self.m_protocol:AddTextToSession(self, session_id, message, callback)
end

---
-- Sends a "system" message
-- @param message is a table with the message options
-- @field text holds the message to be sent
--
function Client:SendSystemMessage (session_id, message, callback)
	self.m_protocol:SendSystemMessage(self, session_id, message, callback)
end

return LnChatClient
