--[=[
@c Message x Snowflake
@d Represents a text message sent in a Discord text channel. Messages can contain
simple content strings, rich embeds, attachments, or reactions.
]=]

local json = require('json')
local enums = require('enums')
local constants = require('constants')
local Cache = require('iterables/Cache')
local ArrayIterable = require('iterables/ArrayIterable')
local Snowflake = require('containers/abstract/Snowflake')
local Reaction = require('containers/Reaction')
local Resolver = require('client/Resolver')

local insert = table.insert
local null = json.null
local format = string.format
local messageFlag = enums.messageFlag
local band, bor, bnot = bit.band, bit.bor, bit.bnot

local Message, get = require('class')('Message', Snowflake)

function Message:__init(data, parent)
	Snowflake.__init(self, data, parent)
	self._author = self.client._users:_insert(data.author)
	if data.member then
		data.member.user = data.author
		self._parent._parent._members:_insert(data.member)
	end
	self._timestamp = nil -- waste of space; can be calculated from Snowflake ID
	if data.reactions and #data.reactions > 0 then
		self._reactions = Cache(data.reactions, Reaction, self)
	end
	return self:_loadMore(data)
end

function Message:_load(data)
	Snowflake._load(self, data)
	return self:_loadMore(data)
end

local function parseMentions(content, pattern)
	if not content:find('%b<>') then return end
	local mentions, seen = {}, {}
	for id in content:gmatch(pattern) do
		if not seen[id] then
			insert(mentions, id)
			seen[id] = true
		end
	end
	return mentions
end

function Message:_loadMore(data)

	local mentions = {}
	if data.mentions then
		for _, user in ipairs(data.mentions) do
			mentions[user.id] = true
			if user.member then
				user.member.user = user
				self._parent._parent._members:_insert(user.member)
			else
				self.client._users:_insert(user)
			end
		end
	end

	if data.referenced_message and data.referenced_message ~= null then
		if mentions[data.referenced_message.author.id] then
			self._reply_target = data.referenced_message.author.id
		end
		self._referencedMessage = self._parent._messages:_insert(data.referenced_message)
	end

	local content = data.content
	if content then
		if self._mentioned_users then
			self._mentioned_users._array = parseMentions(content, '<@!?(%d+)>')
			if self._reply_target then
				insert(self._mentioned_users._array, 1, self._reply_target)
			end
		end
		if self._mentioned_roles then
			self._mentioned_roles._array = parseMentions(content, '<@&(%d+)>')
		end
		if self._mentioned_channels then
			self._mentioned_channels._array = parseMentions(content, '<#(%d+)>')
		end
		if self._mentioned_emojis then
			self._mentioned_emojis._array = parseMentions(content, '<a?:[%w_]+:(%d+)>')
		end
		self._clean_content = nil
	end

	if data.embeds then
		self._embeds = #data.embeds > 0 and data.embeds or nil
	end

	if data.attachments then
		self._attachments = #data.attachments > 0 and data.attachments or nil
	end

end

function Message:_addReaction(d)

	local reactions = self._reactions

	if not reactions then
		reactions = Cache({}, Reaction, self)
		self._reactions = reactions
	end

	local emoji = d.emoji
	local k = emoji.id ~= null and emoji.id or emoji.name
	local reaction = reactions:get(k)

	if reaction then
		reaction._count = reaction._count + 1
		if d.user_id == self.client._user._id then
			reaction._me = true
		end
	else
		d.me = d.user_id == self.client._user._id
		d.count = 1
		reaction = reactions:_insert(d)
	end
	return reaction

end

function Message:_removeReaction(d)

	local reactions = self._reactions
	if not reactions then return nil end

	local emoji = d.emoji
	local k = emoji.id ~= null and emoji.id or emoji.name
	local reaction = reactions:get(k) or nil

	if not reaction then return nil end -- uncached reaction?

	reaction._count = reaction._count - 1
	if d.user_id == self.client._user._id then
		reaction._me = false
	end

	if reaction._count == 0 then
		reactions:_delete(k)
	end

	return reaction

end

