import { Player } from "@player";
import { AugmentationName, FactionName } from "@enums";
import { Fragment } from "./Fragment";
import { ActiveFragment } from "./ActiveFragment";
import { FragmentTypeEnum } from "./FragmentType";
import { BaseGift } from "./BaseGift";
import { Factions } from "../Faction/Factions";
import { CalculateEffect } from "./formulas/effect";
import { StaneksGiftEvents } from "./StaneksGiftEvents";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { StanekConstants } from "./data/Constants";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { defaultMultipliers, mergeMultipliers, Multipliers, scaleMultipliers } from "../PersonObjects/Multipliers";
import { Augmentations } from "../Augmentation/Augmentations";
import { getKeyList } from "../utils/helpers/getKeyList";

export class StaneksGift extends BaseGift {
  isBonusCharging = false;
  justCharged = true;
  storedCycles = 0;

  baseSize(): number {
    return StanekConstants.BaseSize + currentNodeMults.StaneksGiftExtraSize + Player.activeSourceFileLvl(13);
  }

  width(): number {
    return Math.max(2, Math.min(Math.floor(this.baseSize() / 2 + 1), StanekConstants.MaxSize));
  }
  height(): number {
    return Math.max(3, Math.min(Math.floor(this.baseSize() / 2 + 0.6), StanekConstants.MaxSize));
  }

  charge(af: ActiveFragment, threads: number): void {
    if (threads > af.highestCharge) {
      af.numCharge = (af.highestCharge * af.numCharge) / threads + 1;
      af.highestCharge = threads;
    } else {
      af.numCharge += threads / af.highestCharge;
    }

    const cotmg = Factions[FactionName.ChurchOfTheMachineGod];
    cotmg.playerReputation += (Player.mults.faction_rep * (Math.pow(threads, 0.95) * (cotmg.favor + 100))) / 1000;
    this.justCharged = true;
  }

  inBonus(): boolean {
    return this.storedCycles >= 5;
  }

  process(numCycles = 1): void {
    if (!Player.hasAugmentation(AugmentationName.StaneksGift1)) return;
    this.storedCycles += numCycles;
    const usedCycles = this.isBonusCharging ? 5 : 1;
    this.isBonusCharging = false;
    this.storedCycles = Math.max(0, this.storedCycles - usedCycles);
    // Only update multipliers (slow) if there was charging done since last process tick.
    if (this.justCharged) {
      Player.applyEntropy(Player.entropy);
      this.justCharged = false;
    }
    StaneksGiftEvents.emit();
  }

  effect(fragment: ActiveFragment): number {
    // Find all the neighboring cells
    const cells = fragment.neighbors();
    // find the neighboring active fragments.
    const maybeFragments = cells.map((n) => this.fragmentAt(n[0], n[1]));

    // Filter out undefined with typescript "Type guard". Whatever
    let neighbors = maybeFragments.filter((v: ActiveFragment | undefined): v is ActiveFragment => !!v);

    neighbors = neighbors.filter((fragment) => fragment.fragment().type === FragmentTypeEnum.Booster);
    let boost = 1;

    neighbors = neighbors.filter((v, i, s) => s.indexOf(v) === i);
    for (const neighboor of neighbors) {
      boost *= neighboor.fragment().power;
    }
    return CalculateEffect(fragment.highestCharge, fragment.numCharge, fragment.fragment().power, boost);
  }

  canPlace(rootX: number, rootY: number, rotation: number, fragment: Fragment): boolean {
    if (rootX < 0 || rootY < 0) return false;
    if (rootX + fragment.width(rotation) > this.width()) return false;
    if (rootY + fragment.height(rotation) > this.height()) return false;
    if (this.count(fragment) >= fragment.limit) return false;
    const newFrag = new ActiveFragment({ x: rootX, y: rootY, rotation: rotation, fragment: fragment });
    for (const aFrag of this.fragments) {
      if (aFrag.collide(newFrag)) return false;
    }
    return true;
  }

  place(rootX: number, rootY: number, rotation: number, fragment: Fragment): boolean {
    if (!this.canPlace(rootX, rootY, rotation, fragment)) return false;
    this.fragments.push(new ActiveFragment({ x: rootX, y: rootY, rotation: rotation, fragment: fragment }));
    return true;
  }

  findFragment(rootX: number, rootY: number): ActiveFragment | undefined {
    return this.fragments.find((f) => f.x === rootX && f.y === rootY);
  }

  count(fragment: Fragment): number {
    let amt = 0;
    for (const aFrag of this.fragments) {
      if (aFrag.fragment().id === fragment.id) amt++;
    }
    return amt;
  }

