local enums = require('enums')
local json = require('json')

local channelType = enums.channelType
local insert = table.insert
local null = json.null

local function warning(client, object, id, event)
	return client:warning('Uncached %s (%s) on %s', object, id, event)
end

local function checkReady(shard)
	for _, v in pairs(shard._loading) do
		if next(v) then return end
	end
	shard._ready = true
	shard._loading = nil
	collectgarbage()
	local client = shard._client
	client:emit('shardReady', shard._id)
	for _, other in pairs(client._shards) do
		if not other._ready then return end
	end
	return client:emit('ready')
end

local function getChannel(client, id)
	local guild = client._channel_map[id]
	if guild then
		return guild._text_channels:get(id)
	else
		return client._private_channels:get(id) or client._group_channels:get(id)
	end
end

local EventHandler = setmetatable({}, {__index = function(self, k)
	self[k] = function(_, _, shard)
		return shard:warning('Unhandled gateway event: %s', k)
	end
	return self[k]
end})

function EventHandler.READY(d, client, shard)

	shard:info('Received READY')
	shard:emit('READY')

	shard._session_id = d.session_id
	client._user = client._users:_insert(d.user)

	local guilds = client._guilds
	local group_channels = client._group_channels
	local private_channels = client._private_channels
	local relationships = client._relationships

	for _, channel in ipairs(d.private_channels) do
		if channel.type == channelType.private then
			private_channels:_insert(channel)
		elseif channel.type == channelType.group then
			group_channels:_insert(channel)
		end
	end

	local loading = shard._loading

	if d.user.bot then
		for _, guild in ipairs(d.guilds) do
			guilds:_insert(guild)
			loading.guilds[guild.id] = true
		end
	else
		if client._options.syncGuilds then
			local ids = {}
			for _, guild in ipairs(d.guilds) do
				guilds:_insert(guild)
				if not guild.unavailable then
					loading.syncs[guild.id] = true
					insert(ids, guild.id)
				end
			end
			shard:syncGuilds(ids)
		else
			guilds:_load(d.guilds)
		end
	end

	relationships:_load(d.relationships)

	for _, presence in ipairs(d.presences) do
		local relationship = relationships:get(presence.user.id)
		if relationship then
			relationship:_loadPresence(presence)
		end
	end

	return checkReady(shard)

end

function EventHandler.RESUMED(_, client, shard)
	shard:info('Received RESUMED')
	return client:emit('shardResumed', shard._id)
end

function EventHandler.GUILD_MEMBERS_CHUNK(d, client, shard)
	local guild = client._guilds:get(d.guild_id)
	if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBERS_CHUNK') end
	guild._members:_load(d.members)
	if shard._loading and guild._member_count == #guild._members then
		shard._loading.chunks[d.guild_id] = nil
		return checkReady(shard)
	end
end

function EventHandler.GUILD_SYNC(d, client, shard)
	local guild = client._guilds:get(d.id)
	if not guild then return warning(client, 'Guild', d.id, 'GUILD_SYNC') end
	guild._large = d.large
	guild:_loadMembers(d, shard)
	if shard._loading then
		shard._loading.syncs[d.id] = nil
		return checkReady(shard)
	end
end

function EventHandler.CHANNEL_CREATE(d, client)
	local channel
	local t = d.type
	if t == channelType.text or t == channelType.news then
		local guild = client._guilds:get(d.guild_id)
		if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_CREATE') end
		channel = guild._text_channels:_insert(d)
	elseif t == channelType.voice then
		local guild = client._guilds:get(d.guild_id)
		if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_CREATE') end
		channel = guild._voice_channels:_insert(d)
	elseif t == channelType.private then
		channel = client._private_channels:_insert(d)
	elseif t == channelType.group then
		channel = client._group_channels:_insert(d)
	elseif t == channelType.category then
		local guild = client._guilds:get(d.guild_id)
		if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_CREATE') end
		channel = guild._categories:_insert(d)
	else
		return client:warning('Unhandled CHANNEL_CREATE (type %s)', d.type)
	end
	return client:emit('channelCreate', channel)
end

function EventHandler.CHANNEL_UPDATE(d, client)
	local channel
	local t = d.type
	if t == channelType.text or t == channelType.news then
		local guild = client._guilds:get(d.guild_id)
		if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_UPDATE') end
		channel = guild._text_channels:_insert(d)
	elseif t == channelType.voice then
		local guild = client._guilds:get(d.guild_id)
		if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_UPDATE') end
		channel = guild._voice_channels:_insert(d)
	elseif t == channelType.private then -- private channels should never update
		channel = client._private_channels:_insert(d)
	elseif t == channelType.group then
		channel = client._group_channels:_insert(d)
	elseif t == channelType.category then
		local guild = client._guilds:get(d.guild_id)
		if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_UPDATE') end
		channel = guild._categories:_insert(d)
	else
		return client:warning('Unhandled CHANNEL_UPDATE (type %s)', d.type)
	end
	return client:emit('channelUpdate', channel)
