Structures_Guild.js

'user strict'
const Discord = require('discord.js')
const { BaseClass } = require('./BaseClass')
const { ErrorCodes } = require('../Errors/ErrorCodes')
const { ValRnkAchv } = require('../Utils/ValRnkAchv')
const { RankManager } = require('../Managers/RankManager')
const { UserManager } = require('../Managers/UserManager')
const { RankBuilder } = require('./RankBuilder')
const { AchievementManager } = require('../Managers/AchievementManager')
const { AchievementBuilder } = require('./AchievementBuilder')
const { RankPriorityInUse, MissingValue, InvalidValue, InvalidDataArgument, DuplicateAchievement, RankNotFound, AchievementNotFound } = require('../Errors/LME')

/** @typedef {import('../../typings').GuildData} GuildData */
/** @typedef {import('../../typings').Rank} Rank */

/**
 * Represents the data stored in the database, is different from the Discord.js Guild.
 */
class Guild extends BaseClass {
  /** @type {Discord.Snowflake} - The ID of the guild. */
  guildId
  /** @type {string} - The name of the guild. */
  guildName
  /** @type {RankManager} - The manager for the ranks. */
  ranks
  /** @type {AchievementManager} - The manager for the achievements. */
  achievements
  /** @type {UserManager} - The manager for the users. */
  users

  /**
   * @param {Discord.Client} client - The Discord Client.
   * @param {GuildData} data - The data to create the guild with.
   */
  constructor (client, data) {
    super(client)

    if (!data) throw new MissingValue(ErrorCodes.MissingArgument, 'data')

    if (typeof data !== 'object') throw new InvalidDataArgument(ErrorCodes.InvalidDataArgument)

    this.#checkData(data)

    this.guildId = data.guildId

    this.guildName = data.guildName

    this.ranks = new RankManager(client, data.ranks)

    this.achievements = new AchievementManager(client, data.achievements)

    this.users = new UserManager(client, data.users)
  }

  /**
   * Check if each property of the data is valid.
   * @param {GuildData} data - The data to check.
   *
   * @throws {MissingValue} - If a property is missing.
   * @throws {InvalidValue} - If a property is not valid.
   *
   * @ignore
   */
  #checkData (data) {
    if (!data.guildId) throw new MissingValue(ErrorCodes.MissingProperty, 'guildId')

    if (typeof data.guildId !== 'string') throw new InvalidValue(ErrorCodes.InvalidValue, 'guildId', 'a string')

    if (!data.guildName) throw new MissingValue(ErrorCodes.MissingProperty, 'guildName')

    if (typeof data.guildName !== 'string') throw new InvalidValue(ErrorCodes.InvalidValue, 'guildName', 'a string')

    if (!data.ranks) throw new MissingValue(ErrorCodes.MissingProperty, 'ranks')

    if (!Array.isArray(data.ranks)) throw new InvalidValue(ErrorCodes.InvalidValue, 'ranks', 'an array')

    if (data.ranks.some(rank => typeof rank !== 'object')) throw new InvalidValue(ErrorCodes.InvalidValue, 'ranks', 'an array of objects')

    if (!data.achievements) throw new MissingValue(ErrorCodes.MissingProperty, 'achievements')

    if (!Array.isArray(data.achievements)) throw new InvalidValue(ErrorCodes.InvalidValue, 'achievements', 'an array')

    if (data.achievements.some(achievement => typeof achievement !== 'object')) throw new InvalidValue(ErrorCodes.InvalidValue, 'achievements', 'an array of objects')

    if (!data.users) throw new MissingValue(ErrorCodes.MissingProperty, 'users')
    if (!Array.isArray(data.users)) throw new InvalidValue(ErrorCodes.InvalidValue, 'users', 'an array')

