/**
 * Sleeves are bodies that contain the player's cloned consciousness.
 * The player can use these bodies to perform different tasks synchronously.
 *
 * Each sleeve is its own individual, meaning it has its own stats/exp
 *
 * Sleeves are unlocked in BitNode-10.
 */
import { SleeveTaskType } from "./SleeveTaskTypesEnum";

import { IPlayer } from "../IPlayer";
import { Person, ITaskTracker, createTaskTracker } from "../Person";

import { Augmentation } from "../../Augmentation/Augmentation";

import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";

import { Crime } from "../../Crime/Crime";
import { Crimes } from "../../Crime/Crimes";

import { Companies } from "../../Company/Companies";
import { Company } from "../../Company/Company";
import { CompanyPosition } from "../../Company/CompanyPosition";
import { CompanyPositions } from "../../Company/CompanyPositions";

import { CONSTANTS } from "../../Constants";

import { Faction } from "../../Faction/Faction";
import { Factions } from "../../Faction/Factions";
import { FactionWorkType } from "../../Faction/FactionWorkTypeEnum";

import { CityName } from "../../Locations/data/CityNames";
import { LocationName } from "../../Locations/data/LocationNames";

import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../../utils/JSONReviver";

export class Sleeve extends Person {
  /**
   * Stores the name of the class that the player is currently taking
   */
  className = "";

  /**
   * Stores the type of crime the sleeve is currently attempting
   * Must match the name of a Crime object
   */
  crimeType = "";

  /**
   * Enum value for current task
   */
  currentTask: SleeveTaskType = SleeveTaskType.Idle;

  /**
   * Contains details about the sleeve's current task. The info stored
   * in this depends on the task type
   *
   * Faction/Company Work: Name of Faction/Company
   * Crime: Money earned if successful
   * Class/Gym: Name of university/gym
   */
  currentTaskLocation = "";

  /**
   * Maximum amount of time (in milliseconds) that can  be spent on current task.
   */
  currentTaskMaxTime = 0;

  /**
   * Milliseconds spent on current task
   */
  currentTaskTime = 0;

  /**
   * Keeps track of experience earned for other sleeves
   */
  earningsForSleeves: ITaskTracker = createTaskTracker();

  /**
   * Keeps track of experience + money earned for player
   */
  earningsForPlayer: ITaskTracker = createTaskTracker();

  /**
   * Keeps track of experienced earned in the current task/action
   */
  earningsForTask: ITaskTracker = createTaskTracker();

  /**
   * Keeps track of what type of work sleeve is doing for faction, if applicable
   */
  factionWorkType: FactionWorkType = FactionWorkType.None;

  /**
   * Records experience gain rate for the current task
   */
  gainRatesForTask: ITaskTracker = createTaskTracker();

  /**
   * String that stores what stat the sleeve is training at the gym
   */
  gymStatType = "";

  /**
   * Keeps track of events/notifications for this sleeve
   */
  logs: string[] = [];

  /**
   * Clone retains 'memory' synchronization (and maybe exp?) upon prestige/installing Augs
   */
  memory = 1;

  /**
   * Sleeve shock. Number between 0 and 100
   * Trauma/shock that comes with being in a sleeve. Experience earned
   * is multipled by shock%. This gets applied before synchronization
   *
   * Reputation earned is also multiplied by shock%
   */
  shock = 1;

  /**
   * Stored number of game "loop" cycles
   */
  storedCycles = 0;

  /**
   * Synchronization. Number between 0 and 100
   * When experience is earned  by sleeve, both the player and the sleeve get
   * sync% of the experience earned. Other sleeves get sync^2% of exp
   */
  sync = 1;

  constructor(p: IPlayer | null = null) {
    super();
    if (p != null) {
      this.shockRecovery(p);
    }
  }