  delete(rootX: number, rootY: number): boolean {
    for (let i = 0; i < this.fragments.length; i++) {
      if (this.fragments[i].x === rootX && this.fragments[i].y === rootY) {
        this.fragments.splice(i, 1);
        return true;
      }
    }

    return false;
  }

  clear(): void {
    this.fragments = [];
  }

  clearCharge(): void {
    this.fragments.forEach((f) => {
      f.highestCharge = 0;
      f.numCharge = 0;
    });
  }

  calculateMults(): Multipliers {
    const mults = defaultMultipliers();
    for (const aFrag of this.fragments) {
      const fragment = aFrag.fragment();

      const power = this.effect(aFrag);
      switch (fragment.type) {
        case FragmentTypeEnum.HackingSpeed:
          mults.hacking_speed *= power;
          break;
        case FragmentTypeEnum.HackingMoney:
          mults.hacking_money *= power;
          break;
        case FragmentTypeEnum.HackingGrow:
          mults.hacking_grow *= power;
          break;
        case FragmentTypeEnum.Hacking:
          mults.hacking *= power;
          mults.hacking_exp *= power;
          break;
        case FragmentTypeEnum.Strength:
          mults.strength *= power;
          mults.strength_exp *= power;
          break;
        case FragmentTypeEnum.Defense:
          mults.defense *= power;
          mults.defense_exp *= power;
          break;
        case FragmentTypeEnum.Dexterity:
          mults.dexterity *= power;
          mults.dexterity_exp *= power;
          break;
        case FragmentTypeEnum.Agility:
          mults.agility *= power;
          mults.agility_exp *= power;
          break;
        case FragmentTypeEnum.Charisma:
          mults.charisma *= power;
          mults.charisma_exp *= power;
          break;
        case FragmentTypeEnum.HacknetMoney:
          mults.hacknet_node_money *= power;
          break;
        case FragmentTypeEnum.HacknetCost:
          mults.hacknet_node_purchase_cost /= power;
          mults.hacknet_node_ram_cost /= power;
          mults.hacknet_node_core_cost /= power;
          mults.hacknet_node_level_cost /= power;
          break;
        case FragmentTypeEnum.Rep:
          mults.company_rep *= power;
          mults.faction_rep *= power;
          break;
        case FragmentTypeEnum.WorkMoney:
          mults.work_money *= power;
          break;
        case FragmentTypeEnum.Crime:
          mults.crime_success *= power;
          mults.crime_money *= power;
          break;
        case FragmentTypeEnum.Bladeburner:
          mults.bladeburner_max_stamina *= power;
          mults.bladeburner_stamina_gain *= power;
          mults.bladeburner_analysis *= power;
          mults.bladeburner_success_chance *= power;
          break;
      }
    }
    return mults;
  }

  updateMults(): void {
    const mults = this.calculateMults();
    Player.mults = mergeMultipliers(Player.mults, mults);
    Player.updateSkillLevels();
    const zoeAmt = Player.sleeves.reduce((n, sleeve) => n + (sleeve.hasAugmentation(AugmentationName.ZOE) ? 1 : 0), 0);
    if (zoeAmt === 0) return;
    // Less powerful for each copy.
    const scaling = 3 / (zoeAmt + 2);
    const sleeveMults = scaleMultipliers(mults, scaling);
    for (const sleeve of Player.sleeves) {
      if (!sleeve.hasAugmentation(AugmentationName.ZOE)) continue;
      sleeve.resetMultipliers();
      //reapplying augmentation's multiplier
      for (let i = 0; i < sleeve.augmentations.length; ++i) {
        const aug = Augmentations[sleeve.augmentations[i].name];
        sleeve.applyAugmentation(aug);
      }
      //applying stanek multiplier
      sleeve.mults = mergeMultipliers(sleeve.mults, sleeveMults);
      sleeve.updateSkillLevels();
    }
  }

  prestigeAugmentation(): void {
    this.clearCharge();
  }

  prestigeSourceFile(): void {
    this.clear();
    this.storedCycles = 0;
  }

  static includedProperties = getKeyList(StaneksGift, { removedKeys: ["justCharged"] });

  /** Serialize Stanek's Gift to a JSON save state. */
  toJSON(): IReviverValue {
    return Generic_toJSON("StaneksGift", this, StaneksGift.includedProperties);
  }

  /** Initializes Stanek's Gift from a JSON save state */
  static fromJSON(value: IReviverValue): StaneksGift {
    return Generic_fromJSON(StaneksGift, value.data, StaneksGift.includedProperties);
  }
}

constructorsForReviver.StaneksGift = StaneksGift;
