// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT

import { packageId } from '../../constants';
import { getGame } from '../../helpers';

export const registerCombatantFunctionality = () => {
  CONFIG.Combatant.documentClass = CombatantMixin(CONFIG.Combatant.documentClass);
};

/**
 * Enhance a co,batant class with functionality for the Tickwerk.
 * @param {typeof Combatant} BaseCombatant  The combat class to enhance
 * @returns A combat class, adapted for the Tickwerk
 */
const CombatantMixin = (BaseCombatant) => {
  return class TickwerkCombatant extends BaseCombatant {
    /**
     * An temporary property to make changes to the initiative available to other instances in their `_pre…` methods.
     * @type {number|null|undefined}
     */
    _newInitiative;

    /**
     * An temporary property to make changes to the tiebreaker available to other instances in their `_pre…` methods.
     * @type {number|undefined}
     */
    _newTiebreaker;

    /***
     * Is this combatant currently waiting?
     * @type {boolean}
     */
    get waiting() {
      return this.getFlag(packageId, 'waiting') ?? false;
    }

    /**
     * Toggle the waiting state of this combatant.
     * @returns {Promise<this|undefined>} The updated combatant
     */
    toggleWaiting() {
      const update = { [`flags.${packageId}.waiting`]: !this.waiting };
      if (this.parent?.started && this.waiting) update.initiative = this.parent?.tickValue;
      return this.update(update);
    }

    /**
     * Advance for the given number of ticks.
     * @param {number} ticks  The number of ticks to advance for
     * @returns {Promise<void>} A promise that resolves once when the combatant has advanced
     */
    async advanceTicks(ticks) {
      if (this.initiative === null) {
        ui.notifications?.warn('TICKWERK.WarningCannotAdvanceWithoutTickValue', { localize: true });
        return;
      }
      if (this.waiting) {
        ui.notifications?.warn('TICKWERK.WarningCannotAdvanceWhileWaiting', { localize: true });
        return;
      }
      if (!this.combat?.started) {
        ui.notifications?.warn('TICKWERK.WarningCannotAdvanceWithoutStartedCombat', { localize: true });
        return;
      }

      await this.update({ initiative: this.initiative + ticks });
      const tickTime = CONFIG.tickwerk?.tickTime;
      const advanceTime = tickTime !== undefined ? ticks * tickTime : undefined;
      if (advanceTime !== 0) {
        await this.combat?.update(undefined, { diff: false, advanceTime });
      }
    }

    /**
     * Show a dialog for advancing the combatant a certain number of ticks.
     * @returns {Promise<void>} A promise that resolves when the dialog has been confirmed and the combatant has advanced
     */
    async advanceTicksDialog() {
      const game = getGame();
      const id = foundry.utils.randomID();

      const form = `<form><div class="form-group">
      <label for="ticks-${id}">${game.i18n.localize('TICKWERK.NumberOfTicks')}</label>
      <input id="ticks-${id}" name="ticks" type="number" value="5" required />
      </div></form>`;

      const ticks = await Dialog.confirm({
        title: game.i18n.localize('TICKWERK.AdvanceTicks'),
        content: form,
        yes: (html) => {
          const ticks = html[0]?.querySelector('input[name="ticks"]')?.value;
          const parsedTicks = ticks !== undefined ? parseInt(ticks) : undefined;
          return Number.isSafeInteger(parsedTicks) ? parsedTicks : NaN;
        },
        rejectClose: false,
      });

      if (Number.isNaN(ticks)) {
        ui.notifications?.warn('TICKWERK.WarningInvalidNumberOfTicks', { localize: true });
        return;
      }

      if (ticks !== null && ticks !== false) {
        await this.advanceTicks(ticks);
      }
    }

    /**
     * Update tiebreaker data for a given creation or update.
     * @param {object} data The data of the creation / update
     */
    async #updateTiebreakerData(data) {
      const waiting = data.flags?.[packageId]?.waiting;
      if ('initiative' in data || waiting !== undefined) {
        const newInitiative = data.initiative ?? this.initiative;
        const combatantsWithSameTickValue =
          this.parent?.combatants.filter((combatant) => {
            const otherInitiative =
              combatant._newInitiative !== undefined ? combatant._newInitiative : combatant.initiative;
            return otherInitiative === newInitiative && combatant !== this;
          }) ?? [];
        const tiebreaker = await this.#getTiebreaker(combatantsWithSameTickValue, waiting);
        foundry.utils.setProperty(data, `flags.${packageId}.tiebreaker`, tiebreaker);
        this._newInitiative = data.initiative;
        this._newTiebreaker = tiebreaker;
      }
    }

    /**
     * Get a tiebreaker between this combatant and the given other combatants.
     * @param {TickwerkCombatant[]} combatants  The other combatants among which to find a tiebreaker
     * @param {boolean | undefined} waiting     The change of the waiting state of the combatanmt
     * @returns {Promise<number>} A promise that resolves to the tiebreaker
     */
    async #getTiebreaker(combatants, waiting) {
      const getTiebreaker = CONFIG.tickwerk?.getTiebreaker ?? defaultGetTiebreaker;
      return getTiebreaker(this, combatants, waiting);
    }

    /** @override */
    testUserPermission(user, permission, { exact } = {}) {
      if (user.isGM) return true;
      return super.testUserPermission(user, permission, { exact });
    }

    /** @override */
    _getInitiativeFormula() {
      const getInitiativeFormula = CONFIG.tickwerk?.getInitiativeFormula;
      if (getInitiativeFormula) return getInitiativeFormula(this);
      return super._getInitiativeFormula();
    }

    /** @override */
    async _preCreate(...args) {
      await super._preCreate(...args);
      await this.#updateTiebreakerData(args[0]);
    }

    /** @override */
    async _preUpdate(...args) {
      await super._preUpdate(...args);
      await this.#updateTiebreakerData(args[0]);
    }

    /** @override */
    _onCreate(...args) {
      super._onCreate(...args);
      this._newInitiative = undefined;
      this._newTiebreaker = undefined;
    }

    /** @override */
    _onUpdate(...args) {
      super._onUpdate(...args);
      this._newInitiative = undefined;
      this._newTiebreaker = undefined;
    }
  };
};

/**
 * A function to get a tiebreaker for a combatant
 * @typedef {(combatant: TickwerkCombatant, combatants: TickwerkCombatant[], waiting: boolean | undefined) => Promise<number>} GetTiebreaker
 */

/**
 * Default implementation to get a tiebreaker for a combatant.
 * @type {GetTiebreaker}
 */
export const defaultGetTiebreaker = async (combatant, combatants) => {
  if (combatants.length === 0) return 0;
  const tiebreakers = combatants.map((combatant) => {
    return (
      (combatant._newTiebreaker !== undefined
        ? combatant._newTiebreaker
        : combatant.getFlag(packageId, 'tiebreaker')) ?? 0
    );
  });
  return Math.max(...tiebreakers) + 1;
};