  /**
   * Commit crimes
   */
  commitCrime(p: IPlayer, crimeKey: string): boolean {
    const crime: Crime | null = Crimes[crimeKey];
    if (!(crime instanceof Crime)) {
      return false;
    }

    if (this.currentTask !== SleeveTaskType.Idle) {
      this.finishTask(p);
    } else {
      this.resetTaskStatus();
    }

    this.gainRatesForTask.hack = crime.hacking_exp * this.hacking_exp_mult * BitNodeMultipliers.CrimeExpGain;
    this.gainRatesForTask.str = crime.strength_exp * this.strength_exp_mult * BitNodeMultipliers.CrimeExpGain;
    this.gainRatesForTask.def = crime.defense_exp * this.defense_exp_mult * BitNodeMultipliers.CrimeExpGain;
    this.gainRatesForTask.dex = crime.dexterity_exp * this.dexterity_exp_mult * BitNodeMultipliers.CrimeExpGain;
    this.gainRatesForTask.agi = crime.agility_exp * this.agility_exp_mult * BitNodeMultipliers.CrimeExpGain;
    this.gainRatesForTask.cha = crime.charisma_exp * this.charisma_exp_mult * BitNodeMultipliers.CrimeExpGain;
    this.gainRatesForTask.money = crime.money * this.crime_money_mult * BitNodeMultipliers.CrimeMoney;

    this.currentTaskLocation = String(this.gainRatesForTask.money);

    this.crimeType = crimeKey;
    this.currentTaskMaxTime = crime.time;
    this.currentTask = SleeveTaskType.Crime;
    return true;
  }

  /**
   * Called to stop the current task
   */
  finishTask(p: IPlayer): ITaskTracker {
    let retValue: ITaskTracker = createTaskTracker(); // Amount of exp to be gained by other sleeves

    if (this.currentTask === SleeveTaskType.Crime) {
      // For crimes, all experience and money is gained at the end
      if (this.currentTaskTime >= this.currentTaskMaxTime) {
        const crime: Crime | null = Crimes[this.crimeType];
        if (!(crime instanceof Crime)) {
          console.error(`Invalid data stored in sleeve.crimeType: ${this.crimeType}`);
          this.resetTaskStatus();
          return retValue;
        }
        if (Math.random() < crime.successRate(this)) {
          // Success
          const successGainRates: ITaskTracker = createTaskTracker();

          const keysForIteration: (keyof ITaskTracker)[] = Object.keys(successGainRates) as (keyof ITaskTracker)[];
          for (let i = 0; i < keysForIteration.length; ++i) {
            const key = keysForIteration[i];
            successGainRates[key] = this.gainRatesForTask[key] * 2;
          }
          retValue = this.gainExperience(p, successGainRates);
          this.gainMoney(p, this.gainRatesForTask);

          p.karma -= crime.karma * (this.sync / 100);
        } else {
          retValue = this.gainExperience(p, this.gainRatesForTask);
        }

        // Do not reset task to IDLE
        this.currentTaskTime = 0;
        return retValue;
      }
    } else {
      // For other crimes... I dont think anything else needs to be done
    }

    this.resetTaskStatus();

    return retValue;
  }

