import type { BitNodeOptions, Player as IPlayer } from "@nsdefs";
import type { PlayerAchievement } from "../../Achievements/Achievements";
import type { Bladeburner } from "../../Bladeburner/Bladeburner";
import type { Corporation } from "../../Corporation/Corporation";
import type { Infiltration } from "../../Infiltration/Infiltration";
import type { Exploit } from "../../Exploits/Exploit";
import type { Gang } from "../../Gang/Gang";
import type { HacknetNode } from "../../Hacknet/HacknetNode";
import type { Sleeve } from "../Sleeve/Sleeve";
import type { PlayerBaseWork } from "../../Work/Work";

import * as augmentationMethods from "./PlayerObjectAugmentationMethods";
import * as bladeburnerMethods from "./PlayerObjectBladeburnerMethods";
import * as corporationMethods from "./PlayerObjectCorporationMethods";
import * as gangMethods from "./PlayerObjectGangMethods";
import * as generalMethods from "./PlayerObjectGeneralMethods";
import * as serverMethods from "./PlayerObjectServerMethods";
import * as workMethods from "./PlayerObjectWorkMethods";

import { setPlayer } from "@player";
import { CompanyName, FactionName, JobName, LocationName } from "@enums";
import { HashManager } from "../../Hacknet/HashManager";
import { type MoneySource, MoneySourceTracker } from "../../utils/MoneySourceTracker";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../../utils/JSONReviver";
import { JSONMap, JSONSet } from "../../Types/Jsonable";
import { cyrb53 } from "../../utils/HashUtils";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
import { getKeyList } from "../../utils/helpers/getKeyList";
import { CONSTANTS } from "../../Constants";
import { Person } from "../Person";
import { isMember } from "../../utils/EnumHelper";
import { PartialRecord } from "../../Types/Record";
import { isSleeveSupportWork } from "../Sleeve/Work/SleeveSupportWork";

export class PlayerObject extends Person implements IPlayer {
  // Player-specific properties
  bitNodeN = 1; //current bitnode
  corporation: Corporation | null = null;
  gang: Gang | null = null;
  bladeburner: Bladeburner | null = null;
  infiltration: Infiltration | null = null;
  currentServer = "";
  factions: FactionName[] = [];
  factionInvitations: FactionName[] = [];
  factionRumors = new JSONSet<FactionName>();
  hacknetNodes: (HacknetNode | string)[] = []; // HacknetNode object or hostname of Hacknet Server
  has4SData = false;
  has4SDataTixApi = false;
  hashManager = new HashManager();
  hasTixApiAccess = false;
  hasWseAccount = false;
  jobs: PartialRecord<CompanyName, JobName> = {};
  karma = 0;
  numPeopleKilled = 0;
  location = LocationName.TravelAgency;
  money = 1000 + CONSTANTS.Donations;
  moneySourceA = new MoneySourceTracker();
  moneySourceB = new MoneySourceTracker();
  playtimeSinceLastAug = 0;
  playtimeSinceLastBitnode = 0;
  lastAugReset = -1;
  lastNodeReset = -1;
  purchasedServers: string[] = [];
  scriptProdSinceLastAug = 0;
  sleeves: Sleeve[] = [];
  sleevesFromCovenant = 0;
  sourceFiles = new JSONMap<number, number>();
  exploits: Exploit[] = [];
  achievements: PlayerAchievement[] = [];
  terminalCommandHistory: string[] = [];
  identifier: string;
  lastUpdate = 0;
  lastSave = 0;
  totalPlaytime = 0;

  currentWork: PlayerBaseWork | null = null;
  focus = false;

  entropy = 0;

  bitNodeOptions: BitNodeOptions = {
    sourceFileOverrides: new JSONMap<number, number>(),
    intelligenceOverride: undefined,
    restrictHomePCUpgrade: false,
    disableGang: false,
    disableCorporation: false,
    disableBladeburner: false,
    disable4SData: false,
    disableHacknetServer: false,
    disableSleeveExpAndAugmentation: false,
  };

  get activeSourceFiles(): JSONMap<number, number> {
    return new JSONMap([...this.sourceFiles, ...this.bitNodeOptions.sourceFileOverrides]);
  }