end

function EventHandler.CHANNEL_DELETE(d, client)
	local channel
	local t = d.type
	if t == channelType.text or t == channelType.news then
		local guild = client._guilds:get(d.guild_id)
		if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_DELETE') end
		channel = guild._text_channels:_remove(d)
	elseif t == channelType.voice then
		local guild = client._guilds:get(d.guild_id)
		if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_DELETE') end
		channel = guild._voice_channels:_remove(d)
	elseif t == channelType.private then
		channel = client._private_channels:_remove(d)
	elseif t == channelType.group then
		channel = client._group_channels:_remove(d)
	elseif t == channelType.category then
		local guild = client._guilds:get(d.guild_id)
		if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_DELETE') end
		channel = guild._categories:_remove(d)
	else
		return client:warning('Unhandled CHANNEL_DELETE (type %s)', d.type)
	end
	return client:emit('channelDelete', channel)
end

function EventHandler.CHANNEL_RECIPIENT_ADD(d, client)
	local channel = client._group_channels:get(d.channel_id)
	if not channel then return warning(client, 'GroupChannel', d.channel_id, 'CHANNEL_RECIPIENT_ADD') end
	local user = channel._recipients:_insert(d.user)
	return client:emit('recipientAdd', channel, user)
end

function EventHandler.CHANNEL_RECIPIENT_REMOVE(d, client)
	local channel = client._group_channels:get(d.channel_id)
	if not channel then return warning(client, 'GroupChannel', d.channel_id, 'CHANNEL_RECIPIENT_REMOVE') end
	local user = channel._recipients:_remove(d.user)
	return client:emit('recipientRemove', channel, user)
end

function EventHandler.GUILD_CREATE(d, client, shard)
	if client._options.syncGuilds and not d.unavailable and not client._user._bot then
		shard:syncGuilds({d.id})
	end
	local guild = client._guilds:get(d.id)
	if guild then
		if guild._unavailable and not d.unavailable then
			guild:_load(d)
			guild:_makeAvailable(d)
			client:emit('guildAvailable', guild)
		end
		if shard._loading then
			shard._loading.guilds[d.id] = nil
			return checkReady(shard)
		end
	else
		guild = client._guilds:_insert(d)
		return client:emit('guildCreate', guild)
	end
end

function EventHandler.GUILD_UPDATE(d, client)
	local guild = client._guilds:_insert(d)
	return client:emit('guildUpdate', guild)
end

function EventHandler.GUILD_DELETE(d, client)
	if d.unavailable then
		local guild = client._guilds:_insert(d)
		return client:emit('guildUnavailable', guild)
	else
		local guild = client._guilds:_remove(d)
		return client:emit('guildDelete', guild)
	end
end

function EventHandler.GUILD_BAN_ADD(d, client)
	local guild = client._guilds:get(d.guild_id)
	if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_BAN_ADD') end
	local user = client._users:_insert(d.user)
	return client:emit('userBan', user, guild)
end

function EventHandler.GUILD_BAN_REMOVE(d, client)
	local guild = client._guilds:get(d.guild_id)
	if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_BAN_REMOVE') end
	local user = client._users:_insert(d.user)
	return client:emit('userUnban', user, guild)
end

function EventHandler.GUILD_EMOJIS_UPDATE(d, client)
	local guild = client._guilds:get(d.guild_id)
	if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_EMOJIS_UPDATE') end
	guild._emojis:_load(d.emojis, true)
	return client:emit('emojisUpdate', guild)
end

function EventHandler.GUILD_MEMBER_ADD(d, client)
	local guild = client._guilds:get(d.guild_id)
	if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBER_ADD') end
	local member = guild._members:_insert(d)
	guild._member_count = guild._member_count + 1
	return client:emit('memberJoin', member)
end

function EventHandler.GUILD_MEMBER_UPDATE(d, client)
	local guild = client._guilds:get(d.guild_id)
	if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBER_UPDATE') end
	local member = guild._members:_insert(d)
	return client:emit('memberUpdate', member)
end

function EventHandler.GUILD_MEMBER_REMOVE(d, client)
	local guild = client._guilds:get(d.guild_id)
	if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBER_REMOVE') end
	local member = guild._members:_remove(d)
	guild._member_count = guild._member_count - 1
	return client:emit('memberLeave', member)
