structures/Guild.js

import {
  AUDIT_LOG_TYPES,
  CDN_BASE_URL,
  NAME,
  PERMISSIONS,
  TO_JSON_TYPES_ENUM,
} from "../constants.js";
import GuildChannelsManager from "../managers/GuildChannelsManager.js";
import GuildEmojisManager from "../managers/GuildEmojisManager.js";
import GuildInviteManager from "../managers/GuildInviteManager.js";
import GuildMemberManager from "../managers/GuildMemberManager.js";
import GuildRoleManager from "../managers/GuildRoleManager.js";
import GuildScheduledEventManager from "../managers/GuildScheduledEventManager.js";
import GuildVoiceStatesManager from "../managers/GuildVoiceStatesManager.js";
import cacheChannel from "../util/gluon/cacheChannel.js";
import checkPermission from "../util/discord/checkPermission.js";
import AuditLog from "./AuditLog.js";
import Emoji from "./Emoji.js";
import Invite from "./Invite.js";
import Member from "./Member.js";
import Role from "./Role.js";
import Thread from "./Thread.js";
import VoiceState from "./VoiceState.js";
import GuildCacheOptions from "../managers/GuildCacheOptions.js";
import Channel from "./Channel.js";
import GluonCacheOptions from "../managers/GluonCacheOptions.js";
import util from "util";
import Client from "../Client.js";
import Message from "./Message.js";

/**
 * Represents a Discord guild.
 * @see {@link https://discord.com/developers/docs/resources/guild}
 */
