local EventEmitter = require "luanode.event_emitter"
local Class = require "luanode.class"
local json = require "json"

local User = Class.InheritsFrom(EventEmitter)
local _NAME = ...
local _ENV = User
package.loaded[_NAME] = _ENV

local uuid = require "uuid"

local ChatClient = require "LnChatClient"
local helpers = require "LnChatClient.helpers"
local Session = require "LnChatClient.Session"
local Protocol = require "LnChatClient.Protocol"
local inspect = require "luanode.utils".inspect

local assert, type, pairs, tostring = assert, type, pairs, tostring

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

User.__tostring = function(user)
	return ("User: '%s'"):format(user.user_id or "")
end

User.__type = "LnChatClient.User"

local event_handlers = {}

--
--
function event_handlers.on_connect (user, session_id, data)
	user.m_logger.debug("[LnChatClient] on_connect: %s", session_id)
	local session = user.sessions[session_id]
	if session then
		local count = helpers.CountKeys(data.participants)
		-- if there is only one participant and its me, then don't notify (REVER ESTO)
		--if count == 1 and data.participants[user.user_id] then
			--return
		--end
		user.m_logger.info("[LnChatClient] New participant for session %s", session_id)
		data.message = "OnConnect"
		data.reason = "new_participant"

		user.client:emit("session_added", session)

		user:emit("new_participant", session, data)
	else
		-- I was added to a new session. Someone wants to chat with me
		user.m_logger.info("[LnChatClient] I've been added to session %s", session_id)

		local filtered_data = {}
		for k,v in pairs(data) do
			if not Session.IsExcludedKey(k) then
				filtered_data[k] = v
			end
		end

		local session = Session(user.client, session_id, user, filtered_data)
		user.sessions[session_id] = session

		data.message = "OnConnect"
		data.reason = "chat_offer"

		user.client:emit("session_added", session)

		user:emit("new_chat", session, data)
	end
end

--
--
function event_handlers.on_disconnect (user, session_id, data)
	user.m_logger.debug("[LnChatClient] on_disconnect: %s", session_id)
	local session = user.sessions[session_id]
	if session then
		user.client:emit("session_removed", session)
		user.sessions[session_id] = nil
		if data.reason == "finish_session" then
			session.properties.session_status = "disconnected"
			data.message = "OnSessionClosed"
			user:emit("session_closed", session, data)
		else
			data.message = "OnDisconnect"
			user:emit("session_disconnect", session, data)
		end
	end
end

--
--
function event_handlers.on_change_session_properties (user, session_id, data)
	user.m_logger.debug("[LnChatClient] on_change_session_properties: %s", session_id)
	local session = user.sessions[session_id]
	if not session then
		return
	end

	for k,v in pairs(data) do
		if not Session.IsExcludedKey(k) then
			session.properties[k] = v
		end
	end

	if data.session_change_reason == "user_join" then
		local newUserId = data.joined_user_id
		session.participants[newUserId] = data.joined_user_name

		if data.joined_user_id ~= user.user_id then
			data.message = "OnUserJoined"
			user:emit("user_joined", session, data, newUserId)
		end

	elseif data.session_change_reason == "user_left" then
		local leftUserId = data.left_user_id
		session.participants[leftUserId] = nil

		if data.left_user_id ~= user.user_id then
			data.message = "OnUserLeft"
			user:emit("user_left", session, data, newUserId)
		end

	else
		-- no change reason, either user_join or user_left
		data.message = "OnSessionUpdateProperties"
		user:emit("session_update_properties", session, data)
	end
end

function event_handlers.on_new_message (user, session_id, data)
	--user.m_logger.debug("[LnChatClient] on_new_message: %s", session_id)
	local session = user.sessions[session_id]
	if not session then
		return
	end
	if data.msgtype == "text" or data.msgtype == "previous_text" then
		data.message = "OnNewMessage"
		user:emit("new_message", session, data, data.text)

	elseif data.msgtype == "urlpush" then
		data.message = "OnUrlPush"
		user:emit("url_push", session, data, data.text)

	elseif data.msgtype == "indication" then
		data.message = "OnIndication"
		user:emit("indication", session, data, data.indication_type, data.values)

	elseif data.msgtype == "geo" then
		data.message = "OnCoordinatesMessage"
		user:emit("coordinates", session, data)

	else
		data.message = "OnUnknownMessage"
		user:emit("unknown_message", session, data)
	end
