--[=[
@c Guild x Snowflake
@d Represents a Discord guild (or server). Guilds are a collection of members,
channels, and roles that represents one community.
]=]

local Cache = require('iterables/Cache')
local Role = require('containers/Role')
local Emoji = require('containers/Emoji')
local Invite = require('containers/Invite')
local Webhook = require('containers/Webhook')
local Ban = require('containers/Ban')
local Member = require('containers/Member')
local Resolver = require('client/Resolver')
local AuditLogEntry = require('containers/AuditLogEntry')
local GuildTextChannel = require('containers/GuildTextChannel')
local GuildVoiceChannel = require('containers/GuildVoiceChannel')
local GuildCategoryChannel = require('containers/GuildCategoryChannel')
local Snowflake = require('containers/abstract/Snowflake')

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

local channelType = enums.channelType
local floor = math.floor
local format = string.format

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

function Guild:__init(data, parent)
	Snowflake.__init(self, data, parent)
	self._roles = Cache({}, Role, self)
	self._emojis = Cache({}, Emoji, self)
	self._members = Cache({}, Member, self)
	self._text_channels = Cache({}, GuildTextChannel, self)
	self._voice_channels = Cache({}, GuildVoiceChannel, self)
	self._categories = Cache({}, GuildCategoryChannel, self)
	self._voice_states = {}
	if not data.unavailable then
		return self:_makeAvailable(data)
	end
end

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

function Guild:_loadMore(data)
	if data.features then
		self._features = data.features
	end
end

function Guild:_makeAvailable(data)

	self._roles:_load(data.roles)
	self._emojis:_load(data.emojis)
	self:_loadMore(data)

	if not data.channels then return end -- incomplete guild

	local states = self._voice_states
	for _, state in ipairs(data.voice_states) do
		states[state.user_id] = state
	end

	local text_channels = self._text_channels
	local voice_channels = self._voice_channels
	local categories = self._categories

	for _, channel in ipairs(data.channels) do
		local t = channel.type
		if t == channelType.text or t == channelType.news then
			text_channels:_insert(channel)
		elseif t == channelType.voice then
			voice_channels:_insert(channel)
		elseif t == channelType.category then
			categories:_insert(channel)
		end
	end

	return self:_loadMembers(data)

end

function Guild:_loadMembers(data)
	local members = self._members
	members:_load(data.members)
	for _, presence in ipairs(data.presences) do
		local member = members:get(presence.user.id)
		if member then -- rogue presence check
			member:_loadPresence(presence)
		end
	end
	if self._large and self.client._options.cacheAllMembers then
		return self:requestMembers()
	end
end

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

--[=[
@m requestMembers
@t ws
@r boolean
@d Asynchronously loads all members for this guild. You do not need to call this
if the `cacheAllMembers` client option (and the `syncGuilds` option for
user-accounts) is enabled on start-up.
]=]
function Guild:requestMembers()
	local shard = self.client._shards[self.shardId]
	if not shard then
		return false, 'Invalid shard'
	end
	if shard._loading then
		shard._loading.chunks[self._id] = true
	end
	return shard:requestGuildMembers(self._id)
end

--[=[
@m sync
@t ws
@r boolean
@d Asynchronously loads certain data and enables the receiving of certain events
for this guild. You do not need to call this if the `syncGuilds` client option
is enabled on start-up.

Note: This is only for user accounts. Bot accounts never need to sync guilds!
]=]
function Guild:sync()
	local shard = self.client._shards[self.shardId]
	if not shard then
		return false, 'Invalid shard'
	end
	if shard._loading then
		shard._loading.syncs[self._id] = true
	end
	return shard:syncGuilds({self._id})
end

--[=[
@m getMember
@t http?
@p id User-ID-Resolvable
@r Member
@d Gets a member object by ID. If the object is already cached, then the cached
object will be returned; otherwise, an HTTP request is made.
]=]
function Guild:getMember(id)
	id = Resolver.userId(id)
	local member = self._members:get(id)
	if member then
		return member
	else
		local data, err = self.client._api:getGuildMember(self._id, id)
		if data then
			return self._members:_insert(data)
		else
			return nil, err
		end
	end
