import RegionBehaviorType from "./base.mjs";
import {REGION_EVENTS} from "@common/constants.mjs";
import * as fields from "@common/data/fields.mjs";
import {fromUuid} from "@client/utils/helpers.mjs";
import {modifyBatch} from "@client/documents/_module.mjs";

/**
 * @import {ActiveEffectData, RegionTokenEnterEvent, RegionTokenExitEvent} from "@client/documents/_types.mjs";
 * @import {ActiveEffect, Actor} from "@client/documents/_module.mjs";
 */

/**
 * The data model for a behavior that applies Active Effects to Tokens within the Region.
 *
 * This is a behavior that applies the configured Active Effects to the Token's Actor when the Token enters the region.
 * Once the Token exists the Region, these Active Effects are removed from the Token's Actor.
 *
 * For example, this behavior could apply a Slowed effect in a blizzard region, or grant extra cover and concealment
 * in areas of tall grass.
 *
 * @property {Set<ActiveEffect>} effects    The Active Effects that are applied to Token within the Region.
 */
export default class ApplyActiveEffectRegionBehaviorType extends RegionBehaviorType {

  /** @override */
  static LOCALIZATION_PREFIXES = ["BEHAVIOR.TYPES.applyActiveEffect", "BEHAVIOR.TYPES.base"];

  /* ---------------------------------------- */

  /** @override */
  static defineSchema() {
    return {
      effects: new fields.SetField(new fields.DocumentUUIDField({type: "ActiveEffect", nullable: false}))
    };
  }

  /* ---------------------------------------- */

  /**
   * Apply the Active Effects when the Token enters the Region.
   * @param {RegionTokenEnterEvent} event
   * @this {ApplyActiveEffectRegionBehaviorType}
   */
  static async #onTokenEnter(event) {
    if ( !event.user.isSelf ) return;
    const {token, movement} = event.data;
    const actor = token.actor;
    if ( !actor ) return;
    const resumeMovement = movement ? token.pauseMovement() : undefined;
    const effects = await Promise.all(this.effects.map(fromUuid));
    const toCreate = this.#getEffectsToCreate(actor, effects);
    if ( toCreate.length ) await actor.createEmbeddedDocuments("ActiveEffect", toCreate);
    await resumeMovement?.();
  }

  /* ---------------------------------------- */

  /**
   * Un-apply the Active Effects when the Token exists the Region.
   * @param {RegionTokenExitEvent} event
   * @this {ApplyActiveEffectRegionBehaviorType}
   */
  static async #onTokenExit(event) {
    if ( !event.user.isSelf ) return;
    const {token, movement} = event.data;
    const actor = token.actor;
    if ( !actor ) return;
    const toDelete = this.#getEffectsToDelete(actor);
    if ( !toDelete.length ) return;
    const resumeMovement = movement ? token.pauseMovement() : undefined;
    await actor.deleteEmbeddedDocuments("ActiveEffect", toDelete);
    await resumeMovement?.();
  }

  /* ---------------------------------------- */

  /** @override */
  static events = {
    [REGION_EVENTS.TOKEN_ENTER]: this.#onTokenEnter,
    [REGION_EVENTS.TOKEN_EXIT]: this.#onTokenExit
  };

  /* ---------------------------------------- */

  /** @inheritDoc */
  _onUpdate(changed, options, userId) {
    super._onUpdate(changed, options, userId);
    if ( !this.behavior.active || !("system" in changed) ) return;
    this.#recreateEffectsForAllTokens();
  }

  /* ---------------------------------------- */

  /**
   * Recreate the effects of tokens within the region.
   * @returns {Promise<void>}
   */
  async #recreateEffectsForAllTokens() {
    const effects = await Promise.all(this.effects.map(fromUuid));
    const operations = [];
    for ( const token of this.region.tokens ) {
      const actor = token.actor;
      if ( !actor ) continue;
      const toDelete = this.#getEffectsToDelete(actor);
      if ( toDelete.length ) {
        operations.push({
          action: "delete",
          documentName: "ActiveEffect",
          ids: toDelete,
          parent: actor
        });
      }
      const toCreate = this.#getEffectsToCreate(actor, effects);
      if ( toCreate.length ) {
        operations.push({
          action: "create",
          documentName: "ActiveEffect",
          data: toCreate,
          parent: actor
        });
      }
    }
    await modifyBatch(operations);
  }

  /* ---------------------------------------- */

  /**
   * Get Active Effects that should be created for the Actor.
   * @param {Actor} actor             The actor the Active Effects should be created for.
   * @param {ActiveEffect[]} effects  The effects the Active Effects are created from.
   * @returns {ActiveEffectData[]}    The data of Active Effects that should be created.
   */
  #getEffectsToCreate(actor, effects) {
    const toCreate = [];
    for ( const effect of effects ) {
      const data = effect.toObject();
      delete data._id;
      if ( effect.compendium ) {
        data._stats.duplicateSource = null;
        data._stats.compendiumSource = effect.uuid;
      } else {
        data._stats.duplicateSource = effect.uuid;
        data._stats.compendiumSource = null;
      }
      data._stats.exportSource = null;
      data.origin = this.behavior.uuid;
      toCreate.push(data);
    }
    return toCreate;
  }

  /* ---------------------------------------- */

  /**
   * Get Active Effects that should be deleted from the Actor.
   * @param {Actor} actor  The actor the Active Effects should be deleted from.
   * @returns {string[]}   The IDs of Active Effects that should be deleted.
   */
  #getEffectsToDelete(actor) {
    return actor.effects.reduce((ids, effect) => {
      if ( effect.origin === this.behavior.uuid ) ids.push(effect.id);
      return ids;
    }, []);
  }
}