function Message:_setOldContent(d)
	local ts = d.edited_timestamp
	if not ts then return end
	local old = self._old
	if old then
		old[ts] = old[ts] or self._content
	else
		self._old = {[ts] = self._content}
	end
end

function Message:_modify(payload)
	local data, err = self.client._api:editMessage(self._parent._id, self._id, payload)
	if data then
		self:_setOldContent(data)
		self:_load(data)
		return true
	else
		return false, err
	end
end

--[=[
@m setContent
@t http
@p content string
@r boolean
@d Sets the message's content. The message must be authored by the current user
(ie: you cannot change the content of messages sent by other users). The content
must be from 1 to 2000 characters in length.
]=]
function Message:setContent(content)
	return self:_modify({content = content or null})
end

--[=[
@m setEmbed
@t http
@p embed table
@r boolean
@d Sets the message's embed. The message must be authored by the current user.
(ie: you cannot change the embed of messages sent by other users).
]=]
function Message:setEmbed(embed)
	return self:_modify({embed = embed or null})
end

--[=[
@m hideEmbeds
@t http
@r boolean
@d Hides all embeds for this message.
]=]
function Message:hideEmbeds()
	local flags = bor(self._flags or 0, messageFlag.suppressEmbeds)
	return self:_modify({flags = flags})
end

--[=[
@m showEmbeds
@t http
@r boolean
@d Shows all embeds for this message.
]=]
function Message:showEmbeds()
	local flags = band(self._flags or 0, bnot(messageFlag.suppressEmbeds))
	return self:_modify({flags = flags})
end

--[=[
@m hasFlag
@t mem
@p flag Message-Flag-Resolvable
@r boolean
@d Indicates whether the message has a particular flag set.
]=]
function Message:hasFlag(flag)
	flag = Resolver.messageFlag(flag)
	return band(self._flags or 0, flag) > 0
end

--[=[
@m update
@t http
@p data table
@r boolean
@d Sets multiple properties of the message at the same time using a table similar
to the one supported by `TextChannel.send`, except only `content` and `embed`
are valid fields; `mention(s)`, `file(s)`, etc are not supported. The message
must be authored by the current user. (ie: you cannot change the embed of messages
sent by other users).
]=]
function Message:update(data)
	return self:_modify({
		content = data.content or null,
		embed = data.embed or null,
	})
end

--[=[
@m pin
@t http
@r boolean
@d Pins the message in the channel.
]=]
function Message:pin()
	local data, err = self.client._api:addPinnedChannelMessage(self._parent._id, self._id)
	if data then
		self._pinned = true
		return true
	else
		return false, err
	end
end

--[=[
@m unpin
@t http
@r boolean
@d Unpins the message in the channel.
]=]
function Message:unpin()
	local data, err = self.client._api:deletePinnedChannelMessage(self._parent._id, self._id)
	if data then
		self._pinned = false
		return true
	else
		return false, err
	end
end

--[=[
@m addReaction
@t http
@p emoji Emoji-Resolvable
@r boolean
@d Adds a reaction to the message. Note that this does not return the new reaction
object; wait for the `reactionAdd` event instead.
]=]
function Message:addReaction(emoji)
	emoji = Resolver.emoji(emoji)
	local data, err = self.client._api:createReaction(self._parent._id, self._id, emoji)
	if data then
		return true
	else
		return false, err
	end
end

--[=[
@m removeReaction
@t http
@p emoji Emoji-Resolvable
@op id User-ID-Resolvable
@r boolean
@d Removes a reaction from the message. Note that this does not return the old
reaction object; wait for the `reactionRemove` event instead. If no user is
indicated, then this will remove the current user's reaction.
]=]
function Message:removeReaction(emoji, id)
	emoji = Resolver.emoji(emoji)
	local data, err
	if id then
		id = Resolver.userId(id)
		data, err = self.client._api:deleteUserReaction(self._parent._id, self._id, emoji, id)
	else
		data, err = self.client._api:deleteOwnReaction(self._parent._id, self._id, emoji)
	end
	if data then
		return true
	else
		return false, err
	end