  // Player-specific methods
  init = generalMethods.init;
  startWork = workMethods.startWork;
  processWork = workMethods.processWork;
  finishWork = workMethods.finishWork;
  applyForJob = generalMethods.applyForJob;
  canAccessBladeburner = bladeburnerMethods.canAccessBladeburner;
  canAccessCorporation = corporationMethods.canAccessCorporation;
  canAccessGang = gangMethods.canAccessGang;
  canAccessGrafting = generalMethods.canAccessGrafting;
  canAfford = generalMethods.canAfford;
  gainMoney = generalMethods.gainMoney;
  getCurrentServer = serverMethods.getCurrentServer;
  getGangFaction = gangMethods.getGangFaction;
  getGangName = gangMethods.getGangName;
  getHomeComputer = serverMethods.getHomeComputer;
  getNextCompanyPosition = generalMethods.getNextCompanyPosition;
  getUpgradeHomeRamCost = serverMethods.getUpgradeHomeRamCost;
  getUpgradeHomeCoresCost = serverMethods.getUpgradeHomeCoresCost;
  gotoLocation = generalMethods.gotoLocation;
  hasGangWith = gangMethods.hasGangWith;
  hasTorRouter = serverMethods.hasTorRouter;
  hasProgram = generalMethods.hasProgram;
  inGang = gangMethods.inGang;
  isAwareOfGang = gangMethods.isAwareOfGang;
  isQualified = generalMethods.isQualified;
  loseMoney = generalMethods.loseMoney;
  reapplyAllAugmentations = generalMethods.reapplyAllAugmentations;
  reapplyAllSourceFiles = generalMethods.reapplyAllSourceFiles;
  recordMoneySource = generalMethods.recordMoneySource;
  setMoney = generalMethods.setMoney;
  startBladeburner = bladeburnerMethods.startBladeburner;
  startCorporation = corporationMethods.startCorporation;
  startFocusing = generalMethods.startFocusing;
  startGang = gangMethods.startGang;
  takeDamage = generalMethods.takeDamage;
  giveExploit = generalMethods.giveExploit;
  giveAchievement = generalMethods.giveAchievement;
  getCasinoWinnings = generalMethods.getCasinoWinnings;
  quitJob = generalMethods.quitJob;
  hasJob = generalMethods.hasJob;
  createHacknetServer = serverMethods.createHacknetServer;
  queueAugmentation = generalMethods.queueAugmentation;
  receiveInvite = generalMethods.receiveInvite;
  receiveRumor = generalMethods.receiveRumor;
  gainCodingContractReward = generalMethods.gainCodingContractReward;
  stopFocusing = generalMethods.stopFocusing;
  prestigeAugmentation = generalMethods.prestigeAugmentation;
  prestigeSourceFile = generalMethods.prestigeSourceFile;
  calculateSkillProgress = generalMethods.calculateSkillProgress;
  hospitalize = generalMethods.hospitalize;
  checkForFactionInvitations = generalMethods.checkForFactionInvitations;
  setBitNodeNumber = generalMethods.setBitNodeNumber;
  canAccessCotMG = generalMethods.canAccessCotMG;
  sourceFileLvl = generalMethods.sourceFileLvl;
  activeSourceFileLvl = generalMethods.activeSourceFileLvl;
  applyEntropy = augmentationMethods.applyEntropy;
  focusPenalty = generalMethods.focusPenalty;
  initInfiltration = generalMethods.initInfiltration;

  constructor() {
    super();
    // Let's get a hash of some semi-random stuff so we have something unique.
    this.identifier = cyrb53(
      "I-" +
        new Date().getTime() +
        navigator.userAgent +
        window.innerWidth +
        window.innerHeight +
        getRandomIntInclusive(100, 999),
    );
    this.lastAugReset = this.lastNodeReset = Date.now();
  }

  travelCostMoneySource(): MoneySource {
    return "other";
  }

  whoAmI(): string {
    return "Player";
  }

  sleevesSupportingBladeburner(): Sleeve[] {
    return this.sleeves.filter((s) => isSleeveSupportWork(s.currentWork));
  }

  /** Serialize the current object to a JSON save state. */
  toJSON(): IReviverValue {
    // For the time being, infiltration is not part of the save.
    return Generic_toJSON("PlayerObject", this, getKeyList(PlayerObject, { removedKeys: ["infiltration"] }));
  }

  /** Initializes a PlayerObject object from a JSON save state. */
  static fromJSON(value: IReviverValue): PlayerObject {
    const player = Generic_fromJSON(PlayerObject, value.data);
    // Any statistics that could be infinite would be serialized as null (JSON.stringify(Infinity) is "null")
    player.hp = { current: player.hp?.current ?? 10, max: player.hp?.max ?? 10 };
    player.money ??= 0;
    // Just remove from the save file any augs that have invalid name
    player.augmentations = player.augmentations.filter((ownedAug) => isMember("AugmentationName", ownedAug.name));
    player.queuedAugmentations = player.queuedAugmentations.filter((ownedAug) =>
      isMember("AugmentationName", ownedAug.name),
    );
    player.updateSkillLevels();
    // Conversion code for Player.sourceFiles is here instead of normal save conversion area because it needs
    // to happen earlier for use in the savegame comparison tool.
    if (Array.isArray(player.sourceFiles)) {
      // Expect pre-2.3 sourcefile format here.
      type OldSourceFiles = { n: number; lvl: number }[];
      player.sourceFiles = new JSONMap((player.sourceFiles as OldSourceFiles).map(({ n, lvl }) => [n, lvl]));
    }
    // Remove any invalid jobs
    for (const [loadedCompanyName, loadedJobName] of Object.entries(player.jobs)) {
      if (!isMember("CompanyName", loadedCompanyName) || !isMember("JobName", loadedJobName)) {
        // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
        delete player.jobs[loadedCompanyName as CompanyName];
      }
    }
    // A bug created ill-formed UTF-16 darknet hostnames that caused the in-game editor to crash. Player.currentServer
    // may point to one of these invalid hostnames. This code migrates the invalid hostnames and protects against
    // similar issues in the future.
    if (!player.currentServer.isWellFormed()) {
      player.currentServer = player.currentServer.toWellFormed();
    }
    return player;
  }
}

setPlayer(new PlayerObject());

constructorsForReviver.PlayerObject = PlayerObject;