end

--[=[
@m getRole
@t mem
@p id Role-ID-Resolvable
@r Role
@d Gets a role object by ID.
]=]
function Guild:getRole(id)
	id = Resolver.roleId(id)
	return self._roles:get(id)
end

--[=[
@m getEmoji
@t mem
@p id Emoji-ID-Resolvable
@r Emoji
@d Gets a emoji object by ID.
]=]
function Guild:getEmoji(id)
	id = Resolver.emojiId(id)
	return self._emojis:get(id)
end

--[=[
@m getChannel
@t mem
@p id Channel-ID-Resolvable
@r GuildChannel
@d Gets a text, voice, or category channel object by ID.
]=]
function Guild:getChannel(id)
	id = Resolver.channelId(id)
	return self._text_channels:get(id) or self._voice_channels:get(id) or self._categories:get(id)
end

--[=[
@m createTextChannel
@t http
@p name string
@r GuildTextChannel
@d Creates a new text channel in this guild. The name must be between 2 and 100
characters in length.
]=]
function Guild:createTextChannel(name)
	local data, err = self.client._api:createGuildChannel(self._id, {name = name, type = channelType.text})
	if data then
		return self._text_channels:_insert(data)
	else
		return nil, err
	end
end

--[=[
@m createVoiceChannel
@t http
@p name string
@r GuildVoiceChannel
@d Creates a new voice channel in this guild. The name must be between 2 and 100
characters in length.
]=]
function Guild:createVoiceChannel(name)
	local data, err = self.client._api:createGuildChannel(self._id, {name = name, type = channelType.voice})
	if data then
		return self._voice_channels:_insert(data)
	else
		return nil, err
	end
end

--[=[
@m createCategory
@t http
@p name string
@r GuildCategoryChannel
@d Creates a channel category in this guild. The name must be between 2 and 100
characters in length.
]=]
function Guild:createCategory(name)
	local data, err = self.client._api:createGuildChannel(self._id, {name = name, type = channelType.category})
	if data then
		return self._categories:_insert(data)
	else
		return nil, err
	end
end

--[=[
@m createRole
@t http
@p name string
@r Role
@d Creates a new role in this guild. The name must be between 1 and 100 characters
in length.
]=]
function Guild:createRole(name)
	local data, err = self.client._api:createGuildRole(self._id, {name = name})
	if data then
		return self._roles:_insert(data)
	else
		return nil, err
	end
end

--[=[
@m createEmoji
@t http
@p name string
@p image Base64-Resolvable
@r Emoji
@d Creates a new emoji in this guild. The name must be between 2 and 32 characters
in length. The image must not be over 256kb, any higher will return a 400 Bad Request
]=]
function Guild:createEmoji(name, image)
	image = Resolver.base64(image)
	local data, err = self.client._api:createGuildEmoji(self._id, {name = name, image = image})
	if data then
		return self._emojis:_insert(data)
	else
		return nil, err
	end
end

--[=[
@m setName
@t http
@p name string
@r boolean
@d Sets the guilds name. This must be between 2 and 100 characters in length.
]=]
function Guild:setName(name)
	return self:_modify({name = name or json.null})
end

--[=[
@m setRegion
@t http
@p region string
@r boolean
@d Sets the guild's voice region (eg: `us-east`). See `listVoiceRegions` for a list
of acceptable regions.
]=]
function Guild:setRegion(region)
	return self:_modify({region = region or json.null})
end

--[=[
@m setVerificationLevel
@t http
@p verification_level number
@r boolean
@d Sets the guild's verification level setting. See the `verificationLevel`
enumeration for acceptable values.
]=]
function Guild:setVerificationLevel(verification_level)
	return self:_modify({verification_level = verification_level or json.null})
end