end

--[=[
@m clearReactions
@t http
@r boolean
@d Removes all reactions from the message.
]=]
function Message:clearReactions()
	local data, err = self.client._api:deleteAllReactions(self._parent._id, self._id)
	if data then
		return true
	else
		return false, err
	end
end

--[=[
@m delete
@t http
@r boolean
@d Permanently deletes the message. This cannot be undone!
]=]
function Message:delete()
	local data, err = self.client._api:deleteMessage(self._parent._id, self._id)
	if data then
		local cache = self._parent._messages
		if cache then
			cache:_delete(self._id)
		end
		return true
	else
		return false, err
	end
end

--[=[
@m reply
@t http
@p content string/table
@r Message
@d Equivalent to `Message.channel:send(content)`.
]=]
function Message:reply(content)
	return self._parent:send(content)
end

--[=[@p reactions Cache An iterable cache of all reactions that exist for this message.]=]
function get.reactions(self)
	if not self._reactions then
		self._reactions = Cache({}, Reaction, self)
	end
	return self._reactions
end

--[=[@p mentionedUsers ArrayIterable An iterable array of all users that are mentioned in this message.]=]
function get.mentionedUsers(self)
	if not self._mentioned_users then
		local users = self.client._users
		local mentions = parseMentions(self._content, '<@!?(%d+)>')
		if self._reply_target then
			insert(mentions, 1, self._reply_target)
		end
		self._mentioned_users = ArrayIterable(mentions, function(id)
			return users:get(id)
		end)
	end
	return self._mentioned_users
end