  /**
   * Earn experience for any stats (supports multiple)
   * This function also handles experience propogating to Player and other sleeves
   */
  gainExperience(p: IPlayer, exp: ITaskTracker, numCycles = 1, fromOtherSleeve = false): ITaskTracker {
    // If the experience is coming from another sleeve, it is not multiplied by anything.
    // Also the player does not earn anything
    if (fromOtherSleeve) {
      if (exp.hack > 0) {
        this.hacking_exp += exp.hack;
      }

      if (exp.str > 0) {
        this.strength_exp += exp.str;
      }

      if (exp.def > 0) {
        this.defense_exp += exp.def;
      }

      if (exp.dex > 0) {
        this.dexterity_exp += exp.dex;
      }

      if (exp.agi > 0) {
        this.agility_exp += exp.agi;
      }

      if (exp.cha > 0) {
        this.charisma_exp += exp.cha;
      }

      return createTaskTracker();
    }

    // Experience is first multiplied by shock. Then 'synchronization'
    // is accounted for

    const multFac = (this.shock / 100) * (this.sync / 100) * numCycles;
    const pHackExp = exp.hack * multFac;
    const pStrExp = exp.str * multFac;
    const pDefExp = exp.def * multFac;
    const pDexExp = exp.dex * multFac;
    const pAgiExp = exp.agi * multFac;
    const pChaExp = exp.cha * multFac;

    // Experience is gained by both this sleeve and player
    if (pHackExp > 0) {
      this.hacking_exp += pHackExp;
      p.gainHackingExp(pHackExp);
      this.earningsForPlayer.hack += pHackExp;
      this.earningsForTask.hack += pHackExp;
    }

    if (pStrExp > 0) {
      this.strength_exp += pStrExp;
      p.gainStrengthExp(pStrExp);
      this.earningsForPlayer.str += pStrExp;
      this.earningsForTask.str += pStrExp;
    }

    if (pDefExp > 0) {
      this.defense_exp += pDefExp;
      p.gainDefenseExp(pDefExp);
      this.earningsForPlayer.def += pDefExp;
      this.earningsForTask.def += pDefExp;
    }

    if (pDexExp > 0) {
      this.dexterity_exp += pDexExp;
      p.gainDexterityExp(pDexExp);
      this.earningsForPlayer.dex += pDexExp;
      this.earningsForTask.dex += pDexExp;
    }

    if (pAgiExp > 0) {
      this.agility_exp += pAgiExp;
      p.gainAgilityExp(pAgiExp);
      this.earningsForPlayer.agi += pAgiExp;
      this.earningsForTask.agi += pAgiExp;
    }

    if (pChaExp > 0) {
      this.charisma_exp += pChaExp;
      p.gainCharismaExp(pChaExp);
      this.earningsForPlayer.cha += pChaExp;
      this.earningsForTask.cha += pChaExp;
    }

    // Record earnings for other sleeves
    this.earningsForSleeves.hack += pHackExp * (this.sync / 100);
    this.earningsForSleeves.str += pStrExp * (this.sync / 100);
    this.earningsForSleeves.def += pDefExp * (this.sync / 100);
    this.earningsForSleeves.dex += pDexExp * (this.sync / 100);
    this.earningsForSleeves.agi += pAgiExp * (this.sync / 100);
    this.earningsForSleeves.cha += pChaExp * (this.sync / 100);

    // Return the experience to be gained by other sleeves
    return {
      hack: pHackExp * (this.sync / 100),
      str: pStrExp * (this.sync / 100),
      def: pDefExp * (this.sync / 100),
      dex: pDexExp * (this.sync / 100),
      agi: pAgiExp * (this.sync / 100),
      cha: pChaExp * (this.sync / 100),
      money: 0,
    };
  }

  /**
   * Earn money for player
   */
  gainMoney(p: IPlayer, task: ITaskTracker, numCycles = 1): void {
    const gain: number = task.money * numCycles;
    this.earningsForTask.money += gain;
    this.earningsForPlayer.money += gain;
    p.gainMoney(gain);
    p.recordMoneySource(gain, "sleeves");
  }

  /**
   * Returns the cost of upgrading this sleeve's memory by a certain amount
   */
  getMemoryUpgradeCost(n: number): number {
    const amt = Math.round(n);
    if (amt < 0) {
      return 0;
    }

    if (this.memory + amt > 100) {
      return this.getMemoryUpgradeCost(100 - this.memory);
    }

    const mult = 1.02;
    const baseCost = 1e12;
    let currCost = 0;
    let currMemory = this.memory - 1;
    for (let i = 0; i < n; ++i) {
      currCost += Math.pow(mult, currMemory);
      ++currMemory;
    }

    return currCost * baseCost;
  }

  /**
   * Gets reputation gain for the current task
   * Only applicable when working for company or faction
   */
  getRepGain(p: IPlayer): number {
    if (this.currentTask === SleeveTaskType.Faction) {
      let favorMult = 1;
      const fac: Faction | null = Factions[this.currentTaskLocation];
      if (fac != null) {
        favorMult = 1 + fac.favor / 100;
      }

      switch (this.factionWorkType) {
        case FactionWorkType.Hacking:
          return this.getFactionHackingWorkRepGain() * (this.shock / 100) * favorMult;
        case FactionWorkType.Field:
          return this.getFactionFieldWorkRepGain() * (this.shock / 100) * favorMult;
        case FactionWorkType.Security:
          return this.getFactionSecurityWorkRepGain() * (this.shock / 100) * favorMult;
        default:
          console.warn(`Invalid Sleeve.factionWorkType property in Sleeve.getRepGain(): ${this.factionWorkType}`);
          return 0;
      }
    } else if (this.currentTask === SleeveTaskType.Company) {
      const companyName: string = this.currentTaskLocation;
      const company: Company | null = Companies[companyName];
      if (company == null) {
        console.error(`Invalid company found when trying to calculate rep gain: ${companyName}`);
        return 0;
      }

      const companyPosition: CompanyPosition | null = CompanyPositions[p.jobs[companyName]];
      if (companyPosition == null) {
        console.error(`Invalid company position name found when trying to calculate rep gain: ${p.jobs[companyName]}`);
        return 0;
      }

      const jobPerformance: number = companyPosition.calculateJobPerformance(
        this.hacking_skill,
        this.strength,
        this.defense,
        this.dexterity,
        this.agility,
        this.charisma,
      );
      const favorMult = 1 + company.favor / 100;

      return jobPerformance * this.company_rep_mult * favorMult;
    } else {
      console.warn(`Sleeve.getRepGain() called for invalid task type: ${this.currentTask}`);
      return 0;
    }
  }

