import { CityName, CorpEmployeeJob } from "@enums";
import * as corpConstants from "./data/Constants";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { Division } from "./Division";
import { Corporation } from "./Corporation";
import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive";
import { createEnumKeyedRecord, getRecordKeys } from "../Types/Record";
import { throwIfReachable } from "../utils/helpers/throwIfReachable";

interface IParams {
  city: CityName;
  size: number;
}

export class OfficeSpace {
  city = CityName.Sector12;
  size = 1;

  maxEnergy = 100;
  maxMorale = 100;

  avgEnergy = 75;
  avgMorale = 75;

  avgIntelligence = 75;
  avgCharisma = 75;
  avgCreativity = 75;
  avgEfficiency = 75;

  totalExperience = 0;
  numEmployees = 0;
  totalSalary = 0;

  autoTea = false;
  autoParty = false;
  teaPending = false;
  partyMult = 1;

  employeeProductionByJob = { total: 0, ...createEnumKeyedRecord(CorpEmployeeJob, () => 0) };
  employeeJobs = createEnumKeyedRecord(CorpEmployeeJob, () => 0);
  employeeNextJobs = createEnumKeyedRecord(CorpEmployeeJob, () => 0);

  constructor(params: IParams | null = null) {
    if (!params) return;
    this.city = params.city;
    this.size = params.size;
  }

  atCapacity(): boolean {
    return this.numEmployees >= this.size;
  }

  process(marketCycles = 1, corporation: Corporation, industry: Division): number {
    // HRBuddy AutoRecruitment and Interning
    if (industry.hasResearch("HRBuddy-Recruitment") && !this.atCapacity()) {
      this.hireRandomEmployee(
        industry.hasResearch("HRBuddy-Training") ? CorpEmployeeJob.Intern : CorpEmployeeJob.Unassigned,
      );
    }

    // Update employee jobs and job counts
    for (const [pos, jobCount] of Object.entries(this.employeeNextJobs) as [CorpEmployeeJob, number][]) {
      this.employeeJobs[pos] = jobCount;
    }

    // Process Office properties
    this.maxEnergy = 100;
    this.maxMorale = 100;

    if (industry.hasResearch("Go-Juice")) this.maxEnergy += 10;
    if (industry.hasResearch("Sti.mu")) this.maxMorale += 10;
    if (industry.hasResearch("AutoBrew")) this.autoTea = true;
    if (industry.hasResearch("AutoPartyManager")) this.autoParty = true;

    if (this.numEmployees > 0) {
      /** Multiplier for employee morale/energy based on company performance */
      let perfMult = 1.002;
      if (this.numEmployees >= 9) {
        perfMult = Math.pow(
          1 +
            0.002 * Math.min(1 / 9, this.employeeJobs.Intern / this.numEmployees - 1 / 9) * 9 -
            (corporation.funds < 0 && industry.lastCycleRevenue < industry.lastCycleExpenses ? 0.001 : 0),
          marketCycles,
        );
      }
      // Flat reduction per cycle.
      // This does not cause a noticable decrease (it's only -.001% per cycle).
      const reduction = 0.002 * marketCycles;

      if (this.autoTea) {
        this.avgEnergy = this.maxEnergy;
      } else {
        // Tea gives a flat +2 to energy
        this.avgEnergy = (this.avgEnergy - reduction * Math.random()) * perfMult + (this.teaPending ? 2 : 0);
      }

      if (this.autoParty) {
        this.avgMorale = this.maxMorale;
      } else {
        // Each 10% multiplier gives an extra flat +1 to morale to make recovering from low morale easier.
        const increase = this.partyMult > 1 ? (this.partyMult - 1) * 10 : 0;
        this.avgMorale = ((this.avgMorale - reduction * Math.random()) * perfMult + increase) * this.partyMult;
      }

      this.avgEnergy = Math.max(Math.min(this.avgEnergy, this.maxEnergy), corpConstants.minEmployeeDecay);
      this.avgMorale = Math.max(Math.min(this.avgMorale, this.maxMorale), corpConstants.minEmployeeDecay);

      this.teaPending = false;
      this.partyMult = 1;
    }

    // Get experience increase; unassigned employees do not contribute, interning employees contribute 10x
    this.totalExperience +=
      0.0015 *
      marketCycles *
      (this.numEmployees -
        this.employeeJobs[CorpEmployeeJob.Unassigned] +
        this.employeeJobs[CorpEmployeeJob.Intern] * 9);

    this.calculateEmployeeProductivity(corporation, industry);
    if (this.numEmployees === 0) {
      this.totalSalary = 0;
    } else {
      this.totalSalary =
        corpConstants.employeeSalaryMultiplier *
        marketCycles *
        this.numEmployees *
        (this.avgIntelligence +
          this.avgCharisma +
          this.totalExperience / this.numEmployees +
          this.avgCreativity +
          this.avgEfficiency);
    }
    return this.totalSalary;
  }

