Source: WorldState.js

import wsData from 'warframe-worldstate-data';
import { parseDate } from 'warframe-worldstate-data/utilities';

import EarthCycle from './models/EarthCycle.js';
import CetusCycle from './models/CetusCycle.js';
import ConstructionProgress from './models/ConstructionProgress.js';
import VallisCycle from './models/VallisCycle.js';
import ZarimanCycle from './models/ZarimanCycle.js';
import Nightwave from './models/Nightwave.js';
import Kuva from './models/Kuva.js';
import SentientOutpost from './models/SentientOutpost.js';
import CambionCycle from './models/CambionCycle.js';
import SteelPathOffering from './models/SteelPathOffering.js';
import Dependency from './supporting/Dependency.js'; // eslint-disable-line no-unused-vars
import DuviriCycle from './models/DuviriCycle.js';
import WeeklyChallenge from './models/WeeklyChallenge.js';
import Sortie from './models/Sortie.js';
import DuviriChoice from './supporting/DuviriChoice.js';
import VoidTrader from './models/VoidTrader.js';
import PersistentEnemy from './models/PersistentEnemy.js';
import ConclaveChallenge from './models/ConclaveChallenge.js';
import Simaris from './models/Simaris.js';
import DailyDeal from './models/DailyDeal.js';
import DarkSector from './models/DarkSector.js';
import Invasion from './models/Invasion.js';
import FlashSale from './models/FlashSale.js';
import GlobalUpgrade from './models/GlobalUpgrade.js';
import Fissure from './models/Fissure.js';
import SyndicateMission from './models/SyndicateMission.js';
import Alert from './models/Alert.js';
import WorldEvent from './models/WorldEvent.js';
import News from './models/News.js';
import Kinepage from './models/Kinepage.js';
import DeepArchimedea from './models/DeepArchidemea.js';

const { sortieData } = wsData;

const safeArray = (arr) => arr || [];
const safeObj = (obj) => obj || {};

/**
 * Default Dependency object
 * @type {Dependency}
 */
const defaultDeps = {
  sortieData,
  locale: 'en',
  logger: console,
};

/**
 *
 * @param {object} ParserClass class for parsing data
 * @param {Array<BaseContentObject>} dataArray array of raw data
 * @param {Dependency} deps shared dependency object
 * @param {*} [uniqueField] field to treat as unique
 * @returns {WorldstateObject[]} array of parsed objects
 */
export function parseArray(ParserClass, dataArray, deps, uniqueField) {
  const arr = (dataArray || []).map((d) => new ParserClass(d, deps));
  if (uniqueField) {
    const utemp = {};
    arr.sort((a, b) => a.id?.localeCompare(b.id));
    arr.forEach((obj) => {
      utemp[obj[uniqueField]] = obj;
    });
    return Array.from(arr).filter((obj) => {
      if (obj && obj.active && typeof obj.active !== 'undefined') return obj.active;
      /* istanbul ignore next */
      return true;
    });
  }
  return arr;
}

/**
 * Parse array of objects that requires async parsing
 * @param {object} ParserClass class for parsing data - must expose a static build method
 * @param {Array<BaseContentObject>} dataArray array of raw data
 * @param {Dependency} deps shared dependency object
 * @param {*} [uniqueField] field to treat as unique
 * @returns {Promise<WorldstateObject[]>} array of parsed objects
 */
export const parseAsyncArray = async (ParserClass, dataArray, deps, uniqueField) => {
  const arr = [];
  // eslint-disable-next-line no-restricted-syntax
  for await (const d of dataArray ?? []) {
    arr.push(await ParserClass.build(d, deps));
  }
  if (uniqueField) {
    const utemp = {};
    arr.sort((a, b) => a.id?.localeCompare(b.id));
    arr.forEach((obj) => {
      utemp[obj[uniqueField]] = obj;
    });
    return Array.from(arr).filter((obj) => {
      if (obj && obj.active && typeof obj.active !== 'undefined') return obj.active;
      /* istanbul ignore next */
      return true;
    });
  }
  return arr;
};

/**
 * Parses Warframe Worldstate JSON
 */