--[=[@p mentionedRoles ArrayIterable An iterable array of known roles that are mentioned in this message, excluding
the default everyone role. The message must be in a guild text channel and the
roles must be cached in that channel's guild for them to appear here.]=]
function get.mentionedRoles(self)
	if not self._mentioned_roles then
		local client = self.client
		local mentions = parseMentions(self._content, '<@&(%d+)>')
		self._mentioned_roles = ArrayIterable(mentions, function(id)
			local guild = client._role_map[id]
			return guild and guild._roles:get(id) or nil
		end)
	end
	return self._mentioned_roles
end

--[=[@p mentionedEmojis ArrayIterable An iterable array of all known emojis that are mentioned in this message. If
the client does not have the emoji cached, then it will not appear here.]=]
function get.mentionedEmojis(self)
	if not self._mentioned_emojis then
		local client = self.client
		local mentions = parseMentions(self._content, '<a?:[%w_]+:(%d+)>')
		self._mentioned_emojis = ArrayIterable(mentions, function(id)
			local guild = client._emoji_map[id]
			return guild and guild._emojis:get(id)
		end)
	end
	return self._mentioned_emojis
end

--[=[@p mentionedChannels ArrayIterable An iterable array of all known channels that are mentioned in this message. If
the client does not have the channel cached, then it will not appear here.]=]
function get.mentionedChannels(self)
	if not self._mentioned_channels then
		local client = self.client
		local mentions = parseMentions(self._content, '<#(%d+)>')
		self._mentioned_channels = ArrayIterable(mentions, function(id)
			local guild = client._channel_map[id]
			if guild then
				return guild._text_channels:get(id) or guild._voice_channels:get(id) or guild._categories:get(id)
			else
				return client._private_channels:get(id) or client._group_channels:get(id)
			end
		end)
	end
	return self._mentioned_channels
end

local usersMeta = {__index = function(_, k) return '@' .. k end}
local rolesMeta = {__index = function(_, k) return '@' .. k end}
local channelsMeta = {__index = function(_, k) return '#' .. k end}
local everyone = '@' .. constants.ZWSP .. 'everyone'
local here = '@' .. constants.ZWSP .. 'here'

--[=[@p cleanContent string The message content with all recognized mentions replaced by names and with
@everyone and @here mentions escaped by a zero-width space (ZWSP).]=]
function get.cleanContent(self)

	if not self._clean_content then

		local content = self._content
		local guild = self.guild

		local users = setmetatable({}, usersMeta)
		for user in self.mentionedUsers:iter() do
			local member = guild and guild._members:get(user._id)
			users[user._id] = '@' .. (member and member._nick or user._username)
		end

		local roles = setmetatable({}, rolesMeta)
		for role in self.mentionedRoles:iter() do
			roles[role._id] = '@' .. role._name
		end

		local channels = setmetatable({}, channelsMeta)
		for channel in self.mentionedChannels:iter() do
			channels[channel._id] = '#' .. channel._name
		end

		self._clean_content = content
			:gsub('<@!?(%d+)>', users)
			:gsub('<@&(%d+)>', roles)
			:gsub('<#(%d+)>', channels)
			:gsub('<a?(:[%w_]+:)%d+>', '%1')
			:gsub('@everyone', everyone)
			:gsub('@here', here)

	end

	return self._clean_content

end

--[=[@p mentionsEveryone boolean Whether this message mentions @everyone or @here.]=]
function get.mentionsEveryone(self)
	return self._mention_everyone
end

--[=[@p pinned boolean Whether this message belongs to its channel's pinned messages.]=]
function get.pinned(self)
	return self._pinned
end

--[=[@p tts boolean Whether this message is a text-to-speech message.]=]
function get.tts(self)
	return self._tts
end

--[=[@p nonce string/number/boolean/nil Used by the official Discord client to detect the success of a sent message.]=]
function get.nonce(self)
	return self._nonce
end

--[=[@p editedTimestamp string/nil The date and time at which the message was most recently edited, represented as
an ISO 8601 string plus microseconds when available.]=]
function get.editedTimestamp(self)
	return self._edited_timestamp
end

--[=[@p oldContent string/table Yields a table containing keys as timestamps and
value as content of the message at that time.]=]
function get.oldContent(self)
	return self._old
end

--[=[@p content string The raw message content. This should be between 0 and 2000 characters in length.]=]
function get.content(self)
	return self._content
end

--[=[@p author User The object of the user that created the message.]=]
function get.author(self)
	return self._author
end

--[=[@p channel TextChannel The channel in which this message was sent.]=]
function get.channel(self)
	return self._parent
end

--[=[@p type number The message type. Use the `messageType` enumeration for a human-readable
representation.]=]
function get.type(self)
	return self._type
end

--[=[@p embed table/nil A raw data table that represents the first rich embed that exists in this
message. See the Discord documentation for more information.]=]
function get.embed(self)
	return self._embeds and self._embeds[1]
end

--[=[@p attachment table/nil A raw data table that represents the first file attachment that exists in this
message. See the Discord documentation for more information.]=]
function get.attachment(self)
	return self._attachments and self._attachments[1]
end

--[=[@p embeds table A raw data table that contains all embeds that exist for this message. If
there are none, this table will not be present.]=]
function get.embeds(self)
	return self._embeds
end

--[=[@p attachments table A raw data table that contains all attachments that exist for this message. If
there are none, this table will not be present.]=]
function get.attachments(self)
	return self._attachments
end

--[=[@p guild Guild/nil The guild in which this message was sent. This will not exist if the message
was not sent in a guild text channel. Equivalent to `Message.channel.guild`.]=]
function get.guild(self)
	return self._parent.guild
end

--[=[@p member Member/nil The member object of the message's author. This will not exist if the message
is not sent in a guild text channel or if the member object is not cached.
Equivalent to `Message.guild.members:get(Message.author.id)`.]=]
function get.member(self)
	local guild = self.guild
	return guild and guild._members:get(self._author._id)
end

--[=[@p referencedMessage Message/nil If available, the previous message that
this current message references as seen in replies.]=]
function get.referencedMessage(self)
	return self._referencedMessage
end

--[=[@p link string URL that can be used to jump-to the message in the Discord client.]=]
function get.link(self)
	local guild = self.guild
	return format('https://discord.com/channels/%s/%s/%s', guild and guild._id or '@me', self._parent._id, self._id)
end

--[=[@p webhookId string/nil The ID of the webhook that generated this message, if applicable.]=]
function get.webhookId(self)
	return self._webhook_id
end

return Message