--[=[
@m setNotificationSetting
@t http
@p default_message_notifications number
@r boolean
@d Sets the guild's default notification setting. See the `notficationSetting`
enumeration for acceptable values.
]=]
function Guild:setNotificationSetting(default_message_notifications)
	return self:_modify({default_message_notifications = default_message_notifications or json.null})
end

--[=[
@m setExplicitContentSetting
@t http
@p explicit_content_filter number
@r boolean
@d Sets the guild's explicit content level setting. See the `explicitContentLevel`
enumeration for acceptable values.
]=]
function Guild:setExplicitContentSetting(explicit_content_filter)
	return self:_modify({explicit_content_filter = explicit_content_filter or json.null})
end

--[=[
@m setAFKTimeout
@t http
@p afk_timeout number
@r number
@d Sets the guild's AFK timeout in seconds.
]=]
function Guild:setAFKTimeout(afk_timeout)
	return self:_modify({afk_timeout = afk_timeout or json.null})
end

--[=[
@m setAFKChannel
@t http
@p id Channel-ID-Resolvable
@r boolean
@d Sets the guild's AFK channel.
]=]
function Guild:setAFKChannel(id)
	id = id and Resolver.channelId(id)
	return self:_modify({afk_channel_id = id or json.null})
end

--[=[
@m setSystemChannel
@t http
@p id Channel-Id-Resolvable
@r boolean
@d Sets the guild's join message channel.
]=]
function Guild:setSystemChannel(id)
	id = id and Resolver.channelId(id)
	return self:_modify({system_channel_id = id or json.null})
end

--[=[
@m setOwner
@t http
@p id User-ID-Resolvable
@r boolean
@d Transfers ownership of the guild to another user. Only the current guild owner
can do this.
]=]
function Guild:setOwner(id)
	id = id and Resolver.userId(id)
	return self:_modify({owner_id = id or json.null})
end

--[=[
@m setIcon
@t http
@p icon Base64-Resolvable
@r boolean
@d Sets the guild's icon. To remove the icon, pass `nil`.
]=]
function Guild:setIcon(icon)
	icon = icon and Resolver.base64(icon)
	return self:_modify({icon = icon or json.null})
end

--[=[
@m setBanner
@t http
@p banner Base64-Resolvable
@r boolean
@d Sets the guild's banner. To remove the banner, pass `nil`.
]=]
function Guild:setBanner(banner)
	banner = banner and Resolver.base64(banner)
	return self:_modify({banner = banner or json.null})
end

--[=[
@m setSplash
@t http
@p splash Base64-Resolvable
@r boolean
@d Sets the guild's splash. To remove the splash, pass `nil`.
]=]
function Guild:setSplash(splash)
	splash = splash and Resolver.base64(splash)
	return self:_modify({splash = splash or json.null})
end

--[=[
@m getPruneCount
@t http
@op days number
@r number
@d Returns the number of members that would be pruned from the guild if a prune
were to be executed.
]=]
function Guild:getPruneCount(days)
	local data, err = self.client._api:getGuildPruneCount(self._id, days and {days = days} or nil)
	if data then
		return data.pruned
	else
		return nil, err
	end
end

--[=[
@m pruneMembers
@t http
@op days number
@op count boolean
@r number
@d Prunes (removes) inactive, roleless members from the guild who have not been online in the last provided days.
If the `count` boolean is provided, the number of pruned members is returned; otherwise, `0` is returned.
]=]
function Guild:pruneMembers(days, count)
	local t1 = type(days)
	if t1 == 'number' then
		count = type(count) == 'boolean' and count
	elseif t1 == 'boolean' then
		count = days
		days = nil
	end
	local data, err = self.client._api:beginGuildPrune(self._id, nil, {
		days = days,
		compute_prune_count = count,
	})
	if data then
		return data.pruned
	else
		return nil, err
	end
end

--[=[
@m getBans
@t http
@r Cache
@d Returns a newly constructed cache of all ban objects for the guild. The
cache and its objects are not automatically updated via gateway events. You must
call this method again to get the updated objects.
]=]
function Guild:getBans()
	local data, err = self.client._api:getGuildBans(self._id)
	if data then
		return Cache(data, Ban, self)
	else
		return nil, err
	end