export class WorldState {
  static async build(json, deps = defaultDeps) {
    const ws = new WorldState(json, deps);
    const data = JSON.parse(json);

    ws.events = await parseAsyncArray(WorldEvent, data.Goals, deps);
    ws.syndicateMissions = await parseAsyncArray(SyndicateMission, data.SyndicateMissions, deps, 'syndicate');

    return ws;
  }

  /**
   * Generates the worldstate json as a string into usable objects
   * @param {string} json The worldstate JSON string
   * @param {Dependency} [deps] The options object
   * @class
   * @async
   */
  constructor(json, deps = defaultDeps) {
    if (typeof json !== 'string') {
      throw new TypeError(`json needs to be a string, provided ${typeof json} : ${JSON.stringify(json)}`);
    }
    const data = JSON.parse(json);
    const tmp = JSON.parse(data.Tmp);

    // eslint-disable-next-line no-param-reassign
    deps = {
      ...defaultDeps,
      ...deps,
    };

    /**
     * The date and time at which the World State was generated
     * @type {Date}
     */
    this.timestamp = new Date(data.Time * 1000);

    /**
     * The in-game news
     * @type {Array.<News>}
     */
    this.news = parseArray(
      News,
      data.Events
        ? data.Events.filter((e) => typeof e.Messages.find((msg) => msg.LanguageCode === deps.locale) !== 'undefined')
        : [],
      deps
    );

    /**
     * The current events
     * @type {Array.<WorldEvent>}
     */
    this.events = [];

    /**
     * The current alerts
     * @type {Array.<Alert>}
     */
    this.alerts = parseArray(Alert, data.Alerts, deps);

    /**
     * The current sortie
     * @type {Sortie}
     */
    [this.sortie] = parseArray(Sortie, data.Sorties, deps);

    /**
     * The current syndicate missions
     * @type {Array.<SyndicateMission>}
     */
    this.syndicateMissions = [];

    /**
     * The current fissures: 'ActiveMissions' & 'VoidStorms'
     * @type {Array.<Fissure>}
     */
    this.fissures = parseArray(Fissure, data.ActiveMissions, deps).concat(parseArray(Fissure, data.VoidStorms, deps));

    /**
     * The current global upgrades
     * @type {Array.<GlobalUpgrade>}
     */
    this.globalUpgrades = parseArray(GlobalUpgrade, data.GlobalUpgrades, deps);

    /**
     * The current flash sales
     * @type {Array.<FlashSale>}
     */
    this.flashSales = parseArray(FlashSale, data.FlashSales, deps);

    /**
     * The current invasions
     * @type {Array.<Invasion>}
     */
    this.invasions = parseArray(Invasion, data.Invasions, deps);

    /**
     * The state of the dark sectors
     * @type {Array.<DarkSector>}
     */
    this.darkSectors = parseArray(DarkSector, data.BadlandNodes, deps);

    /**
     * The state of all Void Traders
     * @type {VoidTrader[]}
     */
    this.voidTraders = parseArray(VoidTrader, data.VoidTraders, deps).sort(
      (a, b) => Date.parse(a.activation) - Date.parse(b.activation)
    );

    /**
     * The state of the Void Trader
     * @type {VoidTrader}
     * @deprecated
     */
    [this.voidTrader] = this.voidTraders;

    /**
     * The current daily deals
     * @type {Array.<DailyDeal>}
     */
    this.dailyDeals = parseArray(DailyDeal, data.DailyDeals, deps);

    /**
     * The state of the sanctuary synthesis targets
     * @type {Simaris}
     */
    this.simaris = new Simaris(safeObj(data.LibraryInfo), deps);

    /**
     * The current conclave challenges
     * @type {Array.<ConclaveChallenge>}
     */
    this.conclaveChallenges = parseArray(ConclaveChallenge, data.PVPChallengeInstances, deps);

    /**
     * The currently active persistent enemies
     * @type {Array.<PersistentEnemy>}
     */
    this.persistentEnemies = parseArray(PersistentEnemy, data.PersistentEnemies, deps);

    /**
     * The current earth cycle
     * @type {EarthCycle}
     */
    this.earthCycle = new EarthCycle(deps);

    const cetusSynd = safeArray(data.SyndicateMissions).filter((syndicate) => syndicate.Tag === 'CetusSyndicate');
    const cetusBountyEnd = parseDate(cetusSynd.length > 0 ? cetusSynd[0].Expiry : { $date: 0 });

    /**
     * The current Cetus cycle
     * @type {CetusCycle}
     */
    this.cetusCycle = new CetusCycle(cetusBountyEnd, deps);

    /**
     * Cambion Drift Cycle
     * @type {CambionCycle}
     */
    this.cambionCycle = new CambionCycle(this.cetusCycle, deps);

    const zarimanSynd = safeArray(data.SyndicateMissions).filter((syndicate) => syndicate.Tag === 'ZarimanSyndicate');
    const zarimanBountyEnd = parseDate(zarimanSynd.length > 0 ? zarimanSynd[0].Expiry : { $date: 0 });

    /**
     * The current Zariman cycle based off current time
     * @type {ZarimanCycle}
     */
    this.zarimanCycle = new ZarimanCycle(zarimanBountyEnd, deps);

    /**
     * Weekly challenges
     * @type {Array.<WeeklyChallenge>}
     */
    this.weeklyChallenges = data.WeeklyChallenges ? new WeeklyChallenge(data.WeeklyChallenges, deps) : [];

    const projectPCTwithOid = data.ProjectPct
      ? {
          ProjectPct: data.ProjectPct,
          _id: {
            $oid: `${Date.now()}${data.ProjectPct[0]}`,
          },
        }
      : undefined;

    /**
     * The Current construction progress for Fomorians/Razorback/etc.
     * @type {ConstructionProgress}
     */
    this.constructionProgress = projectPCTwithOid ? new ConstructionProgress(projectPCTwithOid, deps) : {};

    /**
     * The current Orb Vallis cycle state
     * @type {VallisCycle}
     */
    this.vallisCycle = new VallisCycle(deps);

    if (data.SeasonInfo) {
      /**
       * The current nightwave season
       * @type {Nightwave}
       */
      this.nightwave = new Nightwave(data.SeasonInfo, deps);
    }

    const externalMissions = new Kuva(deps);

    ({
      /**
       * Kuva missions array
       * @type {ExternalMission[]}
       */
      kuva: this.kuva,
      /**
       * Arbitration mission
       * @type {ExternalMission}
       */
      arbitration: this.arbitration,
    } = externalMissions);

    if (!this.arbitration || !Object.keys(this.arbitration).length) {
      this.arbitration = {
        node: 'SolNode000',
        nodeKey: 'SolNode000',
        activation: new Date(0),
        expiry: new Date(8.64e15),
        enemy: 'Tenno',
        type: 'Unknown',
        typeKey: 'Unknown',
        archwing: false,
        sharkwing: false,
      };
    }

    /**
     * Current sentient outposts
     * @type {SentientOutpost}
     */
    this.sentientOutposts = new SentientOutpost(tmp.sfn, deps);

    /**
     * Steel path offering rotation
     * @type {SteelPathOffering}
     */
    this.steelPath = new SteelPathOffering(deps);

    [this.vaultTrader] = parseArray(VoidTrader, data.PrimeVaultTraders, deps);

    /**
     * The current archon hunt
     * @type {Sortie}
     */
    [this.archonHunt] = parseArray(Sortie, data.LiteSorties, deps);

    deps.duviriChoices = parseArray(DuviriChoice, data.EndlessXpChoices, deps);

    this.duviriCycle = new DuviriCycle(deps);

    this.kinepage = new Kinepage(tmp.pgr, deps.locale);

    if (tmp.lqo27) {
      const { activation, expiry } = this.nightwave.activeChallenges.filter((c) => !c.isDaily)[0];

      /**
       * The current Deep Archimedea missions and modifiers
       * @type {DeepArchimedea}
       */
      this.deepArchimedea = new DeepArchimedea(activation, expiry, tmp.lqo27);
    }
  }
}

export default async (json, deps) => WorldState.build(json, deps);