end

--function event_handlers:on_close_session()
--end

function event_handlers.on_server_disconnected (user)
	user.m_logger.debug("[LnChatClient] on_server_disconnected")

	for key, session in pair(user.sessions) do
		session.properties.session_status = "disconnected"
		local data = { message = "OnDisconnect", session_id = key }
		user:emit("session_disconnect", session, data)
	end

	local data = { message = "OnServerDisconnect", session_id = "*" }
	user:emit("server_disconnected", nil, data)
end


function User:__init (client, user_key, token, query)
	assert(self == User)
	assert(client and token)

	local u = Class.construct(User)

	u.handlers = event_handlers
	u.client = client
	u.user_id = user_key
	u.token = token
	u.properties = query
	u.vcc = assert(query.vcc)
	u.display_name = assert(query.display_name)
	u.user_type = assert(query.user_type)
	u.user_name = assert(query.user_name)

	u.sessions = {}

	u.m_logger = client.m_logger

	return u
end


--
--
function User:Logout (options, callback)

	if type(options) == "function" then
		callback = options
		options = nil
	end

	local message = tostring(self.token)

	self.client.m_protocol:RemoveUser(self.client, self.token, options, function(ok, err)
		local chatSessionsWasRemovedFrom = self.sessions
		self.client:_RemoveUser(self)
		self.sessions = {}
		helpers.TryCall(callback, ok, err, chatSessionsWasRemovedFrom)
	end)
end

--
-- Creates a session and assigns itself to it
-- @param params is a table with properties for the session [optional]
-- If you want to assign an id to the session, pass a field 'id' in 'params'
function User:CreateSession (params, callback)
	local sessionId
	local sessionArgs = {}

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

	if type(params) == "table" then
		sessionId = params.id or uuid.new()
		for k,v in pairs(params) do
			if not helpers.IsExcludedKey(k) then
				sessionArgs[k] = tostring(v)
			end
		end
	else
		sessionId = uuid.new()
	end

	-- Create the session on the server
	self.client.m_protocol:CreateSession(self.client, sessionId, sessionArgs, function(ok, err)
		if ok then
			local session = Session(self.client, sessionId, self, sessionArgs)
			self.sessions[sessionId] = session
			-- connect user to session
			self:AddTo(session, "normal", callback)
		else
			helpers.TryCall(callback, false, err)
		end
	end)
end