  installAugmentation(aug: Augmentation): void {
    this.hacking_exp = 0;
    this.strength_exp = 0;
    this.defense_exp = 0;
    this.dexterity_exp = 0;
    this.agility_exp = 0;
    this.charisma_exp = 0;
    this.applyAugmentation(aug);
    this.augmentations.push({ name: aug.name, level: 1 });
    this.updateStatLevels();
  }

  log(entry: string): void {
    const MaxLogSize = 50;
    this.logs.push(entry);
    if (this.logs.length > MaxLogSize) {
      this.logs.shift();
    }
  }

  /**
   * Called on every sleeve for a Source File prestige
   */
  prestige(p: IPlayer): void {
    // Reset exp
    this.hacking_exp = 0;
    this.strength_exp = 0;
    this.defense_exp = 0;
    this.dexterity_exp = 0;
    this.agility_exp = 0;
    this.charisma_exp = 0;

    // Reset task-related stuff
    this.resetTaskStatus();
    this.earningsForSleeves = createTaskTracker();
    this.earningsForPlayer = createTaskTracker();
    this.shockRecovery(p);

    // Reset augs and multipliers
    this.augmentations = [];
    this.resetMultipliers();

    // Reset sleeve-related stats
    this.shock = 1;
    this.storedCycles = 0;
    this.sync = Math.max(this.memory, 1);

    this.logs = [];
  }

  /**
   * Process loop
   * Returns an object containing the amount of experience that should be
   * transferred to all other sleeves
   */
  process(p: IPlayer, numCycles = 1): ITaskTracker | null {
    // Only process once every second (5 cycles)
    const CyclesPerSecond = 1000 / CONSTANTS.MilliPerCycle;
    this.storedCycles += numCycles;
    if (this.storedCycles < CyclesPerSecond) {
      return null;
    }

    let time = this.storedCycles * CONSTANTS.MilliPerCycle;
    let cyclesUsed = this.storedCycles;
    if (this.currentTaskMaxTime !== 0 && this.currentTaskTime + time > this.currentTaskMaxTime) {
      time = this.currentTaskMaxTime - this.currentTaskTime;
      cyclesUsed = Math.floor(time / CONSTANTS.MilliPerCycle);

      if (time < 0 || cyclesUsed < 0) {
        console.warn(`Sleeve.process() calculated negative cycle usage`);
        time = 0;
        cyclesUsed = 0;
      }
    }
    this.currentTaskTime += time;

    // Shock gradually goes towards 100
    this.shock = Math.min(100, this.shock + 0.0001 * cyclesUsed);

    let retValue: ITaskTracker = createTaskTracker();
    switch (this.currentTask) {
      case SleeveTaskType.Idle:
        break;
      case SleeveTaskType.Class:
      case SleeveTaskType.Gym:
        this.updateTaskGainRates(p);
        retValue = this.gainExperience(p, this.gainRatesForTask, cyclesUsed);
        this.gainMoney(p, this.gainRatesForTask, cyclesUsed);
        break;
      case SleeveTaskType.Faction: {
        retValue = this.gainExperience(p, this.gainRatesForTask, cyclesUsed);
        this.gainMoney(p, this.gainRatesForTask, cyclesUsed);

        // Gain faction reputation
        const fac: Faction = Factions[this.currentTaskLocation];
        if (!(fac instanceof Faction)) {
          console.error(`Invalid faction for Sleeve task: ${this.currentTaskLocation}`);
          break;
        }

        fac.playerReputation += this.getRepGain(p) * cyclesUsed;
        break;
      }
      case SleeveTaskType.Company: {
        retValue = this.gainExperience(p, this.gainRatesForTask, cyclesUsed);
        this.gainMoney(p, this.gainRatesForTask, cyclesUsed);

        const company: Company = Companies[this.currentTaskLocation];
        if (!(company instanceof Company)) {
          console.error(`Invalid company for Sleeve task: ${this.currentTaskLocation}`);
          break;
        }

        company.playerReputation += this.getRepGain(p) * cyclesUsed;
        break;
      }
      case SleeveTaskType.Recovery:
        this.shock = Math.min(100, this.shock + 0.0002 * cyclesUsed);
        break;
      case SleeveTaskType.Synchro:
        this.sync = Math.min(100, this.sync + p.getIntelligenceBonus(0.5) * 0.0002 * cyclesUsed);
        break;
      default:
        break;
    }

    if (this.currentTaskMaxTime !== 0 && this.currentTaskTime >= this.currentTaskMaxTime) {
      if (this.currentTask === SleeveTaskType.Crime) {
        retValue = this.finishTask(p);
      } else {
        this.finishTask(p);
      }
    }

    this.updateStatLevels();

    this.storedCycles -= cyclesUsed;

    return retValue;
  }