end

function EventHandler.GUILD_ROLE_CREATE(d, client)
	local guild = client._guilds:get(d.guild_id)
	if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_ROLE_CREATE') end
	local role = guild._roles:_insert(d.role)
	return client:emit('roleCreate', role)
end

function EventHandler.GUILD_ROLE_UPDATE(d, client)
	local guild = client._guilds:get(d.guild_id)
	if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_ROLE_UPDATE') end
	local role = guild._roles:_insert(d.role)
	return client:emit('roleUpdate', role)
end

function EventHandler.GUILD_ROLE_DELETE(d, client) -- role object not provided
	local guild = client._guilds:get(d.guild_id)
	if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_ROLE_DELETE') end
	local role = guild._roles:_delete(d.role_id)
	if not role then return warning(client, 'Role', d.role_id, 'GUILD_ROLE_DELETE') end
	return client:emit('roleDelete', role)
end

function EventHandler.MESSAGE_CREATE(d, client)
	local channel = getChannel(client, d.channel_id)
	if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_CREATE') end
	local message = channel._messages:_insert(d)
	return client:emit('messageCreate', message)
end

function EventHandler.MESSAGE_UPDATE(d, client) -- may not contain the whole message
	local channel = getChannel(client, d.channel_id)
	if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_UPDATE') end
	local message = channel._messages:get(d.id)
	if message then
		message:_setOldContent(d)
		message:_load(d)
		return client:emit('messageUpdate', message)
	else
		return client:emit('messageUpdateUncached', channel, d.id)
	end
end

function EventHandler.MESSAGE_DELETE(d, client) -- message object not provided
	local channel = getChannel(client, d.channel_id)
	if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_DELETE') end
	local message = channel._messages:_delete(d.id)
	if message then
		return client:emit('messageDelete', message)
	else
		return client:emit('messageDeleteUncached', channel, d.id)
	end
end

function EventHandler.MESSAGE_DELETE_BULK(d, client)
	local channel = getChannel(client, d.channel_id)
	if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_DELETE_BULK') end
	for _, id in ipairs(d.ids) do
		local message = channel._messages:_delete(id)
		if message then
			client:emit('messageDelete', message)
		else
			client:emit('messageDeleteUncached', channel, id)
		end
	end
end

function EventHandler.MESSAGE_REACTION_ADD(d, client)
	local channel = getChannel(client, d.channel_id)
	if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_REACTION_ADD') end
	local message = channel._messages:get(d.message_id)
	if message then
		local reaction = message:_addReaction(d)
		return client:emit('reactionAdd', reaction, d.user_id)
	else
		local k = d.emoji.id ~= null and d.emoji.id or d.emoji.name
		return client:emit('reactionAddUncached', channel, d.message_id, k, d.user_id)
	end
end

function EventHandler.MESSAGE_REACTION_REMOVE(d, client)
	local channel = getChannel(client, d.channel_id)
	if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_REACTION_REMOVE') end
	local message = channel._messages:get(d.message_id)
	if message then
		local reaction = message:_removeReaction(d)
		if not reaction then -- uncached reaction?
			local k = d.emoji.id ~= null and d.emoji.id or d.emoji.name
			return warning(client, 'Reaction', k, 'MESSAGE_REACTION_REMOVE')
		end
		return client:emit('reactionRemove', reaction, d.user_id)
	else
		local k = d.emoji.id ~= null and d.emoji.id or d.emoji.name
		return client:emit('reactionRemoveUncached', channel, d.message_id, k, d.user_id)
	end
end

function EventHandler.MESSAGE_REACTION_REMOVE_ALL(d, client)
	local channel = getChannel(client, d.channel_id)
	if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_REACTION_REMOVE_ALL') end
	local message = channel._messages:get(d.message_id)
	if message then
		local reactions = message._reactions
		if reactions then
			for reaction in reactions:iter() do
				reaction._count = 0
			end
			message._reactions = nil
		end
		return client:emit('reactionRemoveAll', message)
	else
		return client:emit('reactionRemoveAllUncached', channel, d.message_id)
	end
end

function EventHandler.CHANNEL_PINS_UPDATE(d, client)
	local channel = getChannel(client, d.channel_id)
	if not channel then return warning(client, 'TextChannel', d.channel_id, 'CHANNEL_PINS_UPDATE') end
	return client:emit('pinsUpdate', channel)
end

