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

import { mathEvaluator } from "../expression-evaluation/evaluator";
import { getGame } from "../utils/utils";

/**
 * @typedef {object} ItemEffectConfig
 * @property {boolean} [applyToItems]   Whether or not to apply this effect to owned items instead of the actor
 * @property {string} [itemName]        Only apply this effect to items with this name
 * @property {string} [condition]       Only apply this effect to items where this condition is fullfilled
 */

/**
 * @typedef {object} DS4ActiveEffectFlags
 * @property {ItemEffectConfig} [itemEffectConfig]  Configuration for applying this effect to owned items
 */

/**
 * @typedef {Record<string, unknown>} ActiveEffectFlags
 * @property {DS4ActiveEffectFlags} [ds4]   Flags for DS4
 */

export class DS4ActiveEffect extends ActiveEffect {
  /**
   * A fallback icon that can be used if no icon is defined for the effect.
   */
  static FALLBACK_ICON = "icons/svg/aura.svg";

  /**
   * A cached reference to the source document to avoid recurring database lookups
   * @type {foundry.abstract.Document | undefined | null}
   * @protected
   */
  source = undefined;

  /** @override */
  get isSuppressed() {
    const originatingItem = this.originatingItem;
    if (!originatingItem) {
      return false;
    }
    return originatingItem.isNonEquippedEuipable();
  }

  /**
   * The item which this effect originates from if it has been transferred from an item to an actor.
   * @return {import("./item/item").DS4Item | undefined}
   */
  get originatingItem() {
    if (this.parent instanceof Item) {
      return this.parent;
    }
    return undefined;
  }

  /**
   * The number of times this effect should be applied.
   * @type {number}
   */
  get factor() {
    return this.originatingItem?.activeEffectFactor ?? 1;
  }

  /** @override */
  apply(document, change) {
    change.value = Roll.replaceFormulaData(change.value, document);
    try {
      change.value = DS4ActiveEffect.safeEval(change.value).toString();
    } catch (e) {
      // this is a valid case, e.g., if the effect change simply is a string
    }
    return super.apply(document, change);
  }

  /**
   * Create a new {@link DS4ActiveEffect} using default values.
   *
   * @param {import("./item/item").DS4Item | import("./actor/actor").DS4Actor} parent The parent of the effect.
   * @returns {Promise<DS4ActiveEffect | undefined>} A promise that resolved to the created effect or udifined of the
   *                                                 creation was prevented.
   */
  static async createDefault(parent) {
    const createData = {
      name: getGame().i18n.localize(`DS4.NewEffectName`),
      icon: this.FALLBACK_ICON,
    };

    return this.create(createData, { parent });
  }

  /**
   * Safely evaluate a mathematical expression.
   * @param {string} expression   The expression to evaluate
   * @returns {number | `${number | boolean}`} The numeric result of the expression
   * @throws If the expression could not be evaluated or did not produce a numeric resilt
   */
  static safeEval(expression) {
    const result = mathEvaluator.evaluate(expression);
    if (!Number.isNumeric(result)) {
      throw new Error(`mathEvaluator.evaluate produced a non-numeric result from expression "${expression}"`);
    }
    return result;
  }

  /**
   * Apply the given effects to the gicen Actor or item.
   * @param {import("./item/item").DS4Item | import("./actor/actor").DS4Actor} document   The Actor or Item to which to apply the effects
   * @param {DS4ActiveEffect[]} effetcs                                                   The effects to apply
   * @param {(change: EffectChangeData) => boolean} [predicate=() => true]                Apply only changes that fullfill this predicate
   * @returns {Set<string>} The statuses that are applied by this effect
   */
  static applyEffetcs(document, effetcs, predicate = () => true) {
    /** @type {Record<string, unknown>} */
    const overrides = {};

    /** @type {Set<string>} */
    const statuses = new Set();

    // Organize active effect changes by their application priority
    const changesWithEffect = effetcs.flatMap((e) => {
      if (!e.active) {
        return [];
      }
      for (const statusId of e.statuses) {
        statuses.add(statusId);
      }
      return e.getFactoredChangesWithEffect(predicate);
    });
    changesWithEffect.sort((a, b) => (a.change.priority ?? 0) - (b.change.priority ?? 0));

    // Apply all changes
    for (const changeWithEffect of changesWithEffect) {
      if (!changeWithEffect.change.key) continue;
      const changes = changeWithEffect.effect.apply(document, changeWithEffect.change);
      Object.assign(overrides, changes);
    }

    // Expand the set of final overrides
    document.overrides = foundry.utils.expandObject({
      ...foundry.utils.flattenObject(document.overrides),
      ...overrides,
    });

    return statuses;
  }

  /**
   * Get the array of changes for this effect, considering the {@link DS4ActiveEffect#factor}.
   * @param {(change: EffectChangeData) => boolean} [predicate=() => true]    An optional predicate to filter which changes should be considered
   * @returns {EffectChangeDataWithEffect[]} The array of changes from this effect, considering the factor.
   * @protected
   */
  getFactoredChangesWithEffect(predicate = () => true) {
    return this.changes.filter(predicate).flatMap((change) => {
      const c = foundry.utils.deepClone(change);
      c.priority = c.priority ?? c.mode * 10;
      return Array(this.factor).fill({ effect: this, change: c });
    });
  }
}

/**
 * @typedef {object} EffectChangeDataWithEffect
 * @property {DS4ActiveEffect} effect
 * @property {EffectChangeData} change
 */

/**
 * @typedef {object} EffectChangeData
 * @property {string} key                 The attribute path in the Actor or Item data which the change modifies
 * @property {string} value               The value of the change effect
 * @property {number} mode                The modification mode with which the change is applied
 * @property {number} priority            The priority level with which this change is applied
 */