  calculateEmployeeProductivity(corporation: Corporation, industry: Division): void {
    const effCre = this.avgCreativity * corporation.getEmployeeCreMultiplier() * industry.getEmployeeCreMultiplier(),
      effCha = this.avgCharisma * corporation.getEmployeeChaMult() * industry.getEmployeeChaMultiplier(),
      effInt = this.avgIntelligence * corporation.getEmployeeIntMult() * industry.getEmployeeIntMultiplier(),
      effEff = this.avgEfficiency * corporation.getEmployeeEffMult() * industry.getEmployeeEffMultiplier();
    const prodBase = this.avgMorale * this.avgEnergy * 1e-4;

    let total = 0;
    const exp = this.totalExperience / this.numEmployees || 0;
    for (const name of getRecordKeys(this.employeeProductionByJob)) {
      let prodMult = 0;
      switch (name) {
        case CorpEmployeeJob.Operations:
          prodMult = 0.6 * effInt + 0.1 * effCha + exp + 0.5 * effCre + effEff;
          break;
        case CorpEmployeeJob.Engineer:
          prodMult = effInt + 0.1 * effCha + 1.5 * exp + effEff;
          break;
        case CorpEmployeeJob.Business:
          prodMult = 0.4 * effInt + effCha + 0.5 * exp;
          break;
        case CorpEmployeeJob.Management:
          prodMult = 2 * effCha + exp + 0.2 * effCre + 0.7 * effEff;
          break;
        case CorpEmployeeJob.RandD:
          prodMult = 1.5 * effInt + 0.8 * exp + effCre + 0.5 * effEff;
          break;
        case CorpEmployeeJob.Unassigned:
        case CorpEmployeeJob.Intern:
        case "total":
          continue;
        default:
          throwIfReachable(name);
      }
      this.employeeProductionByJob[name] = this.employeeJobs[name] * prodMult * prodBase;
      total += this.employeeProductionByJob[name];
    }

    this.employeeProductionByJob.total = total;
  }

  hireRandomEmployee(position: CorpEmployeeJob): boolean {
    if (this.atCapacity()) return false;

    this.totalExperience += getRandomIntInclusive(50, 100);

    this.avgMorale = (this.avgMorale * this.numEmployees + getRandomIntInclusive(50, 100)) / (this.numEmployees + 1);
    this.avgEnergy = (this.avgEnergy * this.numEmployees + getRandomIntInclusive(50, 100)) / (this.numEmployees + 1);

    this.avgIntelligence =
      (this.avgIntelligence * this.numEmployees + getRandomIntInclusive(50, 100)) / (this.numEmployees + 1);
    this.avgCharisma =
      (this.avgCharisma * this.numEmployees + getRandomIntInclusive(50, 100)) / (this.numEmployees + 1);
    this.avgCreativity =
      (this.avgCreativity * this.numEmployees + getRandomIntInclusive(50, 100)) / (this.numEmployees + 1);
    this.avgEfficiency =
      (this.avgEfficiency * this.numEmployees + getRandomIntInclusive(50, 100)) / (this.numEmployees + 1);

    ++this.numEmployees;
    ++this.employeeJobs[position];
    ++this.employeeNextJobs[position];
    return true;
  }

  autoAssignJob(job: CorpEmployeeJob, target: number): boolean {
    if (job === CorpEmployeeJob.Unassigned) {
      throw new Error("internal autoAssignJob function called with EmployeePositions.Unassigned");
    }
    const diff = target - this.employeeNextJobs[job];

    if (diff === 0) return true;
    // We are already at the desired number
    else if (diff <= this.employeeNextJobs[CorpEmployeeJob.Unassigned]) {
      // This covers both a negative diff (reducing the amount of employees in position) and a positive (increasing and using up unassigned employees)
      this.employeeNextJobs[CorpEmployeeJob.Unassigned] -= diff;
      this.employeeNextJobs[job] = target;
      return true;
    }
    return false;
  }

  getTeaCost(): number {
    return corpConstants.teaCostPerEmployee * this.numEmployees;
  }

  setTea(): boolean {
    if (!this.teaPending && !this.autoTea && this.numEmployees > 0) {
      this.teaPending = true;
      return true;
    }
    return false;
  }

  setParty(mult: number): boolean {
    if (mult > 1 && this.partyMult === 1 && !this.autoParty && this.numEmployees > 0) {
      this.partyMult = mult;
      return true;
    }
    return false;
  }

  toJSON(): IReviverValue {
    return Generic_toJSON("OfficeSpace", this);
  }

  static fromJSON(value: IReviverValue): OfficeSpace {
    return Generic_fromJSON(OfficeSpace, value.data);
  }
}

constructorsForReviver.OfficeSpace = OfficeSpace;