end

--[=[
@m getBan
@t http
@p id User-ID-Resolvable
@r Ban
@d This will return a Ban object for a giver user if that user is banned
from the guild; otherwise, `nil` is returned.
]=]
function Guild:getBan(id)
	id = Resolver.userId(id)
	local data, err = self.client._api:getGuildBan(self._id, id)
	if data then
		return Ban(data, self)
	else
		return nil, err
	end
end

--[=[
@m getInvites
@t http
@r Cache
@d Returns a newly constructed cache of all invite objects for the guild. The
cache and its objects are not automatically updated via gateway events. You must
call this method again to get the updated objects.
]=]
function Guild:getInvites()
	local data, err = self.client._api:getGuildInvites(self._id)
	if data then
		return Cache(data, Invite, self.client)
	else
		return nil, err
	end
end

--[=[
@m getAuditLogs
@t http
@op query table
@r Cache
@d Returns a newly constructed cache of audit log entry objects for the guild. The
cache and its objects are not automatically updated via gateway events. You must
call this method again to get the updated objects.

If included, the query parameters include: query.limit: number, query.user: UserId Resolvable
query.before: EntryId Resolvable, query.type: ActionType Resolvable
]=]
function Guild:getAuditLogs(query)
	if type(query) == 'table' then
		query = {
			limit = query.limit,
			user_id = Resolver.userId(query.user),
			before = Resolver.entryId(query.before),
			action_type = Resolver.actionType(query.type),
		}
	end
	local data, err = self.client._api:getGuildAuditLog(self._id, query)
	if data then
		self.client._users:_load(data.users)
		self.client._webhooks:_load(data.webhooks)
		return Cache(data.audit_log_entries, AuditLogEntry, self)
	else
		return nil, err
	end
end

--[=[
@m getWebhooks
@t http
@r Cache
@d Returns a newly constructed cache of all webhook objects for the guild. The
cache and its objects are not automatically updated via gateway events. You must
call this method again to get the updated objects.
]=]
function Guild:getWebhooks()
	local data, err = self.client._api:getGuildWebhooks(self._id)
	if data then
		return Cache(data, Webhook, self.client)
	else
		return nil, err
	end
end

--[=[
@m listVoiceRegions
@t http
@r table
@d Returns a raw data table that contains a list of available voice regions for
this guild, as provided by Discord, with no additional parsing.
]=]
function Guild:listVoiceRegions()
	return self.client._api:getGuildVoiceRegions(self._id)
end

--[=[
@m leave
@t http
@r boolean
@d Removes the current user from the guild.
]=]
function Guild:leave()
	local data, err = self.client._api:leaveGuild(self._id)
	if data then
		return true
	else
		return false, err
	end
end

--[=[
@m delete
@t http
@r boolean
@d Permanently deletes the guild. The current user must owner the server. This cannot be undone!
]=]
function Guild:delete()
	local data, err = self.client._api:deleteGuild(self._id)
	if data then
		local cache = self._parent._guilds
		if cache then
			cache:_delete(self._id)
		end
		return true
	else
		return false, err
	end
end

--[=[
@m kickUser
@t http
@p id User-ID-Resolvable
@op reason string
@r boolean
@d Kicks a user/member from the guild with an optional reason.
]=]
function Guild:kickUser(id, reason)
	id = Resolver.userId(id)
	local query = reason and {reason = reason}
	local data, err = self.client._api:removeGuildMember(self._id, id, query)
	if data then
		return true
	else
		return false, err
	end
end

--[=[
@m banUser
@t http
@p id User-ID-Resolvable
@op reason string
@op days number
@r boolean
@d Bans a user/member from the guild with an optional reason. The `days` parameter
is the number of days to consider when purging messages, up to 7.
]=]
function Guild:banUser(id, reason, days)
	local query = reason and {reason = reason}
	if days then
		query = query or {}
		query['delete-message-days'] = days
	end
	id = Resolver.userId(id)
	local data, err = self.client._api:createGuildBan(self._id, id, query)
	if data then
		return true
	else
		return false, err
	end