class Guild {
  #_client;
  #_id;
  #unavailable;
  #name;
  #description;
  #_icon;
  #_owner_id;
  #joined_at;
  #member_count;
  #system_channel_id;
  #rules_channel_id;
  #preferred_locale;
  #_attributes;
  #premium_subscription_count;
  #_cacheOptions;
  #members;
  #channels;
  #voice_states;
  #roles;
  #emojis;
  #invites;
  #scheduled_events;
  /**
   * Creates the structure for a guild.
   * @param {Client} client The client instance.
   * @param {Object} data Raw guild data.
   * @param {Object?} options The additional options for this structure.
   * @param {Boolean?} options.nocache Whether this guild should be cached or not.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object}
   */
  constructor(client, data, { nocache = false } = { nocache: false }) {
    if (!(client instanceof Client))
      throw new TypeError("GLUON: Client must be an instance of Client");
    if (typeof data !== "object")
      throw new TypeError("GLUON: Data must be an object");
    if (typeof nocache !== "boolean")
      throw new TypeError("GLUON: No cache must be a boolean");

    /**
     * The client instance.
     * @type {Client}
     * @private
     */
    this.#_client = client;

    /**
     * The id of the guild.
     * @type {BigInt}
     * @private
     */
    this.#_id = BigInt(data.id);

    if (data.unavailable == true) {
      this.#unavailable = true;

      if (nocache === false && Guild.shouldCache(this.#_client._cacheOptions))
        this.#_client.guilds.set(data.id, this);
      return;
    }

    const existing = this.#_client.guilds.get(data.id) || null;

    // needed for join/leave logging
    /**
     * The name of the guild.
     * @type {String}
     * @private
     */
    this.#name = data.name;
    if (this.#name === undefined && existing && existing.name)
      this.#name = existing.name;
    else if (!this.#name) this.#name = null;

    /**
     * The description of the guild.
     * @type {String?}
     * @private
     */
    this.#description = data.description;
    if (this.#description === undefined && existing && existing.description)
      this.#description = existing.description;
    else if (!this.#description) this.#description = null;

    /**
     * The guild icon hash.
     * @type {BigInt?}
     * @private
     */
    if (data.icon !== undefined)
      this.#_icon = data.icon
        ? BigInt(`0x${data.icon.replace("a_", "")}`)
        : null;
    else if (data.icon === undefined && existing && existing._icon)
      this.#_icon = existing._icon;

    /**
     * The id of the guild owner.
     * @type {BigInt}
     * @private
     */
    this.#_owner_id = BigInt(data.owner_id);

    if (data.joined_at)
      /**
       * UNIX (seconds) timestamp for when the bot user was added to this guild.
       * @type {Number?}
       * @private
       */
      this.#joined_at = (new Date(data.joined_at).getTime() / 1000) | 0;
    else if (existing?.joinedAt) this.#joined_at = existing.joinedAt;

    if (data.member_count)
      /**
       * The member count of this guild.
       * @type {Number}
       * @private
       */
      this.#member_count = data.member_count;
    else if (existing?.memberCount) this.#member_count = existing.member_count;
    else this.#member_count = 2;

    /**
     * The voice state manager of this guild.
     * @type {GuildVoiceStatesManager}
     * @private
     */
    this.#voice_states = existing
      ? existing.voiceStates
      : new GuildVoiceStatesManager(this.#_client, data.voice_states);

    /**
     * The member manager of this guild.
     * @type {GuildMemberManager}
     * @private
     */
    this.#members = existing
      ? existing.members
      : new GuildMemberManager(this.#_client, this);

    /**
     * The channel manager of this guild.
     * @type {GuildChannelsManager}
     * @private
     */
    this.#channels = existing
      ? existing.channels
      : new GuildChannelsManager(this.#_client, this);

    /**
     * The role manager of this guild.
     * @type {GuildRoleManager}
     * @private
     */
    this.#roles = existing
      ? existing.roles
      : new GuildRoleManager(this.#_client, this);

    this.#scheduled_events = existing
      ? existing.scheduledEvents
      : new GuildScheduledEventManager(this.#_client, this);

    /**
     * The emoji manager of this guild.
     * @type {GuildEmojisManager}
     * @private
     */
    this.#emojis = existing
      ? existing.emojis
      : new GuildEmojisManager(this.#_client, this);

    /**
     * The invite manager of this guild.
     * @type {GuildInviteManager}
     * @private
     */
    this.#invites = existing
      ? existing.invites
      : new GuildInviteManager(this.#_client, this);

    /**
     * The system channel id of the guild.
     * @type {BigInt}
     * @private
     */
    if (data.system_channel_id !== undefined)
      this.#system_channel_id = data.system_channel_id
        ? BigInt(data.system_channel_id)
        : null;
    else if (
      data.system_channel_id === undefined &&
      existing &&
      existing.systemChannelId
    )
      this.#system_channel_id = BigInt(existing.systemChannelId);

    /**
     * The rules channel id of the guild.
     * @type {BigInt}
     * @private
     */
    if (data.rules_channel_id !== undefined)
      this.#rules_channel_id = data.rules_channel_id
        ? BigInt(data.rules_channel_id)
        : null;
    else if (
      data.rules_channel_id === undefined &&
      existing &&
      existing.rulesChannelId
    )
      this.#rules_channel_id = BigInt(existing.rulesChannelId);

    /**
     * The premium subscription count of the guild.
     * @type {Number}
     * @private
     */
    if (typeof data.premium_subscription_count == "number")
      this.#premium_subscription_count = data.premium_subscription_count;
    else if (
      typeof data.premium_subscription_count != "number" &&
      existing &&
      existing.premiumSubscriptionCount
    )
      this.#premium_subscription_count = existing.premiumSubscriptionCount;
    else this.#premium_subscription_count = 0;

    /**
     * The attributes of the guild.
     * @type {Number}
     * @private
     */
    this.#_attributes = data._attributes ?? data.system_channel_flags;

    if (
      typeof data.mfa_level == "number" ||
      (existing && typeof existing.mfaLevel == "number")
    ) {
      const mfaLevel =
        typeof data.mfa_level == "number" ? data.mfa_level : existing.mfaLevel;
      switch (mfaLevel) {
        case 0:
          // none
          this.#_attributes |= 0b1 << 6;
          break;
        case 1:
          // elevated
          this.#_attributes |= 0b1 << 7;
          break;
        default:
          break;
      }
    }

    if (
      typeof data.verification_level == "number" ||
      (existing && typeof existing.verificationLevel == "number")
    ) {
      const verificationLevel =
        typeof data.verification_level == "number"
          ? data.verification_level
          : existing.verificationLevel;
      switch (verificationLevel) {
        case 0:
          // none
          this.#_attributes |= 0b1 << 8;
          break;
        case 1:
          // low
          this.#_attributes |= 0b1 << 9;
          break;
        case 2:
          // medium
          this.#_attributes |= 0b1 << 10;
          break;
        case 3:
          // high
          this.#_attributes |= 0b1 << 11;
          break;
        case 4:
          // very high
          this.#_attributes |= 0b1 << 12;
          break;
        default:
          break;
      }
    }

    if (
      typeof data.default_message_notifications == "number" ||
      (existing && typeof existing.defaultMessageNotifications == "number")
    ) {
      const defaultMessageNotifications =
        typeof data.default_message_notifications == "number"
          ? data.default_message_notifications
          : existing.defaultMessageNotifications;
      switch (defaultMessageNotifications) {
        case 0:
          // all messages
          this.#_attributes |= 0b1 << 13;
          break;
        case 1:
          // only mentions
          this.#_attributes |= 0b1 << 14;
          break;
        default:
          break;
      }
    }

    if (
      typeof data.explicit_content_filter == "number" ||
      (existing && typeof existing.explicitContentFilter == "number")
    ) {
      const explicitContentFilter =
        typeof data.explicit_content_filter == "number"
          ? data.explicit_content_filter
          : existing.explicitContentFilter;
      switch (explicitContentFilter) {
        case 0:
          // disabled
          this.#_attributes |= 0b1 << 15;
          break;
        case 1:
          // members without roles
          this.#_attributes |= 0b1 << 16;
          break;
        case 2:
          // all members
          this.#_attributes |= 0b1 << 17;
          break;
        default:
          break;
      }
    }

    if (
      typeof data.nsfw_level == "number" ||
      (existing && typeof existing.nsfwLevel == "number")
    ) {
      const nsfwLevel =
        typeof data.nsfw_level == "number"
          ? data.nsfw_level
          : existing.nsfwLevel;
      switch (nsfwLevel) {
        case 0:
          // default
          this.#_attributes |= 0b1 << 18;
          break;
        case 1:
          // explicit
          this.#_attributes |= 0b1 << 19;
          break;
        case 2:
          // safe
          this.#_attributes |= 0b1 << 20;
          break;
        case 3:
          // age restricted
          this.#_attributes |= 0b1 << 21;
          break;
        default:
          break;
      }
    }

    if (
      (data && typeof data.premium_tier == "number") ||
      (existing && typeof existing.premiumTier == "number")
    ) {
      const premiumTier =
        typeof data.premium_tier == "number"
          ? data.premium_tier
          : existing.premiumTier;
      switch (premiumTier) {
        case 0:
          // none
          this.#_attributes |= 0b1 << 22;
          break;
        case 1:
          // tier 1
          this.#_attributes |= 0b1 << 23;
          break;
        case 2:
          // tier 2
          this.#_attributes |= 0b1 << 24;
          break;
        case 3:
          // tier 3
          this.#_attributes |= 0b1 << 25;
          break;
        default:
          break;
      }
    }

    if (
      typeof data.premium_progress_bar_enabled == "boolean" &&
      data.premium_progress_bar_enabled == true
    )
      this.#_attributes |= 0b1 << 26;
    else if (
      existing &&
      typeof existing.premiumProgressBarEnabled == "boolean" &&
      existing.premiumProgressBarEnabled == true
    )
      this.#_attributes |= 0b1 << 26;

    /**
     * The locale of this guild, if set up as a community.
     * @type {String}
     * @private
     */
    this.#preferred_locale = data.preferred_locale;
    if (!this.#preferred_locale && existing && existing.preferredLocale)
      this.#preferred_locale = existing.preferredLocale;
    else if (!this.#preferred_locale) this.#preferred_locale = null;

    /**
     * The cache options for this guild.
     * @type {GuildCacheOptions}
     * @private
     */
    this.#_cacheOptions = new GuildCacheOptions(
      data._cacheOptions || this.#_client._defaultGuildCacheOptions.toJSON(),
    );

    if (nocache === false && Guild.shouldCache(this.#_client._cacheOptions))
      this.#_client.guilds.set(data.id, this);

    if (
      data.members &&
      Member.shouldCache(this.#_client._cacheOptions, this._cacheOptions) ===
        true
    )
      for (let i = 0; i < data.members.length; i++)
        new Member(this.#_client, data.members[i], {
          userId: data.members[i].user.id,
          guildId: data.id,
          user: data.members[i].user,
          nocache,
        });

    if (
      data.channels &&
      Channel.shouldCache(this.#_client._cacheOptions, this._cacheOptions) ===
        true
    )
      for (let i = 0; i < data.channels.length; i++)
        cacheChannel(this.#_client, data.channels[i], data.id, nocache);

    if (
      data.threads &&
      Thread.shouldCache(this.#_client._cacheOptions, this._cacheOptions) ===
        true
    )
      for (let i = 0; i < data.threads.length; i++)
        new Thread(this.#_client, data.threads[i], {
          guildId: data.id,
          nocache,
        });

    if (
      data.voice_states &&
      VoiceState.shouldCache(
        this.#_client._cacheOptions,
        this._cacheOptions,
      ) === true
    )
      for (let i = 0; i < data.voice_states.length; i++)
        new VoiceState(this.#_client, data.voice_states[i], {
          guildId: data.id,
          nocache,
        });

    if (
      data.roles &&
      Role.shouldCache(this.#_client._cacheOptions, this._cacheOptions) === true
    )
      for (let i = 0; i < data.roles.length; i++)
        new Role(this.#_client, data.roles[i], { guildId: data.id, nocache });

    if (
      data.emojis &&
      Emoji.shouldCache(this.#_client._cacheOptions, this._cacheOptions) ===
        true
    )
      for (let i = 0; i < data.emojis.length; i++)
        new Emoji(this.#_client, data.emojis[i], {
          guildId: data.id,
          nocache,
        });

    if (
      data.invites &&
      Invite.shouldCache(this.#_client._cacheOptions, this._cacheOptions) ===
        true
    )
      for (let i = 0; i < data.invites.length; i++)
        new Invite(this.#_client, data.invites[i], {
          guildId: data.id,
          nocache,
        });
  }

  /**
   * The id of the guild.
   * @type {String}
   * @readonly
   * @public
   */
  get id() {
    return String(this.#_id);
  }

  /**
   * The hash of the guild's icon, as it was received from Discord.
   * @readonly
   * @type {String?}
   * @private
   */
  get #_originalIconHash() {
    return this.#_icon
      ? // eslint-disable-next-line quotes
        `${this.#_formattedIconHash}`
      : null;
  }

  /**
   * The hash of the guild icon as a string.
   * @readonly
   * @type {String}
   * @private
   */
  get #_formattedIconHash() {
    if (!this.#_icon) return null;

    let formattedHash = this.#_icon.toString(16);

    while (formattedHash.length != 32)
      // eslint-disable-next-line quotes
      formattedHash = `0${formattedHash}`;

    return formattedHash;
  }

  /**
   * The icon URL of the guild.
   * @readonly
   * @type {String?}
   * @public
   */
  get displayIconURL() {
    return Guild.getIcon(this.id, this.#_originalIconHash);
  }

  /**
   * The owner of the guild.
   * @type {Member}
   * @readonly
   * @public
   */
  get owner() {
    return this.members.get(this.ownerId);
  }

  /**
   * System channel flags.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-system-channel-flags}
   * @readonly
   * @type {String[]}
   * @public
   */
  get systemChannelFlags() {
    const flags = [];

    if ((this.#_attributes & (0b1 << 0)) == 0b1 << 0)
      flags.push("SUPPRESS_JOIN_NOTIFICATIONS");
    if ((this.#_attributes & (0b1 << 1)) == 0b1 << 1)
      flags.push("SUPPRESS_PREMIUM_SUBSCRIPTIONS");
    if ((this.#_attributes & (0b1 << 2)) == 0b1 << 2)
      flags.push("SUPPRESS_GUILD_REMINDER_NOTIFICATIONS");
    if ((this.#_attributes & (0b1 << 3)) == 0b1 << 3)
      flags.push("SUPPRESS_JOIN_NOTIFICATION_REPLIES");
    if ((this.#_attributes & (0b1 << 4)) == 0b1 << 4)
      flags.push("SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATIONS");
    if ((this.#_attributes & (0b1 << 5)) == 0b1 << 5)
      flags.push("SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATION_REPLIES");

    return flags;
  }

  /**
   * Raw system channel flags.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-system-channel-flags}
   * @readonly
   * @type {Number}
   * @public
   */
  get rawSystemChannelFlags() {
    let rawFlags = 0;

    if ((this.#_attributes & (0b1 << 0)) == 0b1 << 0) rawFlags |= 0b1 << 0;
    if ((this.#_attributes & (0b1 << 1)) == 0b1 << 1) rawFlags |= 0b1 << 1;
    if ((this.#_attributes & (0b1 << 2)) == 0b1 << 2) rawFlags |= 0b1 << 2;
    if ((this.#_attributes & (0b1 << 3)) == 0b1 << 3) rawFlags |= 0b1 << 3;
    if ((this.#_attributes & (0b1 << 4)) == 0b1 << 4) rawFlags |= 0b1 << 4;
    if ((this.#_attributes & (0b1 << 5)) == 0b1 << 5) rawFlags |= 0b1 << 5;

    return rawFlags;
  }

  /**
   * Server MFA level.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-mfa-level}
   * @readonly
   * @type {String}
   * @public
   */
  get mfaLevel() {
    if ((this.#_attributes & (0b1 << 6)) == 0b1 << 6) return "NONE";
    else if ((this.#_attributes & (0b1 << 7)) == 0b1 << 7) return "ELEVATED";
    else return null;
  }

  /**
   * Server MFA level.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-mfa-level}
   * @readonly
   * @type {Number}
   * @public
   */
  get rawMfaLevel() {
    if ((this.#_attributes & (0b1 << 6)) == 0b1 << 6) return 0;
    else if ((this.#_attributes & (0b1 << 7)) == 0b1 << 7) return 1;
    else return null;
  }

  /**
   * Server verification level.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-verification-level}
   * @readonly
   * @type {String}
   * @public
   */
  get verificationLevel() {
    if ((this.#_attributes & (0b1 << 8)) == 0b1 << 8) return "NONE";
    else if ((this.#_attributes & (0b1 << 9)) == 0b1 << 9) return "LOW";
    else if ((this.#_attributes & (0b1 << 10)) == 0b1 << 10) return "MEDIUM";
    else if ((this.#_attributes & (0b1 << 11)) == 0b1 << 11) return "HIGH";
    else if ((this.#_attributes & (0b1 << 12)) == 0b1 << 12) return "VERY_HIGH";
    else return null;
  }

  /**
   * Server verification level.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-verification-level}
   * @readonly
   * @type {Number}
   * @public
   */
  get rawVerificationLevel() {
    if ((this.#_attributes & (0b1 << 8)) == 0b1 << 8) return 0;
    else if ((this.#_attributes & (0b1 << 9)) == 0b1 << 9) return 1;
    else if ((this.#_attributes & (0b1 << 10)) == 0b1 << 10) return 2;
    else if ((this.#_attributes & (0b1 << 11)) == 0b1 << 11) return 3;
    else if ((this.#_attributes & (0b1 << 12)) == 0b1 << 12) return 4;
    else return null;
  }

  /**
   * Default notification setting.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-default-message-notification-level}
   * @readonly
   * @type {String}
   * @public
   */
  get defaultMessageNotifications() {
    if ((this.#_attributes & (0b1 << 13)) == 0b1 << 13) return "ALL_MESSAGES";
    else if ((this.#_attributes & (0b1 << 14)) == 0b1 << 14)
      return "ONLY_MENTIONS";
    else return null;
  }

  /**
   * Default notification setting.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-default-message-notification-level}
   * @readonly
   * @type {Number}
   * @public
   */
  get rawDefaultMessageNotifications() {
    if ((this.#_attributes & (0b1 << 13)) == 0b1 << 13) return 0;
    else if ((this.#_attributes & (0b1 << 14)) == 0b1 << 14) return 1;
    else return null;
  }

  /**
   * Explicit content filter level.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-explicit-content-filter-level}
   * @readonly
   * @type {String}
   * @public
   */
  get explicitContentFilter() {
    if ((this.#_attributes & (0b1 << 15)) == 0b1 << 15) return "DISABLED";
    else if ((this.#_attributes & (0b1 << 16)) == 0b1 << 16)
      return "MEMBERS_WITHOUT_ROLES";
    else if ((this.#_attributes & (0b1 << 17)) == 0b1 << 17)
      return "ALL_MEMBERS";
    else return null;
  }

  /**
   * Explicit content filter level.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-explicit-content-filter-level}
   * @readonly
   * @type {Number}
   * @public
   */
  get rawExplicitContentFilter() {
    if ((this.#_attributes & (0b1 << 15)) == 0b1 << 15) return 0;
    else if ((this.#_attributes & (0b1 << 16)) == 0b1 << 16) return 1;
    else if ((this.#_attributes & (0b1 << 17)) == 0b1 << 17) return 2;
    else return null;
  }

  /**
   * Server NSFW level.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-guild-nsfw-level}
   * @readonly
   * @type {String}
   * @public
   */
  get nsfwLevel() {
    if ((this.#_attributes & (0b1 << 18)) == 0b1 << 18) return "DEFAULT";
    else if ((this.#_attributes & (0b1 << 19)) == 0b1 << 19) return "EXPLICIT";
    else if ((this.#_attributes & (0b1 << 20)) == 0b1 << 20) return "SAFE";
    else if ((this.#_attributes & (0b1 << 21)) == 0b1 << 21)
      return "AGE_RESTRICTED";
    else return null;
  }

  /**
   * Server NSFW level.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-guild-nsfw-level}
   * @readonly
   * @type {Number}
   * @public
   */
  get rawNsfwLevel() {
    if ((this.#_attributes & (0b1 << 18)) == 0b1 << 18) return 0;
    else if ((this.#_attributes & (0b1 << 19)) == 0b1 << 19) return 1;
    else if ((this.#_attributes & (0b1 << 20)) == 0b1 << 20) return 2;
    else if ((this.#_attributes & (0b1 << 21)) == 0b1 << 21) return 3;
    else return null;
  }

  /**
   * Server boost level.
   * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-premium-tier}
   * @readonly
   * @type {Number}
   * @public
   */
  get premiumTier() {
    if ((this.#_attributes & (0b1 << 22)) == 0b1 << 22) return 0;
    else if ((this.#_attributes & (0b1 << 23)) == 0b1 << 23) return 1;
    else if ((this.#_attributes & (0b1 << 24)) == 0b1 << 24) return 2;
    else if ((this.#_attributes & (0b1 << 25)) == 0b1 << 25) return 3;
    else return null;
  }

  /**
   * Whether the guild has the boost progress bar enabled.
   * @readonly
   * @type {Boolean}
   * @public
   */
  get premiumProgressBarEnabled() {
    return (this.#_attributes & (0b1 << 26)) == 0b1 << 26;
  }

  /**
   * Whether the guild is unavailable.
   * @type {Boolean}
   * @readonly
   * @public
   */
  get unavailable() {
    return this.#unavailable ?? false;
  }

  /**
   * The name of the guild.
   * @type {String}
   * @readonly
   * @public
   */
  get name() {
    return this.#name;
  }

  /**
   * The description of the guild.
   * @type {String?}
   * @readonly
   * @public
   */
  get description() {
    return this.#description;
  }

  /**
   * The icon hash of the guild.
   * @type {String}
   * @readonly
   * @public
   */
  get ownerId() {
    return String(this.#_owner_id);
  }

  /**
   * The id of the guild owner.
   * @type {Number}
   * @readonly
   * @public
   */
  get joinedAt() {
    return this.#joined_at;
  }

  /**
   * The member count of the guild.
   * @type {Number}
   * @readonly
   * @public
   */
  get memberCount() {
    return this.#member_count;
  }

  /**
   * The system channel id of the guild.
   * @type {String?}
   * @readonly
   * @public
   */
  get systemChannelId() {
    return this.#system_channel_id ? String(this.#system_channel_id) : null;
  }

  /**
   * The system channel of the guild.
   * @type {TextChannel?}
   * @readonly
   * @public
   */
  get systemChannel() {
    return this.systemChannelId
      ? this.channels.get(this.systemChannelId)
      : null;
  }

  /**
   * The rules channel id of the guild.
   * @type {String?}
   * @readonly
   * @public
   */
  get rulesChannelId() {
    return this.#rules_channel_id ? String(this.#rules_channel_id) : null;
  }

  /**
   * The rules channel of the guild.
   * @type {TextChannel?}
   * @readonly
   * @public
   */
  get rulesChannel() {
    return this.rulesChannelId ? this.channels.get(this.rulesChannelId) : null;
  }

  /**
   * The preferred locale of the guild.
   * @type {String}
   * @readonly
   * @public
   */
  get preferredLocale() {
    return this.#preferred_locale;
  }

  /**
   * The premium subscription count of the guild.
   * @type {Number}
   * @readonly
   * @public
   */
  get premiumSubscriptionCount() {
    return this.#premium_subscription_count;
  }

  /**
   * The cache options for this guild.
   * @type {GuildCacheOptions}
   * @readonly
   * @public
   */
  get _cacheOptions() {
    return this.#_cacheOptions;
  }

  /**
   * The members in the guild.
   * @type {GuildMemberManager}
   * @readonly
   * @public
   */
  get members() {
    return this.#members;
  }

  /**
   * The channels in the guild.
   * @type {GuildChannelsManager}
   * @readonly
   * @public
   */
  get channels() {
    return this.#channels;
  }

  /**
   * The voice states in the guild.
   * @type {GuildVoiceStatesManager}
   * @readonly
   * @public
   */
  get voiceStates() {
    return this.#voice_states;
  }

  /**
   * The roles in the guild.
   * @type {GuildRoleManager}
   * @readonly
   * @public
   */
  get roles() {
    return this.#roles;
  }

  /**
   * The scheduled events in the guild.
   * @type {GuildScheduledEventManager}
   * @readonly
   * @public
   */
  get scheduledEvents() {
    return this.#scheduled_events;
  }

  /**
   * The emojis in the guild.
   * @type {GuildEmojisManager}
   * @readonly
   * @public
   */
  get emojis() {
    return this.#emojis;
  }

  /**
   * The invites in the guild.
   * @type {GuildInviteManager}
   * @readonly
   * @public
   */
  get invites() {
    return this.#invites;
  }

  /**
   * Increases the member count of the guild.
   * @method
   * @public
   */
  _incrementMemberCount() {
    this.#member_count++;
  }

  /**
   * Decreases the member count of the guild.
   * @method
   * @public
   */
  _decrementMemberCount() {
    this.#member_count--;
  }

  /**
   * Returns the client member for this guild.
   * @returns {Promise<Member>}
   * @public
   * @async
   * @method
   * @throws {Error}
   */
  me() {
    const cached = this.members.get(this.#_client.user.id);

    if (cached) return cached;

    return this.members.fetch(this.#_client.user.id);
  }

  /**
   * Bans a user with the given id from the guild.
   * @param {String} user_id The id of the user to ban.
   * @param {Object?} options Ban options.
   * @returns {Promise<void?>}
   * @async
   * @public
   * @method
   * @throws {Error | TypeError}
   */
  async ban(user_id, { reason, seconds } = {}) {
    if (
      !checkPermission((await this.me()).permissions, PERMISSIONS.BAN_MEMBERS)
    )
      throw new Error("MISSING PERMISSIONS: BAN_MEMBERS");

    if (typeof user_id !== "string")
      throw new TypeError("GLUON: INVALID_TYPE: user_id");

    if (typeof reason !== "undefined" && typeof reason !== "string")
      throw new TypeError("GLUON: INVALID_TYPE: reason");

    if (typeof reason === "string" && reason.length > 512)
      throw new RangeError("GLUON: VALUE_OUT_OF_RANGE: reason");

    if (typeof seconds !== "undefined" && typeof seconds !== "number")
      throw new TypeError("GLUON: INVALID_TYPE: seconds");

    if (typeof seconds === "number" && (seconds < 0 || seconds > 604800))
      throw new RangeError("GLUON: VALUE_OUT_OF_RANGE: seconds");

    const body = {};

    if (reason) body["X-Audit-Log-Reason"] = reason;
    // number of seconds to delete messages for (0-604800)
    if (seconds) body.delete_message_seconds = seconds;

    await this.#_client.request.makeRequest(
      "putCreateGuildBan",
      [this.id, user_id],
      body,
    );
  }

  /**
   * Unbans a user with the given id from the guild.
   * @param {String} user_id The id of the user to unban.
   * @param {Object?} options Unban options.
   * @returns {Promise<void?>}
   * @async
   * @public
   * @method
   * @throws {Error | TypeError}
   */
  async unban(user_id, { reason } = {}) {
    if (
      !checkPermission((await this.me()).permissions, PERMISSIONS.BAN_MEMBERS)
    )
      throw new Error("MISSING PERMISSIONS: BAN_MEMBERS");

    if (typeof user_id !== "string")
      throw new TypeError("GLUON: INVALID_TYPE: user_id");

    if (typeof reason !== "undefined" && typeof reason !== "string")
      throw new TypeError("GLUON: INVALID_TYPE: reason");

    if (typeof reason === "string" && reason.length > 512)
      throw new RangeError("GLUON: VALUE_OUT_OF_RANGE: reason");

    const body = {};

    if (reason) body["X-Audit-Log-Reason"] = reason;

    await this.#_client.request.makeRequest(
      "deleteRemoveGuildBan",
      [this.id, user_id],
      body,
    );
  }

  /**
   * Kicks a user with the given id from the guild.
   * @param {String} user_id The id of the user to kick.
   * @param {Object?} options Kick options.
   * @returns {Promise<void?>}
   * @async
   * @public
   * @method
   * @throws {Error | TypeError}
   */
  async kick(user_id, { reason } = {}) {
    if (
      !checkPermission((await this.me()).permissions, PERMISSIONS.KICK_MEMBERS)
    )
      throw new Error("MISSING PERMISSIONS: KICK_MEMBERS");

    if (typeof user_id !== "string")
      throw new TypeError("GLUON: INVALID_TYPE: user_id");

    if (typeof reason !== "undefined" && typeof reason !== "string")
      throw new TypeError("GLUON: INVALID_TYPE: reason");

    if (typeof reason === "string" && reason.length > 512)
      throw new RangeError("GLUON: VALUE_OUT_OF_RANGE: reason");

    const body = {};

    if (reason) body["X-Audit-Log-Reason"] = reason;

    await this.#_client.request.makeRequest(
      "deleteGuildMember",
      [this.id, user_id],
      body,
    );
  }

  /**
   * Removes the given role from the given member.
   * @param {String} user_id The id of the user.
   * @param {String} role_id The id of the role.
   * @param {Object?} options Remove role options.
   * @returns {Promise<void?>}
   * @async
   * @public
   * @method
   * @throws {Error | TypeError}
   */
  async removeMemberRole(user_id, role_id, { reason } = {}) {
    if (
      !checkPermission((await this.me()).permissions, PERMISSIONS.MANAGE_ROLES)
    )
      throw new Error("MISSING PERMISSIONS: MANAGE_ROLES");

    if (typeof user_id !== "string")
      throw new TypeError("GLUON: INVALID_TYPE: user_id");

    if (typeof role_id !== "string")
      throw new TypeError("GLUON: INVALID_TYPE: role_id");

    if (typeof reason !== "undefined" && typeof reason !== "string")
      throw new TypeError("GLUON: INVALID_TYPE: reason");

    if (typeof reason === "string" && reason.length > 512)
      throw new RangeError("GLUON: VALUE_OUT_OF_RANGE: reason");

    const body = {};

    if (reason) body["X-Audit-Log-Reason"] = reason;

    await this.#_client.request.makeRequest(
      "deleteRemoveMemberRole",
      [this.id, user_id, role_id],
      body,
    );
  }

  /**
   * Fetches audit logs.
   * @param {Object?} options Audit log fetch options.
   * @returns {Promise<AuditLog[]?>}
   * @async
   * @public
   * @method
   * @throws {Error | TypeError}
   */
  async fetchAuditLogs({ limit, type, user_id, before, after } = {}) {
    if (
      !checkPermission(
        (await this.me()).permissions,
        PERMISSIONS.VIEW_AUDIT_LOG,
      )
    )
      throw new Error("MISSING PERMISSIONS: VIEW_AUDIT_LOG");

    if (typeof limit !== "undefined" && typeof limit !== "number")
      throw new TypeError("GLUON: INVALID_TYPE: limit");

    if (typeof limit === "number" && (limit < 1 || limit > 100))
      throw new RangeError("GLUON: VALUE_OUT_OF_RANGE: limit");

    if (typeof type !== "undefined" && typeof type !== "number")
      throw new TypeError("GLUON: INVALID_TYPE: type");

    if (typeof user_id !== "undefined" && typeof user_id !== "string")
      throw new TypeError("GLUON: INVALID_TYPE: user_id");

    if (typeof before !== "undefined" && typeof before !== "string")
      throw new TypeError("GLUON: INVALID_TYPE: before");

    if (typeof after !== "undefined" && typeof after !== "string")
      throw new TypeError("GLUON: INVALID_TYPE: after");

    const body = {};

    if (limit) body.limit = limit;
    else body.limit = 1;

    if (type) body.action_type = AUDIT_LOG_TYPES[type];

    if (user_id) body.user_id = user_id;

    if (before) body.before = before;

    if (after) body.after = after;

    const data = await this.#_client.request.makeRequest(
      "getGuildAuditLog",
      [this.id],
      body,
    );

    if (
      type &&
      AUDIT_LOG_TYPES[type] &&
      data &&
      data.audit_log_entries[0] &&
      data.audit_log_entries[0].action_type != AUDIT_LOG_TYPES[type]
    )
      return null;

    if (!data || data.audit_log_entries.length == 0) return null;

    return data.audit_log_entries.map(
      (e) =>
        new AuditLog(this.#_client, e, {
          users: data.users,
          guildId: this.id,
        }),
    );
  }

  /**
   * Fetches the guild invites.
   * @returns {Promise<Object[]?>}
   * @async
   * @public
   * @method
   * @throws {Error}
   */
  async fetchInvites() {
    if (
      !checkPermission((await this.me()).permissions, PERMISSIONS.MANAGE_GUILD)
    )
      throw new Error("MISSING PERMISSIONS: MANAGE_GUILD");

    return this.#_client.request.makeRequest("getGuildInvites", [this.id]);
  }

  /**
   * Fetches all the guild channels.
   * @returns {Promise<Array<TextChannel | VoiceState>>}
   * @async
   * @public
   * @method
   * @throws {Error}
   */
  async fetchChannels() {
    const data = await this.#_client.request.makeRequest("getGuildChannels", [
      this.id,
    ]);

    const channels = [];
    for (let i = 0; i < data.length; i++)
      channels.push(cacheChannel(this.#_client, data[i], this.id));

    return channels;
  }

  /**
   * Fetches the ban for the provided user id.
   * @param {String} user_id The id of the user to fetch the ban of.
   * @returns {Promise<Object?>}
   * @async
   * @public
   * @method
   * @throws {Error | TypeError}
   */
  async fetchBan(user_id) {
    if (
      !checkPermission((await this.me()).permissions, PERMISSIONS.BAN_MEMBERS)
    )
      throw new Error("MISSING PERMISSIONS: BAN_MEMBERS");

    if (typeof user_id !== "string")
      throw new TypeError("GLUON: INVALID_TYPE: user_id");

    return this.#_client.request.makeRequest("getGuildBan", [this.id, user_id]);
  }

  /**
   * Leaves the guild.
   * @returns {Promise<void?>}
   * @async
   * @public
   * @method
   * @throws {Error}
   */
  async leave() {
    await this.#_client.request.makeRequest("deleteLeaveGuild", [this.id]);
  }

  /**
   * Calculates the number of messages that should be cached per channel for this guild.
   * @returns {Number}
   * @public
   * @method
   */
  calculateMessageCacheCount() {
    const x = (this.memberCount < 500000 ? this.memberCount : 499999) / 500000;
    /* creates an "S-Curve" for how many messages should be cached */
    /* more members => assume more activity => therefore more messages to be cached */
    /* minimum of 50 messages to be cached, and a maximum of 1000 */
    /* having greater than 500000 members has no effect */
    const shouldCacheCount =
      Math.floor((1 / (1 + Math.pow(x / (1 - x), -2))) * 1000) + 50;

    return shouldCacheCount;
  }

  /**
   * Calculates the number of members that should be cached for this guild.
   * @returns {Number}
   * @public
   * @method
   */
  calculateMemberCacheCount() {
    const x = this.memberCount < 500000 ? this.memberCount : 499999;
    /* creates a slope for how many members should stay cached */
    /* more members => smaller percentage of users active => a smaller percentage of users should be cached */
    /* a maximum of 500 seems suitable */
    const shouldCacheCount = Math.floor(0.5 * Math.exp((-x + 1) / 500000) * x);

    return shouldCacheCount;
  }

  /**
   * Deletes a webhook.
   * @param {Client} client The client instance.
   * @param {String} webhookId The id of the webhook to delete.
   * @returns {Promise<void>}
   * @public
   * @method
   * @async
   * @throws {TypeError}
   * @static
   */
  static async deleteWebhook(client, webhookId) {
    if (!(client instanceof Client))
      throw new TypeError("GLUON: Client must be a Client instance.");
    if (typeof webhookId !== "string")
      throw new TypeError("GLUON: Webhook ID is not a string.");
    await client.request.makeRequest("deleteWebhook", [webhookId]);
  }

  /**
   * Creates a webhook in the given channel with the name "Gluon".
   * @param {Client} client The client instance.
   * @param {String} channelId The id of the channel to create the webhook in.
   * @param {Object} options The options for creating the webhook.
   * @param {String} options.name The name of the webhook.
   * @returns {Promise<Object>}
   * @public
   * @method
   * @async
   * @throws {TypeError}
   * @static
   */
  static createWebhook(client, channelId, { name = NAME } = { name: NAME }) {
    if (!(client instanceof Client))
      throw new TypeError("GLUON: Client must be a Client instance.");
    if (typeof channelId !== "string")
      throw new TypeError("GLUON: Channel ID is not a string.");
    if (typeof name !== "string")
      throw new TypeError("GLUON: Name must be a string.");

    const body = {};

    body.name = name;

    return client.request.makeRequest("postCreateWebhook", [channelId], body);
  }

  /**
   * Modified a webhook with the given webhook id.
   * @param {Client} client The client instance.
   * @param {String} webhookId The id of the webhook to modify.
   * @param {Object} options The options to modify the webhook with.
   * @param {String} options.channelId The id of the channel the webhook belongs to.
   * @returns {Promise<Object>}
   * @public
   * @method
   * @async
   * @throws {TypeError}
   * @static
   */
  static modifyWebhook(client, webhookId, { channelId } = {}) {
    if (!(client instanceof Client))
      throw new TypeError("GLUON: Client must be a Client instance.");
    if (typeof webhookId !== "string")
      throw new TypeError("GLUON: Webhook ID is not a string.");
    if (typeof channelId !== "string")
      throw new TypeError("GLUON: Channel ID is not a string.");

    const body = {};

    body.channel_id = channelId;

    return client.request.makeRequest("patchModifyWebhook", [webhookId], body);
  }

  /**
   * Fetches a webhook by the webhook's id.
   * @param {Client} client The client instance.
   * @param {String} webhookId The id of the webhook to fetch.
   * @returns {Promise<Object>}
   * @public
   * @method
   * @async
   * @throws {TypeError}
   * @static
   */
  static fetchWebhook(client, webhookId) {
    if (!(client instanceof Client))
      throw new TypeError("GLUON: Client must be a Client instance.");
    if (typeof webhookId !== "string")
      throw new TypeError("GLUON: Webhook ID is not a string.");
    return client.request.makeRequest("getWebhook", [webhookId]);
  }

  /**
   * Posts a webhook with the provided webhook id and token.
   * @param {Client} client The client instance.
   * @param {Object} referenceData An object with the webhook id and token.
   * @param {String?} content The message to send with the webhook.
   * @param {Object?} options Embeds, components and files to attach to the webhook.
   * @returns {Promise<void>}
   * @public
   * @method
   * @async
   * @throws {TypeError}
   * @static
   */
  static async postWebhook(
    client,
    { id, token },
    content,
    { embeds, components, files } = {},
  ) {
    if (!(client instanceof Client))
      throw new TypeError("GLUON: Client must be a Client instance.");
    if (typeof id !== "string")
      throw new TypeError("GLUON: Webhook ID is not a string.");
    if (typeof token !== "string")
      throw new TypeError("GLUON: Webhook token is not a string.");

    Message.sendValidation(content, { embeds, components, files });

    const body = {};

    if (content) body.content = content;

    if (embeds) body.embeds = embeds;
    if (components) body.components;
    if (files) body.files = files;

    await client.request.makeRequest("postExecuteWebhook", [id, token], body);
  }

  /**
   * Returns the icon URL of the guild.
   * @param {String} id The id of the guild.
   * @param {String?} hash The hash of the guild icon.
   * @returns {String}
   * @public
   * @static
   * @method
   */
  static getIcon(id, hash) {
    if (typeof id !== "string")
      throw new TypeError("GLUON: Guild id must be a string.");
    if (hash && typeof hash !== "string")
      throw new TypeError("GLUON: Guild icon hash must be a string.");
    return hash
      ? `${CDN_BASE_URL}/icons/${id}/${hash}.${
          hash.startsWith("a_") ? "gif" : "png"
        }`
      : null;
  }

  /**
   * @method
   * @public
   */
  _intervalCallback() {
    this.#voice_states._intervalCallback();
    this.#members._intervalCallback();
    this.#channels._intervalCallback();
    this.#roles._intervalCallback();
    this.#scheduled_events._intervalCallback();
    this.#emojis._intervalCallback();
    this.#invites._intervalCallback();
  }

  /**
   * Determines whether the emoji should be cached.
   * @param {GluonCacheOptions} gluonCacheOptions The cache options for the client.
   * @returns {Boolean}
   * @public
   * @static
   * @method
   */
  static shouldCache(gluonCacheOptions) {
    if (!(gluonCacheOptions instanceof GluonCacheOptions))
      throw new TypeError(
        "GLUON: Gluon cache options must be a GluonCacheOptions.",
      );
    if (gluonCacheOptions.cacheGuilds === false) return false;
    return true;
  }

  /**
   * @method
   * @public
   */
  toString() {
    return `<Guild: ${this.id}>`;
  }

  /**
   * @method
   * @public
   */
  [util.inspect.custom]() {
    return this.toString();
  }

  /**
   * Returns the JSON representation of this structure.
   * @param {Number} format The format to return the data in.
   * @returns {Object}
   * @public
   * @method
   */
  toJSON(format) {
    switch (format) {
      case TO_JSON_TYPES_ENUM.CACHE_FORMAT:
      case TO_JSON_TYPES_ENUM.STORAGE_FORMAT: {
        return {
          id: this.id,
          name: this.name,
          icon: this.#_originalIconHash,
          owner_id: this.ownerId,
          joined_at: this.joinedAt * 1000,
          unavailable: this.unavailable,
          member_count: this.memberCount,
          preferred_locale: this.preferredLocale,
          _cache_options: this._cacheOptions,
          _attributes: this.#_attributes,
          system_channel_id: this.systemChannelId ?? undefined,
          rules_channel_id: this.rulesChannelId ?? undefined,
          premium_subscription_count: this.premiumSubscriptionCount,
          members: this.members.toJSON(format),
          channels: this.channels.toJSON(format),
          voice_states: this.voiceStates.toJSON(format),
          roles: this.roles.toJSON(format),
          emojis: this.emojis.toJSON(format),
          invites: this.invites.toJSON(format),
        };
      }
      case TO_JSON_TYPES_ENUM.DISCORD_FORMAT:
      default: {
        return {
          id: this.id,
          name: this.name,
          icon: this.#_originalIconHash,
          owner_id: this.ownerId,
          joined_at: new Date(this.joinedAt * 1000).toISOString(),
          premium_tier: this.premiumTier,
          unavailable: this.unavailable,
          member_count: this.memberCount,
          preferred_locale: this.preferredLocale,
          system_channel_flags: this.rawSystemChannelFlags,
          system_channel_id: this.systemChannelId ?? undefined,
          rules_channel_id: this.rulesChannelId ?? undefined,
          premium_subscription_count: this.premiumSubscriptionCount,
          premium_progress_bar_enabled: this.premiumProgressBarEnabled,
          default_message_notifications: this.rawDefaultMessageNotifications,
          explicit_content_filter: this.rawExplicitContentFilter,
          verification_level: this.rawVerificationLevel,
          nsfw_level: this.rawNsfwLevel,
          mfa_level: this.rawMfaLevel,
          members: this.members.toJSON(format),
          channels: this.channels.toJSON(format),
          voice_states: this.voiceStates.toJSON(format),
          roles: this.roles.toJSON(format),
          emojis: this.emojis.toJSON(format),
          invites: this.invites.toJSON(format),
        };
      }
    }
  }
}

export default Guild;