function EventHandler.PRESENCE_UPDATE(d, client) -- may have incomplete data
	local user = client._users:get(d.user.id)
	if user then
		user:_load(d.user)
	end
	if d.guild_id then
		local guild = client._guilds:get(d.guild_id)
		if not guild then return warning(client, 'Guild', d.guild_id, 'PRESENCE_UPDATE') end
		local member
		if client._options.cacheAllMembers then
			member = guild._members:get(d.user.id)
			if not member then return end -- still loading or member left
		else
			if d.status == 'offline' then -- uncache offline members
				member = guild._members:_delete(d.user.id)
			else
				if d.user.username then -- member was offline
					member = guild._members:_insert(d)
				elseif user then -- member was invisible, user is still cached
					member = guild._members:_insert(d)
					member._user = user
				end
			end
		end
		if member then
			member:_loadPresence(d)
			return client:emit('presenceUpdate', member)
		end
	else
		local relationship = client._relationships:get(d.user.id)
		if relationship then
			relationship:_loadPresence(d)
			return client:emit('relationshipUpdate', relationship)
		end
	end
end

function EventHandler.RELATIONSHIP_ADD(d, client)
	local relationship = client._relationships:_insert(d)
	return client:emit('relationshipAdd', relationship)
end

function EventHandler.RELATIONSHIP_REMOVE(d, client)
	local relationship = client._relationships:_remove(d)
	return client:emit('relationshipRemove', relationship)
end

function EventHandler.TYPING_START(d, client)
	return client:emit('typingStart', d.user_id, d.channel_id, d.timestamp)
end

function EventHandler.USER_UPDATE(d, client)
	client._user:_load(d)
	return client:emit('userUpdate', client._user)
end

local function load(obj, d)
	for k, v in pairs(d) do obj[k] = v end
end

function EventHandler.VOICE_STATE_UPDATE(d, client)
	local guild = client._guilds:get(d.guild_id)
	if not guild then return warning(client, 'Guild', d.guild_id, 'VOICE_STATE_UPDATE') end
	local member = d.member and guild._members:_insert(d.member) or guild._members:get(d.user_id)
	if not member then return warning(client, 'Member', d.user_id, 'VOICE_STATE_UPDATE') end
	local states = guild._voice_states
	local channels = guild._voice_channels
	local new_channel_id = d.channel_id
	local state = states[d.user_id]
	if state then -- user is already connected
		local old_channel_id = state.channel_id
		load(state, d)
		if new_channel_id ~= null then -- state changed, but user has not disconnected
			if new_channel_id == old_channel_id then -- user did not change channels
				client:emit('voiceUpdate', member)
			else -- user changed channels
				local old = channels:get(old_channel_id)
				local new = channels:get(new_channel_id)
				if d.user_id == client._user._id then -- move connection to new channel
					local connection = old._connection
					if connection then
						new._connection = connection
						old._connection = nil
						connection._channel = new
						connection:_continue(true)
					end
				end
				client:emit('voiceChannelLeave', member, old)
				client:emit('voiceChannelJoin', member, new)
			end
		else -- user has disconnected
			states[d.user_id] = nil
			local old = channels:get(old_channel_id)
			client:emit('voiceChannelLeave', member, old)
			client:emit('voiceDisconnect', member)
		end
	else -- user has connected
		states[d.user_id] = d
		local new = channels:get(new_channel_id)
		client:emit('voiceConnect', member)
		client:emit('voiceChannelJoin', member, new)
	end
end

function EventHandler.VOICE_SERVER_UPDATE(d, client)
	local guild = client._guilds:get(d.guild_id)
	if not guild then return warning(client, 'Guild', d.guild_id, 'VOICE_SERVER_UPDATE') end
	local state = guild._voice_states[client._user._id]
	if not state then return client:warning('Voice state not initialized before VOICE_SERVER_UPDATE') end
	load(state, d)
	local channel = guild._voice_channels:get(state.channel_id)
	if not channel then return warning(client, 'GuildVoiceChannel', state.channel_id, 'VOICE_SERVER_UPDATE') end
	local connection = channel._connection
	if not connection then return client:warning('Voice connection not initialized before VOICE_SERVER_UPDATE') end
	return client._voice:_prepareConnection(state, connection)
end

function EventHandler.WEBHOOKS_UPDATE(d, client) -- webhook object is not provided
	local guild = client._guilds:get(d.guild_id)
	if not guild then return warning(client, 'Guild', d.guild_id, 'WEBHOOKS_UDPATE') end
	local channel = guild._text_channels:get(d.channel_id)
	if not channel then return warning(client, 'TextChannel', d.channel_id, 'WEBHOOKS_UPDATE') end
	return client:emit('webhooksUpdate', channel)
end

return EventHandler
