class Monster {
  #level = 2;
  #hp = 0;

  exp = 1;

  tasteWarm = TasteWarm.tasteless;
  tasteCold = TasteCold.tasteless;

  gender = '';

  heldItem = null;

  /**
   * @type {StatusEffect}
   */
  statusEffect = null;

  statModifiers = {
    hp: 0,
    melee: 0,
    armour: 0,
    ranged: 0,
    dodge: 0,
    speed: 0,
  };

  experienceModifier = 1;
  moneyModifier = 1;

  /**
   * @type {Technique[]}
   */
  activeTechniques = [];

  constructor (slug) {
    this.slug = slug;

    const tasteWarm = Object.keys(TasteWarm).slice(1);
    this.tasteWarm = tasteWarm[Math.floor(Math.random() * tasteWarm.length)];
    const tasteCold = Object.keys(TasteCold).slice(1);
    this.tasteCold = tasteCold[Math.floor(Math.random() * tasteCold.length)];

    this.hp = this.stats.hp;

    const possibleGenders = DB.monsters[this.slug].possible_genders;
    this.gender = possibleGenders[Math.floor(Math.random() * possibleGenders.length)];

  }

  async initialize () {
    for (const move of this.getLearnableTechniques()) {
      this.activeTechniques.push(await fetchTechnique(move.technique));
    }
  }

  get shape () {
    for (const shapeData of DB.shapes) {
      if (shapeData.slug === DB.monsters[this.slug].shape) {
        return shapeData;
      }
    }
  }

  /**
   * @returns {string[]}
   */
  get types () {
    return DB.monsters[this.slug].types;
  }

  get category () {
    return DB.monsters[this.slug].category;
  }

  get moveset () {
    return DB.monsters[this.slug].moveset;
  }

  get catch_rate () {
    return DB.monsters[this.slug].catch_rate;
  }

  get upper_catch_resistance () {
    return DB.monsters[this.slug].upper_catch_resistance;
  }

  get lower_catch_resistance () {
    return DB.monsters[this.slug].lower_catch_resistance;
  }

  /**
   * @returns {DB_Evolution[]}
   */
  get evolutions () {
    return DB.monsters[this.slug].evolutions;
  }

  get level () {
    return this.#level;
  }

  set level (level) {
    const statsPreLevelUp = this.stats;
    const hpPreLevelUp = this.hp;

    this.#level = level;

    const statsPostLevelUp = this.stats;

    this.hp = statsPostLevelUp.hp - (statsPreLevelUp.hp - hpPreLevelUp);

    if (this.exp < this.getExperienceRequired(-1)) {
      this.exp = this.getExperienceRequired(-1);
    }
  }

  get hp () {
    return this.#hp;
  }

  set hp (hp) {
    this.#hp = Math.max(0, Math.min(hp, this.stats.hp));
  }

  get name () {
    return translate(this.slug) || slugToName(this.slug);
  }

  /**
   * @returns {Object[]}
   */
  getLearnableTechniques () {
    return this.moveset.filter((move) => this.level >= move.level_learned);
  }

  canLevelUp () {
    return this.exp >= this.getExperienceRequired();
  }

  levelUp () {
    while (this.canLevelUp()) {
      this.level++;
    }
  }

  getExperienceRequired (levelOffset = 0) {
    return Math.max(
      Math.pow(this.level + levelOffset, 3),
      1
    );
  }

  /**
   * @param {EvolutionPathType} path
   */
  getPossibleEvolutions (path) {
    return this.evolutions.filter((evolution) => {
      if (evolution.path !== path) {
        return false;
      }

      if (evolution.path === EvolutionPathType.standard) {
        return this.level >= evolution.at_level;
      }

      else if  (evolution.path === EvolutionPathType.item) {
        return true;
      }
    });
  }

  /**
   * @param {EvolutionPathType} path
   */
  canEvolve (path) {
    return this.getPossibleEvolutions(path).length > 0;
  }

  /**
   * @param {DB_Evolution} evolution
   */
  evolve (evolution) {
    const statsPreEvolve = this.stats;
    const hpPreEvolve = this.hp;

    this.slug = evolution.monster_slug;

    const statsPostEvolve = this.stats;

    this.hp = statsPostEvolve.hp - (statsPreEvolve.hp - hpPreEvolve);
  }

  getTasteStatModifier (statType, baseStat) {
    let positive = 0;
    let negative = 0;

    let isPositive = false;
    let isNegative = false;
    if (statType === StatType.melee) {
      isPositive = this.tasteWarm === TasteWarm.salty;
      isNegative = this.tasteCold === TasteCold.sweet;
    }
    else if (statType === StatType.armour) {
      isPositive = this.tasteWarm === TasteWarm.hearty;
      isNegative = this.tasteCold === TasteCold.soft;
    }
    else if (statType === StatType.ranged) {
      isPositive = this.tasteWarm === TasteWarm.zesty;
      isNegative = this.tasteCold === TasteCold.flakey;
    }
    else if (statType === StatType.dodge) {
      isPositive = this.tasteWarm === TasteWarm.refined;
      isNegative = this.tasteCold === TasteCold.dry;
    }
    else if (statType === StatType.speed) {
      isPositive = this.tasteWarm === TasteWarm.peppy;
      isNegative = this.tasteCold === TasteCold.mild;
    }

    if (isPositive) {
      positive = baseStat * 10 / 100;
    }
    if (isNegative) {
      negative = baseStat * 10 / 100;
    }

    return Math.floor(positive) - Math.floor(negative);
  }

  get stats () {
    const multiplier = this.level + 7;
    let hp = (this.shape.hp * multiplier) + this.statModifiers.hp;
    let melee = (this.shape.melee * multiplier) + this.statModifiers.melee;
    let armour = (this.shape.armour * multiplier) + this.statModifiers.armour;
    let ranged = (this.shape.ranged * multiplier) + this.statModifiers.ranged;
    let dodge = (this.shape.dodge * multiplier) + this.statModifiers.dodge;
    let speed = (this.shape.speed * multiplier) + this.statModifiers.speed;

    // Tastes
    melee += this.getTasteStatModifier(StatType.melee, melee);
    armour += this.getTasteStatModifier(StatType.armour, armour);
    ranged += this.getTasteStatModifier(StatType.ranged, ranged);
    dodge += this.getTasteStatModifier(StatType.dodge, dodge);
    speed += this.getTasteStatModifier(StatType.speed, speed);

    return {
      hp: hp,
      [StatType.melee]: melee,
      [StatType.armour]: armour,
      [StatType.ranged]: ranged,
      [StatType.dodge]: dodge,
      [StatType.speed]: speed,
    };
  }

  setStatModifier (statType, newAbsoluteValue) {
    this.statModifiers[statType] = newAbsoluteValue - this.stats[statType];
  }

  resetStatModifiers () {
    for (const m in this.statModifiers) {
      this.statModifiers[m] = 0;
    }
  }
};