do
---
-- Helper function that adds a user to a session.
-- @param user can only be a User instance
-- @param session can be a session object or a session id
-- @param role is a string stating the role of the user in the session
--   the role can be any of { normal, monitor, ninja }
--   the default role is 'normal'
--
-- When @param session is a session id, it means that we want to add the user to a session created
-- elsewhere (we don't have an instance of Session locally yet). In that case, the new session will
-- be catched in its announcement.
--
local function AddTo (user, session, role, callback)

	-- if the session is non-local, prepare to catch the new session announcement
	local session_id
	if type(session) == "string" then
		session_id = session
		-- catch session announcement
		user:once("new_chat", function(_, new_session, data)
			session = new_session
		end)
	else
		session_id = assert(type(session) == "table" and session.id)
	end

	user.client.m_protocol:ConnectUserToSession(user.client, user.user_id, session_id, role, function(ok, err)
		if ok then
			helpers.TryCall(callback, ok, session)
		else
			helpers.TryCall(callback, false, err)
		end
	end)
end

---
-- Helper function that adds a user as a coach to a session
-- @param user can only be a User instance
-- @param session can only be a session object
-- @param coachedUsername is the username of the user to be coached
--
-- When @param session is a session id, it means that we want to add the user to a session created
-- elsewhere (we don't have an instance of Session locally yet). In that case, the new session will
-- be catched in its announcement.
--
local function AddAsCoachTo (user, session, coachedUsername, callback)
	assert(type(coachedUsername) == "string", "you must supply a user to be coached")

	local coachedUserId = coachedUsername .. "@" .. user.vcc

	-- if the session is non-local, prepare to catch the new session announcement
	local session_id
	if type(session) == "string" then
		session_id = session
		-- catch session announcement
		user:once("new_chat", function(_, new_session, data)
			session = new_session
		end)
	else
		session_id = assert(type(session) == "table" and session.id)
	end

	-- bind the coached user id and finally call the original function
	user.client.m_protocol:ConnectCoachToSession(user.client, user.user_id, session_id, coachedUserId, function(ok, err)
		if ok then
			helpers.TryCall(callback, ok, session)
		else
			helpers.TryCall(callback, false, err)
		end
	end)
end

---
-- Helper function that changes the role of a user in a session.
-- @param user can only be a User instance
-- @param session can be a session object or a session id
-- @param role is a string stating the role of the user in the session
--   the role can only be 'monitor'
--
-- When @param session is a session id, it means that we want to add the user to a session created
-- elsewhere (we don't have an instance of Session locally yet). In that case, the new session will
-- be catched in its announcement.
--
local function ChangeRole (user, session, new_role, callback)
	-- if the session is non-local, prepare to catch the new session announcement
	local session_id
	if type(session) == "string" then
		session_id = session
		-- catch session announcement
		user:once("new_chat", function(_, new_session, data)
			session = new_session
		end)
	else
		session_id = assert(type(session) == "table" and session.id)
	end

	user.client.m_protocol:ChangeUserRoleInSession(user.client, user.user_id, session_id, new_role, function(ok, err)
		if ok then
			helpers.TryCall(callback, ok, session)
		else
			helpers.TryCall(callback, false, err)
		end
	end)
end

---
-- Helper function that makes the user a coach in a session in which she already participates
-- @param user can only be a User instance
-- @param session can only be a session object
-- @param coachedUsername is the username of the user to be coached
--
-- When @param session is a session id, it means that we want to add the user to a session created
-- elsewhere (we don't have an instance of Session locally yet). In that case, the new session will
-- be catched in its announcement.
--
-- For the time being, only monitors can become coaches.
--
local function BecomeCoach (user, session, coachedUsername, callback)
	assert(type(coachedUsername) == "string", "you must supply a user to be coached")

	local coachedUserId = coachedUsername .. "@" .. user.vcc

	-- if the session is non-local, prepare to catch the new session announcement
	local session_id
	if type(session) == "string" then
		session_id = session
		-- catch session announcement
		user:once("new_chat", function(_, new_session, data)
			session = new_session
		end)
	else
		session_id = assert(type(session) == "table" and session.id)
	end

	-- bind the coached user id and finally call the original function
	user.client.m_protocol:BecomeCoachInSession(user.client, user.user_id, session_id, coachedUserId, function(ok, err)
		if ok then
			helpers.TryCall(callback, ok, session)
		else
			helpers.TryCall(callback, false, err)
		end
	end)
end

---
-- Adds itself to a session.
-- @param session can be a session object or a session id
-- @param role is a string stating the role of the user in the session
--   the role can be any of { normal, monitor, ninja, coach }
--
function User:AddTo (session, role, ...)
	helpers.AssertValidRole(role)

	if role == "coach" then
		return AddAsCoachTo(self, session, ...)
	else
		return AddTo(self, session, role, ...)
	end
end

---
-- Change the role of a user which already participates in a session.
-- @param session can be a session object or a session id
-- @param role is a string stating the new role the user will take
--   the role con only be 'coach' or 'monitor' for the time being.
--
function User:ChangeRole (session, role, ...)
	helpers.AssertValidRole(role)

	if role == "coach" then
		return BecomeCoach(self, session, ...)
	elseif role == "monitor" then
		return ChangeRole(self, session, role, ...)
	else
		error("only 'monitor' or 'coach' roles are allowed")
	end
end

end


--
-- Removes itself from a session
-- @param session can be a session object or a session id
-- @param options is an optional table with extra options to be sent
function User:RemoveFrom (session, options, callback)
	if type(options) == "function" then
		callback = options
		options = nil
	end

	if type(session) == "string" then
		session = self.sessions[session]
		if not session then
			helpers.TryAsyncCall(callback, false, "attempt to disconnect from a session you're not participating in")
			return false
		end
	end

	self.client.m_protocol:DisconnectUserFromSession(self.client, self.user_id, session.id, options, callback)
end

--
-- Sends a message to a session
function User:SendTo (session, message, callback)

	assert(type(session) == "string" or type(session) == "table", "'session' must be a string or a table")
	assert(type(message) == "string", "'message' must be a string")

	if type(session) == "string" then
		session = self.sessions[session]
		if not session then
			helpers.TryAsyncCall(callback, false, "attempt to send a message to a session you're not participating in")
			return false
		end
	elseif not self.sessions[session.id] then
		helpers.TryAsyncCall(callback, false, "attempt to send a message to a session you're not participating in")
		return false
	end

	local message = {
		text = message,
		msgtype = "text",
		user_id = self.user_id,
		user_name = self.user_name,
		display_name = self.display_name,
		session_id = session.id
	}

	self.client.m_protocol:SendMessage(self.client, self.token, session.id, message, callback)
end

--
-- Sends internal message to a session
function User:SendInternalTo (session, message, callback)

	assert(type(session) == "string" or type(session) == "table", "'session' must be a string or a table")
	assert(type(message) == "string", "'message' must be a string")

	if type(session) == "string" then
		session = self.sessions[session]
		if not session then
			helpers.TryAsyncCall(callback, false, "attempt to send a message to a session you're not participating in")
			return false
		end
	elseif not self.sessions[session.id] then
		helpers.TryAsyncCall(callback, false, "attempt to send a message to a session you're not participating in")
		return false
	end

	local message_json = json.decode(message)

	local message = {
		text = message_json.text,
		msgtype = "text",
		user_id = self.user_id,
		user_name = self.user_name,
		display_name = self.display_name,
		session_id = session.id,
		isInternalMessage = true,
		add_to_qna = message_json.add_to_qna
	}

	self.client.m_protocol:SendInternalMessage(self.client, self.token, session.id, message, callback)
end
--
-- Sends coordinates to a session
function User:SendXYTo (session, message, callback)

	assert(type(session) == "string" or type(session) == "table", "'session' must be a string or a table")

	if type(session) == "string" then
		session = self.sessions[session]
		if not session then
			helpers.TryAsyncCall(callback, false, "attempt to send a message to a session you're not participating in")
			return false
		end
	elseif not self.sessions[session.id] then
		helpers.TryAsyncCall(callback, false, "attempt to send a message to a session you're not participating in")
		return false
	end

	local message = {
		text = "",
		coordinates = {{lat = message.user_latitude, lon = message.user_longitude}},
		is_enabled = message.is_enabled,
		msgtype = "geo",
		msgtype_content = "geo",
		user_id = self.user_id,
		user_name = self.user_name,
		display_name = self.display_name,
		session_id = session.id
	}

	self.client.m_protocol:SendMessage(self.client, self.token, session.id, message, callback)
end

---
-- Sends and indication to a session
--
-- Sends a message to a session
function User:SendIndicationTo (session, indication_type, values, store, callback)
	assert(type(indication_type) == "string", "'indication_type' must be a string")

	if type(store) == "function" and not callback then
		callback = store
		store = false
	end
	assert(type(store) == "boolean", "'store' must be a true or false")

	if type(session) == "string" then
		session = self.sessions[session]
		if not session then
			helpers.TryAsyncCall(callback, false, "attempt to send a message to a session you're not participating in")
			return
		end
	elseif not self.sessions[session.id] then
		helpers.TryAsyncCall(callback, false, "attempt to send a message to a session you're not participating in")
		return
	end

	local message = {
		msgtype = "indication",
		indication_type = indication_type,
		user_id = self.user_id,
		user_name = self.user_name,
		display_name = self.display_name,
		session_id = session.id,
		values = values,
		store = store
	}

	self.client.m_protocol:SendMessage(self.client, self.token, session.id, message, callback)
end

--
--
function User:GetSessions ()
	return self.sessions
end

return _ENV