end

--[=[
@m unbanUser
@t http
@p id User-ID-Resolvable
@op reason string
@r boolean
@d Unbans a user/member from the guild with an optional reason.
]=]
function Guild:unbanUser(id, reason)
	id = Resolver.userId(id)
	local query = reason and {reason = reason}
	local data, err = self.client._api:removeGuildBan(self._id, id, query)
	if data then
		return true
	else
		return false, err
	end
end

--[=[@p shardId number The ID of the shard on which this guild is served. If only one shard is in
operation, then this will always be 0.]=]
function get.shardId(self)
	return floor(self._id / 2^22) % self.client._total_shard_count
end

--[=[@p name string The guild's name. This should be between 2 and 100 characters in length.]=]
function get.name(self)
	return self._name
end

--[=[@p icon string/nil The hash for the guild's custom icon, if one is set.]=]
function get.icon(self)
	return self._icon
end

--[=[@p iconURL string/nil The URL that can be used to view the guild's icon, if one is set.]=]
function get.iconURL(self)
	local icon = self._icon
	return icon and format('https://cdn.discordapp.com/icons/%s/%s.png', self._id, icon)
end

--[=[@p splash string/nil The hash for the guild's custom splash image, if one is set. Only partnered
guilds may have this.]=]
function get.splash(self)
	return self._splash
end

--[=[@p splashURL string/nil The URL that can be used to view the guild's custom splash image, if one is set.
Only partnered guilds may have this.]=]
function get.splashURL(self)
	local splash = self._splash
	return splash and format('https://cdn.discordapp.com/splashes/%s/%s.png', self._id, splash)
end

--[=[@p banner string/nil The hash for the guild's custom banner, if one is set.]=]
function get.banner(self)
	return self._banner
end

--[=[@p bannerURL string/nil The URL that can be used to view the guild's banner, if one is set.]=]
function get.bannerURL(self)
	local banner = self._banner
	return banner and format('https://cdn.discordapp.com/banners/%s/%s.png', self._id, banner)
end

--[=[@p large boolean Whether the guild has an arbitrarily large amount of members. Guilds that are
"large" will not initialize with all members cached.]=]
function get.large(self)
	return self._large
end

--[=[@p lazy boolean Whether the guild follows rules for the lazy-loading of client data.]=]
function get.lazy(self)
	return self._lazy
end

--[=[@p region string The voice region that is used for all voice connections in the guild.]=]
function get.region(self)
	return self._region
end

--[=[@p vanityCode string/nil The guild's vanity invite URL code, if one exists.]=]
function get.vanityCode(self)
	return self._vanity_url_code
end

--[=[@p description string/nil The guild's custom description, if one exists.]=]
function get.description(self)
	return self._description
end

--[=[@p maxMembers number/nil The guild's maximum member count, if available.]=]
function get.maxMembers(self)
	return self._max_members
end

--[=[@p maxPresences number/nil The guild's maximum presence count, if available.]=]
function get.maxPresences(self)
	return self._max_presences
end

--[=[@p mfaLevel number The guild's multi-factor (or two-factor) verification level setting. A value of
0 indicates that MFA is not required; a value of 1 indicates that MFA is
required for administrative actions.]=]
function get.mfaLevel(self)
	return self._mfa_level
end

--[=[@p joinedAt string The date and time at which the current user joined the guild, represented as
an ISO 8601 string plus microseconds when available.]=]
function get.joinedAt(self)
	return self._joined_at
end

--[=[@p afkTimeout number The guild's voice AFK timeout in seconds.]=]
function get.afkTimeout(self)
	return self._afk_timeout
end

--[=[@p unavailable boolean Whether the guild is unavailable. If the guild is unavailable, then no property
is guaranteed to exist except for this one and the guild's ID.]=]
function get.unavailable(self)
	return self._unavailable or false
end

--[=[@p totalMemberCount number The total number of members that belong to this guild. This should always be
greater than or equal to the total number of cached members.]=]
function get.totalMemberCount(self)
	return self._member_count
end

--[=[@p verificationLevel number The guild's verification level setting. See the `verificationLevel`
enumeration for a human-readable representation.]=]
function get.verificationLevel(self)
	return self._verification_level
end

--[=[@p notificationSetting number The guild's default notification setting. See the `notficationSetting`
enumeration for a human-readable representation.]=]
function get.notificationSetting(self)
	return self._default_message_notifications
end

--[=[@p explicitContentSetting number The guild's explicit content level setting. See the `explicitContentLevel`
enumeration for a human-readable representation.]=]
function get.explicitContentSetting(self)
	return self._explicit_content_filter
end

--[=[@p premiumTier number The guild's premier tier affected by nitro server
boosts. See the `premiumTier` enumeration for a human-readable representation]=]
function get.premiumTier(self)
	return self._premium_tier
end

--[=[@p premiumSubscriptionCount number The number of users that have upgraded
the guild with nitro server boosting.]=]
function get.premiumSubscriptionCount(self)
	return self._premium_subscription_count
end

--[=[@p features table Raw table of VIP features that are enabled for the guild.]=]
function get.features(self)
	return self._features
end

--[=[@p me Member/nil Equivalent to `Guild.members:get(Guild.client.user.id)`.]=]
function get.me(self)
	return self._members:get(self.client._user._id)
end

--[=[@p owner Member/nil Equivalent to `Guild.members:get(Guild.ownerId)`.]=]
function get.owner(self)
	return self._members:get(self._owner_id)
end

--[=[@p ownerId string The Snowflake ID of the guild member that owns the guild.]=]
function get.ownerId(self)
	return self._owner_id
end

--[=[@p afkChannelId string/nil The Snowflake ID of the channel that is used for AFK members, if one is set.]=]
function get.afkChannelId(self)
	return self._afk_channel_id
end

--[=[@p afkChannel GuildVoiceChannel/nil Equivalent to `Guild.voiceChannels:get(Guild.afkChannelId)`.]=]
function get.afkChannel(self)
	return self._voice_channels:get(self._afk_channel_id)
end

--[=[@p systemChannelId string/nil The channel id where Discord's join messages will be displayed.]=]
function get.systemChannelId(self)
	return self._system_channel_id
end

--[=[@p systemChannel GuildTextChannel/nil The channel where Discord's join messages will be displayed.]=]
function get.systemChannel(self)
	return self._text_channels:get(self._system_channel_id)
end

--[=[@p defaultRole Role Equivalent to `Guild.roles:get(Guild.id)`.]=]
function get.defaultRole(self)
	return self._roles:get(self._id)
end

--[=[@p connection VoiceConnection/nil The VoiceConnection for this guild if one exists.]=]
function get.connection(self)
	return self._connection
end

--[=[@p roles Cache An iterable cache of all roles that exist in this guild. This includes the
default everyone role.]=]
function get.roles(self)
	return self._roles
end

--[=[@p emojis Cache An iterable cache of all emojis that exist in this guild. Note that standard
unicode emojis are not found here; only custom emojis.]=]
function get.emojis(self)
	return self._emojis
end

--[=[@p members Cache An iterable cache of all members that exist in this guild and have been
already loaded. If the `cacheAllMembers` client option (and the `syncGuilds`
option for user-accounts) is enabled on start-up, then all members will be
cached. Otherwise, offline members may not be cached. To access a member that
may exist, but is not cached, use `Guild:getMember`.]=]
function get.members(self)
	return self._members
end

--[=[@p textChannels Cache An iterable cache of all text channels that exist in this guild.]=]
function get.textChannels(self)
	return self._text_channels
end

--[=[@p voiceChannels Cache An iterable cache of all voice channels that exist in this guild.]=]
function get.voiceChannels(self)
	return self._voice_channels
end

--[=[@p categories Cache An iterable cache of all channel categories that exist in this guild.]=]
function get.categories(self)
	return self._categories
end

return Guild