  /**
   * Resets all parameters used to keep information about the current task
   */
  resetTaskStatus(): void {
    this.earningsForTask = createTaskTracker();
    this.gainRatesForTask = createTaskTracker();
    this.currentTask = SleeveTaskType.Idle;
    this.currentTaskTime = 0;
    this.currentTaskMaxTime = 0;
    this.factionWorkType = FactionWorkType.None;
    this.crimeType = "";
    this.currentTaskLocation = "";
    this.gymStatType = "";
    this.className = "";
  }

  shockRecovery(p: IPlayer): boolean {
    if (this.currentTask !== SleeveTaskType.Idle) {
      this.finishTask(p);
    } else {
      this.resetTaskStatus();
    }

    this.currentTask = SleeveTaskType.Recovery;
    return true;
  }

  synchronize(p: IPlayer): boolean {
    if (this.currentTask !== SleeveTaskType.Idle) {
      this.finishTask(p);
    } else {
      this.resetTaskStatus();
    }

    this.currentTask = SleeveTaskType.Synchro;
    return true;
  }

  /**
   * Take a course at a university
   */
  takeUniversityCourse(p: IPlayer, universityName: string, className: string): boolean {
    if (this.currentTask !== SleeveTaskType.Idle) {
      this.finishTask(p);
    } else {
      this.resetTaskStatus();
    }

    // Set exp/money multipliers based on which university.
    // Also check that the sleeve is in the right city
    let costMult = 1;
    switch (universityName.toLowerCase()) {
      case LocationName.AevumSummitUniversity.toLowerCase():
        if (this.city !== CityName.Aevum) {
          return false;
        }
        this.currentTaskLocation = LocationName.AevumSummitUniversity;
        costMult = 4;
        break;
      case LocationName.Sector12RothmanUniversity.toLowerCase():
        if (this.city !== CityName.Sector12) {
          return false;
        }
        this.currentTaskLocation = LocationName.Sector12RothmanUniversity;
        costMult = 3;
        break;
      case LocationName.VolhavenZBInstituteOfTechnology.toLowerCase():
        if (this.city !== CityName.Volhaven) {
          return false;
        }
        this.currentTaskLocation = LocationName.VolhavenZBInstituteOfTechnology;
        costMult = 5;
        break;
      default:
        return false;
    }

    // Set experience/money gains based on class
    switch (className.toLowerCase()) {
      case "study computer science":
        break;
      case "data structures":
        this.gainRatesForTask.money = -1 * (CONSTANTS.ClassDataStructuresBaseCost * costMult);
        break;
      case "networks":
        this.gainRatesForTask.money = -1 * (CONSTANTS.ClassNetworksBaseCost * costMult);
        break;
      case "algorithms":
        this.gainRatesForTask.money = -1 * (CONSTANTS.ClassAlgorithmsBaseCost * costMult);
        break;
      case "management":
        this.gainRatesForTask.money = -1 * (CONSTANTS.ClassManagementBaseCost * costMult);
        break;
      case "leadership":
        this.gainRatesForTask.money = -1 * (CONSTANTS.ClassLeadershipBaseCost * costMult);
        break;
      default:
        return false;
    }

    this.className = className;
    this.currentTask = SleeveTaskType.Class;
    return true;
  }