    if (data.users.some(user => typeof user !== 'object')) throw new InvalidValue(ErrorCodes.InvalidValue, 'users', 'an array of objects')
  }

  /**
   * Add a rank to the guild data and update it the database.
   * @param {RankBuilder} rank - The rank to add.
   * @returns {Promise<Rank>} - The rank added.
   *
   * @throws {MissingValue} - If the rank is missing.
   * @throws {InvalidValue} - If the rank is not a RankBuilder instance.
   * @throws {RankPriorityInUse} - If the rank priority is already in use.
   */
  async appendRank (rank) {
    if (!rank) throw new MissingValue(ErrorCodes.MissingArgument, 'rank')

    if (!(rank instanceof RankBuilder)) throw new InvalidValue(ErrorCodes.InvalidValue, 'rank', 'a RankBuilder instance')

    await ValRnkAchv.validateRank(rank)

    const rankData = rank.toJSON()

    const existing = this.ranks.cache.get(rankData.priority.toString())
    if (existing) throw new RankPriorityInUse(ErrorCodes.RankPriorityUsed, rankData.priority, existing.nameplate)

    this.ranks.cache.set(rankData.priority.toString(), rankData)

    // await GuildsData.findOneAndUpdate({ guildId: this.guildId }, { $push: { ranks: rankData } }).catch(console.log)

    return rankData
  }

  /**
   * Add an achievement to the guild data and update it the database.
   * @param {AchievementBuilder} achievement - The achievement to add.
   * @returns {Promise<Achievement>} - The achievement added.
   *
   * @throws {MissingValue} - If the achievement is missing.
   * @throws {InvalidValue} - If the achievement is not an AchievementBuilder instance.
   * @throws {DuplicateAchievement} - If the achievement name is already in use.
   */
  async appendAchievement (achievement) {
    if (!achievement) throw new MissingValue(ErrorCodes.MissingArgument, 'achievement')

    if (!(achievement instanceof AchievementBuilder)) throw new InvalidValue(ErrorCodes.InvalidValue, 'achievement', 'an AchievementBuilder instance')

    await ValRnkAchv.validateAchievement(achievement)

    const achievementData = achievement.toJSON()

    const existing = this.achievements.cache.get(achievementData.name)
    if (existing) throw new DuplicateAchievement(ErrorCodes.DuplicateAchievement, achievementData.name)

    this.achievements.cache.set(achievementData.name, achievementData)

    // await GuildsData.findOneAndUpdate({ guildId: this.guildId }, { $push: { achievements: achievementData } }).catch(console.log)

    for (const user of this.users.cache.values()) {
      user.achievements.push(achievementData)
    }

    return achievementData
  }

  /**
   * Remove a rank from the guild data and update it the database.
   * @param {Number} priority - The priority of the rank to remove.
   * @returns {Promise<Rank>} - The rank removed.
   */
  async removeRank (priority) {
    if (!priority) throw new MissingValue(ErrorCodes.MissingArgument, 'priority')

    if (typeof priority !== 'number') throw new InvalidValue(ErrorCodes.InvalidValue, 'priority', 'a number')

    const rank = this.ranks.cache.get(priority.toString())
    if (!rank) throw new RankNotFound(ErrorCodes.RankNotFound, priority)

    // Only delete rank in cache, const rank still exists
    this.ranks.cache.delete(priority.toString())

    // await GuildsData.findOneAndUpdate({ guildId: this.guildId }, { $pull: { ranks: { priority } } }).catch(console.log)

    return rank
  }

  /**
   * Remove an achievement from the guild data and update it the database.
   *
   * **NOTE: When an achievement is removed, it is removed from all users.**
   * @param {String} name - The name of the achievement to remove.
   * @returns {Promise<Achievement>} - The achievement removed.
   */
  async removeAchievement (name) {
    if (!name) throw new MissingValue(ErrorCodes.MissingArgument, 'name')

    if (typeof name !== 'string') throw new InvalidValue(ErrorCodes.InvalidValue, 'name', 'a string')

    const achievement = this.achievements.cache.get(name)
    if (!achievement) throw new AchievementNotFound(ErrorCodes.AchievementNotFound, name)

    this.achievements.cache.delete(name)

    // await GuildsData.findOneAndUpdate({ guildId: this.guildId }, { $pull: { achievements: { name } } }).catch(console.log)

    for (const user of this.users.cache.values()) {
      user.achievements = user.achievements.filter(achievement => achievement.name !== name)
    }

    return achievement
  }

  /**
   * Convert the guild data to a JSON object used for saving to the database.
   * @returns {GuildData}
   */
  toJSON () {
    return {
      guildId: this.guildId,
      guildName: this.guildName,
      ranks: this.ranks.cache.map(rank => rank),
      achievements: this.achievements.cache.map(achievement => achievement),
      users: this.users.cache.map(user => user.toJSON())
    }
  }
}

module.exports = { Guild }