import DataModel from "@common/abstract/data.mjs";
import {StringField} from "@common/data/fields.mjs";

/**
 * @import { DataField } from "@common/data/fields.mjs";
 * @import { DataFieldOptions, DataFieldContext, DataModelConstructionContext } from "@common/data/_types.mjs"
 * @import { VFXCanvasContainer } from '../canvas/vfx-canvas-container.mjs'
 */

/**
 * Will be extended later to support spritesheet frames and animations
 * @typedef {string} VFXTextureConfig
 **/

/**
 * @template {object} ModelData
 * @extends {foundry.abstract.DataModel<ModelData, DataModelConstructionContext>}
 */
export default class VFXComponent extends DataModel {

  /**
   * The type of this component. Must be overridden in the subclass.
   * @type {string}
   */
  static TYPE = "";

  /** @override */
  static defineSchema() {
    return {
      type: new StringField({
        required: true,
        blank: false,
        initial: this.TYPE,
        validate: value => value === this.TYPE,
        validationError: `must be equal to "${this.TYPE}"`
      })
    };
  }

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

  /**
   * A component-specific sub-timeline.
   * @type {animejs.Timeline}
   */
  get timeline() {
    return this.#timeline;
  }

  #timeline;

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

  /**
   * Have the materials for this component been loaded?
   * @type {boolean}
   */
  get loaded() {
    return this.#loaded;
  }

  #loaded = false;

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

  /**
   * Is the animation for this component playing?
   * @returns {boolean}
   */
  get playing() {
    return this.#timeline?.began && !this.#timeline.completed;
  }

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

  /**
   * Asset paths required to be loaded for this component.
   * @type {Set<string>}
   */
  get assetPaths() {
    return this.#assetPaths;
  }

  #assetPaths = new Set();

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

  /**
   * A registry of display objects which are managed by this component.
   * @type {Record<string, PIXI.DisplayObject[]>}
   */
  get managedDisplayObjects() {
    return this.#managedDisplayObjects;
  }

  #managedDisplayObjects = {primary: []};

  /* -------------------------------------------- */
  /*  Component Management Methods                */
  /* -------------------------------------------- */

  /**
   * Adds a DisplayObject to the set of managed primary display objects.
   * Entries in this list will be added to the primary canvas container when the component is attached and
   * removed when the component is destroyed.
   * @template {PIXI.DisplayObject} DisplayObject
   * @param {DisplayObject} object        The object to manage
   * @param {string} [group="primary"]    A canvas group that should contain the object
   * @returns {DisplayObject}
   */
  addManagedDisplayObject(object, group="primary") {
    this.#managedDisplayObjects[group] ||= [];
    this.#managedDisplayObjects[group].push(object);
    return object;
  }

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

  /**
   * Load this component.
   * @returns {Promise<void>}
   */
  async load() {
    if ( this.#loaded ) return;
    await this._load();
    await foundry.canvas.TextureLoader.loader.load(Array.from(this.assetPaths), {displayProgress: false});
    this.#loaded = true;
  }

  /**
   * Perform subclass-specific loading steps to prepare assets for rendering.
   * @protected
   * @returns {Promise<void>}
   */
  async _load() {}

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

  /**
   * Prepare the timeline and create display objects used by this component.
   * Components should override the _draw method to implement subclass-specific steps.
   * @returns {Promise<void>}
   */
  async draw() {
    this.#timeline = animejs.createTimeline({autoplay: false});
    this.#timeline.label("start");
    await this._draw();

    // Ensure an "end" label exists and that the time required to reach it is reserved
    if ( !Object.hasOwn(this.#timeline.labels, "end") ) this.#timeline.label("end");
    const end = this.#timeline.labels.end;
    if ( end > this.#timeline.iterationDuration ) this.#timeline.add({duration: end}, 0);
    this.#timeline.init();
  }

  /**
   * Perform subclass-specific drawing steps to configure the component timeline and create display objects.
   * @protected
   * @returns {Promise<void>}
   */
  async _draw() {}

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

  /**
   * Attach display objects used by this component to the canvas containers that should render them.
   */
  attach() {
    for ( const [group, objects] of Object.entries(this.#managedDisplayObjects) ) {
      for ( const object of objects ) canvas[group].addChild(object);
    }
    this._attach();
  }

  /**
   * Perform subclass-specific attachment steps to customize how display objects are added to the canvas.
   * @protected
   */
  _attach() {}

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

  /**
   * Stop playback of this component and destroy its contents.
   * @returns {Promise<void>}
   */
  async stop() {
    await this._stop();
    this.#timeline?.complete();
    this._destroy();
  }

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

  /**
   * Cancel playback of this component and destroy its contents.
   * @returns {Promise<void>}
   */
  async cancel() {
    await this._stop();
    this.#timeline?.cancel();
    this._destroy();
  }

  /**
   * Perform subclass-specific steps to discontinue component playback.
   * @protected
   * @returns {Promise<void>}
   */
  async _stop() {}

  /**
   * Perform subclass-specific teardown steps to destroy and dispose of component materials.
   */
  _destroy() {
    for ( const [group, objects] of Object.entries(this.#managedDisplayObjects) ) {
      for ( const object of objects ) {
        if ( object.destroyed ) continue;
        object.removeFromParent();
        object.destroy({children: true});
      }
      this.#managedDisplayObjects[group].length = 0;
    }
  }
}