  /**
   * Travel to another City. Costs money from player
   */
  travel(p: IPlayer, newCity: CityName): boolean {
    p.loseMoney(CONSTANTS.TravelCost);
    this.city = newCity;

    return true;
  }

  tryBuyAugmentation(p: IPlayer, aug: Augmentation): boolean {
    if (!p.canAfford(aug.startingCost)) {
      return false;
    }

    // Verify that this sleeve does not already have that augmentation.
    if (this.augmentations.some((a) => a.name === aug.name)) {
      return false;
    }

    p.loseMoney(aug.startingCost);
    this.installAugmentation(aug);
    return true;
  }

  updateTaskGainRates(p: IPlayer): void {
    if (this.currentTask === SleeveTaskType.Class) {
      let expMult = 1;
      switch (this.currentTaskLocation.toLowerCase()) {
        case LocationName.AevumSummitUniversity.toLowerCase():
          expMult = 3;
          break;
        case LocationName.Sector12RothmanUniversity.toLowerCase():
          expMult = 2;
          break;
        case LocationName.VolhavenZBInstituteOfTechnology.toLowerCase():
          expMult = 4;
          break;
        default:
          return;
      }

      const totalExpMult = expMult * p.hashManager.getStudyMult();
      switch (this.className.toLowerCase()) {
        case "study computer science":
          this.gainRatesForTask.hack =
            CONSTANTS.ClassStudyComputerScienceBaseExp * totalExpMult * this.hacking_exp_mult;
          break;
        case "data structures":
          this.gainRatesForTask.hack = CONSTANTS.ClassDataStructuresBaseExp * totalExpMult * this.hacking_exp_mult;
          break;
        case "networks":
          this.gainRatesForTask.hack = CONSTANTS.ClassNetworksBaseExp * totalExpMult * this.hacking_exp_mult;
          break;
        case "algorithms":
          this.gainRatesForTask.hack = CONSTANTS.ClassAlgorithmsBaseExp * totalExpMult * this.hacking_exp_mult;
          break;
        case "management":
          this.gainRatesForTask.cha = CONSTANTS.ClassManagementBaseExp * totalExpMult * this.charisma_exp_mult;
          break;
        case "leadership":
          this.gainRatesForTask.cha = CONSTANTS.ClassLeadershipBaseExp * totalExpMult * this.charisma_exp_mult;
          break;
        default:
          break;
      }

      return;
    }

    if (this.currentTask === SleeveTaskType.Gym) {
      // Get gym exp multiplier
      let expMult = 1;
      switch (this.currentTaskLocation.toLowerCase()) {
        case LocationName.AevumCrushFitnessGym.toLowerCase():
          expMult = 2;
          break;
        case LocationName.AevumSnapFitnessGym.toLowerCase():
          expMult = 5;
          break;
        case LocationName.Sector12IronGym.toLowerCase():
          expMult = 1;
          break;
        case LocationName.Sector12PowerhouseGym.toLowerCase():
          expMult = 10;
          break;
        case LocationName.VolhavenMilleniumFitnessGym:
          expMult = 4;
          break;
        default:
          return;
      }

      // Set stat gain rate
      const baseGymExp = 1;
      const totalExpMultiplier = p.hashManager.getTrainingMult() * expMult;
      const sanitizedStat: string = this.gymStatType.toLowerCase();
      if (sanitizedStat.includes("str")) {
        this.gainRatesForTask.str = baseGymExp * totalExpMultiplier * this.strength_exp_mult;
      } else if (sanitizedStat.includes("def")) {
        this.gainRatesForTask.def = baseGymExp * totalExpMultiplier * this.defense_exp_mult;
      } else if (sanitizedStat.includes("dex")) {
        this.gainRatesForTask.dex = baseGymExp * totalExpMultiplier * this.dexterity_exp_mult;
      } else if (sanitizedStat.includes("agi")) {
        this.gainRatesForTask.agi = baseGymExp * totalExpMultiplier * this.agility_exp_mult;
      }

      return;
    }

    console.warn(`Sleeve.updateTaskGainRates() called for unexpected task type ${this.currentTask}`);
  }

  upgradeMemory(n: number): void {
    if (n < 0) {
      console.warn(`Sleeve.upgradeMemory() called with negative value: ${n}`);
      return;
    }

    this.memory = Math.min(100, Math.round(this.memory + n));
  }

  /**
   * Start work for one of the player's companies
   * Returns boolean indicating success
   */
  workForCompany(p: IPlayer, companyName: string): boolean {
    if (!(Companies[companyName] instanceof Company) || p.jobs[companyName] == null) {
      return false;
    }

    if (this.currentTask !== SleeveTaskType.Idle) {
      this.finishTask(p);
    } else {
      this.resetTaskStatus();
    }

    const company: Company | null = Companies[companyName];
    const companyPosition: CompanyPosition | null = CompanyPositions[p.jobs[companyName]];
    if (company == null) {
      return false;
    }
    if (companyPosition == null) {
      return false;
    }
    this.gainRatesForTask.money =
      companyPosition.baseSalary *
      company.salaryMultiplier *
      this.work_money_mult *
      BitNodeMultipliers.CompanyWorkMoney;
    this.gainRatesForTask.hack =
      companyPosition.hackingExpGain *
      company.expMultiplier *
      this.hacking_exp_mult *
      BitNodeMultipliers.CompanyWorkExpGain;
    this.gainRatesForTask.str =
      companyPosition.strengthExpGain *
      company.expMultiplier *
      this.strength_exp_mult *
      BitNodeMultipliers.CompanyWorkExpGain;
    this.gainRatesForTask.def =
      companyPosition.defenseExpGain *
      company.expMultiplier *
      this.defense_exp_mult *
      BitNodeMultipliers.CompanyWorkExpGain;
    this.gainRatesForTask.dex =
      companyPosition.dexterityExpGain *
      company.expMultiplier *
      this.dexterity_exp_mult *
      BitNodeMultipliers.CompanyWorkExpGain;
    this.gainRatesForTask.agi =
      companyPosition.agilityExpGain *
      company.expMultiplier *
      this.agility_exp_mult *
      BitNodeMultipliers.CompanyWorkExpGain;
    this.gainRatesForTask.cha =
      companyPosition.charismaExpGain *
      company.expMultiplier *
      this.charisma_exp_mult *
      BitNodeMultipliers.CompanyWorkExpGain;

    this.currentTaskLocation = companyName;
    this.currentTask = SleeveTaskType.Company;
    this.currentTaskMaxTime = CONSTANTS.MillisecondsPer8Hours;

    return true;
  }

  /**
   * Start work for one of the player's factions
   * Returns boolean indicating success
   */
  workForFaction(p: IPlayer, factionName: string, workType: string): boolean {
    if (factionName === "") {
      return false;
    }
    if (!(Factions[factionName] instanceof Faction) || !p.factions.includes(factionName)) {
      return false;
    }

    if (this.currentTask !== SleeveTaskType.Idle) {
      this.finishTask(p);
    } else {
      this.resetTaskStatus();
    }

    const factionInfo = Factions[factionName].getInfo();

    // Set type of work (hacking/field/security), and the experience gains
    const sanitizedWorkType: string = workType.toLowerCase();
    if (sanitizedWorkType.includes("hack")) {
      if (!factionInfo.offerHackingWork) {
        return false;
      }
      this.factionWorkType = FactionWorkType.Hacking;
      this.gainRatesForTask.hack = 0.15 * this.hacking_exp_mult * BitNodeMultipliers.FactionWorkExpGain;
    } else if (sanitizedWorkType.includes("field")) {
      if (!factionInfo.offerFieldWork) {
        return false;
      }
      this.factionWorkType = FactionWorkType.Field;
      this.gainRatesForTask.hack = 0.1 * this.hacking_exp_mult * BitNodeMultipliers.FactionWorkExpGain;
      this.gainRatesForTask.str = 0.1 * this.strength_exp_mult * BitNodeMultipliers.FactionWorkExpGain;
      this.gainRatesForTask.def = 0.1 * this.defense_exp_mult * BitNodeMultipliers.FactionWorkExpGain;
      this.gainRatesForTask.dex = 0.1 * this.dexterity_exp_mult * BitNodeMultipliers.FactionWorkExpGain;
      this.gainRatesForTask.agi = 0.1 * this.agility_exp_mult * BitNodeMultipliers.FactionWorkExpGain;
      this.gainRatesForTask.cha = 0.1 * this.charisma_exp_mult * BitNodeMultipliers.FactionWorkExpGain;
    } else if (sanitizedWorkType.includes("security")) {
      if (!factionInfo.offerSecurityWork) {
        return false;
      }
      this.factionWorkType = FactionWorkType.Security;
      this.gainRatesForTask.hack = 0.1 * this.hacking_exp_mult * BitNodeMultipliers.FactionWorkExpGain;
      this.gainRatesForTask.str = 0.15 * this.strength_exp_mult * BitNodeMultipliers.FactionWorkExpGain;
      this.gainRatesForTask.def = 0.15 * this.defense_exp_mult * BitNodeMultipliers.FactionWorkExpGain;
      this.gainRatesForTask.dex = 0.15 * this.dexterity_exp_mult * BitNodeMultipliers.FactionWorkExpGain;
      this.gainRatesForTask.agi = 0.15 * this.agility_exp_mult * BitNodeMultipliers.FactionWorkExpGain;
    } else {
      return false;
    }

    this.currentTaskLocation = factionName;
    this.currentTask = SleeveTaskType.Faction;
    this.currentTaskMaxTime = CONSTANTS.MillisecondsPer20Hours;

    return true;
  }

  /**
   * Begin a gym workout task
   */
  workoutAtGym(p: IPlayer, gymName: string, stat: string): boolean {
    if (this.currentTask !== SleeveTaskType.Idle) {
      this.finishTask(p);
    } else {
      this.resetTaskStatus();
    }

    // Set exp/money multipliers based on which university.
    // Also check that the sleeve is in the right city
    let costMult = 1;
    switch (gymName.toLowerCase()) {
      case LocationName.AevumCrushFitnessGym.toLowerCase():
        if (this.city != CityName.Aevum) {
          return false;
        }
        this.currentTaskLocation = LocationName.AevumCrushFitnessGym;
        costMult = 3;
        break;
      case LocationName.AevumSnapFitnessGym.toLowerCase():
        if (this.city != CityName.Aevum) {
          return false;
        }
        this.currentTaskLocation = LocationName.AevumSnapFitnessGym;
        costMult = 10;
        break;
      case LocationName.Sector12IronGym.toLowerCase():
        if (this.city != CityName.Sector12) {
          return false;
        }
        this.currentTaskLocation = LocationName.Sector12IronGym;
        costMult = 1;
        break;
      case LocationName.Sector12PowerhouseGym.toLowerCase():
        if (this.city != CityName.Sector12) {
          return false;
        }
        this.currentTaskLocation = LocationName.Sector12PowerhouseGym;
        costMult = 20;
        break;
      case LocationName.VolhavenMilleniumFitnessGym.toLowerCase():
        if (this.city != CityName.Volhaven) {
          return false;
        }
        this.currentTaskLocation = LocationName.VolhavenMilleniumFitnessGym;
        costMult = 7;
        break;
      default:
        return false;
    }

    // Set experience/money gains based on class
    const sanitizedStat: string = stat.toLowerCase();

    // Set cost
    this.gainRatesForTask.money = -1 * (CONSTANTS.ClassGymBaseCost * costMult);

    // Validate "stat" argument
    if (
      !sanitizedStat.includes("str") &&
      !sanitizedStat.includes("def") &&
      !sanitizedStat.includes("dex") &&
      !sanitizedStat.includes("agi")
    ) {
      return false;
    }

    this.gymStatType = stat;
    this.currentTask = SleeveTaskType.Gym;

    return true;
  }

  /**
   * Serialize the current object to a JSON save state.
   */
  toJSON(): any {
    return Generic_toJSON("Sleeve", this);
  }

  /**
   * Initiatizes a Sleeve object from a JSON save state.
   */
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  static fromJSON(value: any): Sleeve {
    return Generic_fromJSON(Sleeve, value.data);
  }
}

Reviver.constructors.Sleeve = Sleeve;
