import {AlphaField, ArrayField, NumberField, ObjectField, StringField, SchemaField} from "@common/data/fields.mjs";
import VFXComponent from "../vfx-component.mjs";
import {VFXPointSourcePolygonField, VFXReferenceField} from "../fields/_module.mjs";

/**
 * @import {ParticleGeneratorConfiguration, ParticleGeneratorRotationOptions} from "../../animation/_types.mjs";
 */

/**
 * @typedef VFXParticleGeneratorData
 * @property {string[]} textures                           Texture paths for particles
 * @property {string} mode                                 Generator mode: "ambient" or "effect" (default "effect")
 * @property {number} count                                Target particle count (default 50)
 * @property {object|null} area                            Spawn area; a point, rect, circle, ring, or line object.
 *                                                         Supports reference resolution (e.g. {reference: "target"}).
 * @property {{min: number, max: number|null}} lifetime    Particle lifetime in ms. If max is null, min is used as a
 *                                                         fixed value. Otherwise, spawns with a random lifetime in
 *                                                         [min, max] (default min 1000).
 * @property {{in: number, out: number}|null} fade         Fade-in and fade-out durations in ms (default null)
 * @property {{min: number, max: number}} alpha            Alpha range [min, max] (default 1)
 * @property {{min: number, max: number}} scale            Scale range [min, max] (default 1)
 * @property {{angle: number, speed: number, x: number, y: number}|null} velocity
 *                                                         Initial particle velocity. Specify either x/y components
 *                                                         or speed (px/s) and angle (degrees). (default null)
 * @property {ParticleGeneratorRotationOptions} [rotation] Rotation configuration for particles.
 * @property {number} blend                                PIXI blend mode (default NORMAL = 0)
 * @property {number} elevation                            Layer elevation for the particle container (default 0)
 * @property {number} sort                                 Sort order within the elevation layer (default 0)
 * @property {number} [duration]                           How long the generator runs in ms before soft-stopping
 *                                                         to let existing particles expire. If absent, runs until
 *                                                         the component is explicitly stopped.
 * @property {number} initial                              Proportion of count to spawn immediately on start [0, 1]
 *                                                         (default 0.25)
 * @property {number} spawnRate                            Maximum particles spawned per second during auto-spawn
 *                                                         (default 300)
 * @property {number} [perFrame=5]                         Deprecated since v14. Migrated to `spawnRate`.
 * @property {ParticleGeneratorConfiguration} config
 * Additional ParticleGenerator configuration passed directly to the constructor. Can include orbit, follow,
 * constraints, drift, blur, clip, anchor, callbacks, and any other {@link ParticleGeneratorConfiguration} fields.
 * When effects are built directly in code, this can also include a custom batchable `shaderClass` that is
 * compatible with SpriteMesh and whose plugin was already registered.
 * Values here override any explicitly-set schema fields of the same name.
 * @property {PointSourcePolygon|{x: number, y: number, type: string, radius: number|null}|null} pointSourceMask
 * An optional mask for clipping particles to wall boundaries. Accepts either a pre-computed PointSourcePolygon
 * (shared via VFXReferenceField resolution) or a serializable config object {x, y, type, radius} from which a polygon
 * is computed at draw time. Supports reference resolution for efficient reuse across multiple components within the
 * same effect.
 */

/**
 * A VFX component that creates and manages a {@link foundry.canvas.animation.ParticleGenerator}.
 * Handles the full lifecycle: loading textures, constructing the generator config, starting and
 * stopping the generator in sync with the VFX timeline.
 *
 * The generator always runs with `manual: false` so particles are spawned automatically. Provide
 * a `duration` to limit how long the generator emits; after that time it soft-stops and lets any
 * in-flight particles expire naturally. If no `duration` is given the generator runs indefinitely
 * until the component is stopped externally.
 *
 * @example A burst of sparks at an impact point
 * ```js
 * const vfxConfig = {
 *   name: "sparkBurst",
 *   components: {
 *     sparks: {
 *       type: "particleGenerator",
 *       textures: ["assets/particles/spark.png"],
 *       area: {reference: "target"},
 *       count: 40,
 *       spawnRate: 90,
 *       duration: 1200,
 *       lifetime: {min: 300, max: 700},
 *       fade: {in: 50, out: 200},
 *       scale: {min: 0.2, max: 0.6},
 *       velocity: {speed: 4, angle: 0},
 *       config: {
 *         constraints: {mode: "none"},
 *         drift: {enabled: true, intensity: 0.4}
 *       }
 *     }
 *   },
 *   timeline: [{component: "sparks"}]
 * };
 * const effect = new foundry.canvas.vfx.VFXEffect(vfxConfig);
 * const target = game.user.targets.first();
 * effect.play({target: target.center});
 * ```
 * @extends {VFXComponent<VFXParticleGeneratorData>}
 */
export default class VFXParticleGeneratorComponent extends VFXComponent {

  /** @override */
  static TYPE = "particleGenerator";

  /** @inheritDoc */
  static defineSchema() {
    return {
      ...super.defineSchema(),
      alpha: new SchemaField({
        max: new AlphaField(),
        min: new AlphaField()
      }),
      area: new VFXReferenceField(new ObjectField({required: false, nullable: true, initial: null})),
      blend: new NumberField({nullable: false, initial: 0}),
      count: new VFXReferenceField(new NumberField({required: true, nullable: true, initial: null, positive: true})),
      duration: new NumberField({nullable: false}),
      elevation: new NumberField({required: true, nullable: false, initial: 0}),
      fade: new SchemaField({
        in: new NumberField({required: true, nullable: false, initial: 0}),
        out: new NumberField({required: true, nullable: false, initial: 0})
      }, {required: false, nullable: true, initial: null}),
      initial: new NumberField({required: true, nullable: false, initial: 0.25, min: 0, max: 1}),
      lifetime: new SchemaField({
        max: new NumberField({initial: null, positive: true}),
        min: new NumberField({required: true, nullable: false, initial: 1000, positive: true})
      }),
      mode: new StringField({required: true, blank: false, initial: "effect",
        choices: ["ambient", "effect"]}),
      config: new ObjectField({required: false}),
      spawnRate: new NumberField({required: true, nullable: false, initial: 300, positive: true}),
      rotation: new SchemaField({
        alignVelocity: new foundry.data.fields.BooleanField(),
        initial: new NumberField({nullable: false, initial: 0}),
        spread: new NumberField({nullable: false, initial: Math.PI}),
        speed: new NumberField({nullable: false, initial: 0})
      }, {required: false}),
      scale: new SchemaField({
        max: new NumberField({required: true, nullable: false, initial: 1, positive: true}),
        min: new NumberField({required: true, nullable: false, initial: 1, positive: true})
      }),
      sort: new NumberField({required: true, nullable: false, initial: 0}),
      textures: new ArrayField(new StringField({required: true, blank: false})),
      velocity: new SchemaField({
        angle: new NumberField({nullable: false}),
        speed: new NumberField({nullable: false}),
        x: new NumberField({nullable: false}),
        y: new NumberField({nullable: false})
      }, {required: false, nullable: true, initial: null}),
      pointSourceMask: new VFXPointSourcePolygonField()
    };
  }

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

  /** @inheritDoc */
  static migrateData(data, options) {
    const hasLegacyPerFrame = data.perFrame != null;
    if ( (data.spawnRate == null) && (data.perFrame != null) ) {
      const perFrame = Number(data.perFrame) || 5;
      data.spawnRate = Math.max(0, perFrame) * PIXI.Ticker.targetFPMS * 1000;
    }
    if ( hasLegacyPerFrame && !(data.config && Object.hasOwn(data.config, "particleAnchor")) ) {
      data.config ??= {};
      data.config.particleAnchor = {x: 0.5, y: 0.5};
    }
    delete data.perFrame;
    return super.migrateData(data, options);
  }

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

  /**
   * The active ParticleGenerator instance, populated when playback begins.
   * @type {foundry.canvas.animation.ParticleGenerator|null}
   */
  #generator = null;

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

  /** @inheritDoc */
  async _load() {
    for ( const texture of this.textures ) this.assetPaths.add(texture);
  }

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

  /** @override */
  async _draw() {
    this.timeline.call(() => {
      const textures = this.textures.map(path => foundry.canvas.getTexture(path)).filter(Boolean);
      if ( !textures.length ) {
        console.warn("VFXParticleGeneratorComponent: no valid textures resolved; generator will not start.");
        return;
      }
      const lifetime = (this.lifetime.max !== null) // Normalize lifetime to {min, max} object range
        ? {min: this.lifetime.min, max: this.lifetime.max}
        : this.lifetime.min;
      const hasConfigLegacyPerFrame = !!(this.config && Object.hasOwn(this.config, "perFrame"));
      const hasConfigSpawnRate = !!(this.config && Object.hasOwn(this.config, "spawnRate")
        && (this.config.spawnRate != null));
      const config = {
        alpha: [this.alpha.min, this.alpha.max],
        blend: this.blend,
        count: this.count,
        elevation: this.elevation,
        initial: this.initial,
        lifetime,
        manual: false,
        mode: this.mode,
        spawnRate: this.spawnRate,
        rotation: this.rotation,
        scale: [this.scale.min, this.scale.max],
        sort: this.sort,
        textures,
        ...this.area !== null ? {area: this.area} : {},
        ...this.fade !== null ? {fade: this.fade} : {},
        ...this.velocity !== null ? {velocity: this.velocity} : {},
        ...this.config
      };
      if ( hasConfigLegacyPerFrame && !hasConfigSpawnRate ) delete config.spawnRate;
      // Apply a PointSourcePolygon mask if configured and successfully resolved
      if ( this.pointSourceMask instanceof foundry.canvas.geometry.PointSourcePolygon ) {
        config.mask = this.pointSourceMask;
      } else if ( this.pointSourceMask ) {
        console.warn("VFXParticleGeneratorComponent: pointSourceMask was configured but did not resolve to a"
          + " PointSourcePolygon. No mask applied.", this.pointSourceMask);
      }
      this.#generator = new foundry.canvas.animation.ParticleGenerator(config);
      this.#generator.start();
    }, 0);

    // Finite duration.
    if ( this.duration ) {
      const maxLifetime = this.lifetime.max ?? this.lifetime.min;
      // Soft stop at end of configured duration.
      this.timeline.call(() => this.#generator?.stop({hard: false}), this.duration);
      // Hard stop once all particles have faded out.
      this.timeline.call(() => this.#generator?.stop({hard: true}), this.duration + maxLifetime);
      this.timeline.label("end", this.duration + maxLifetime);
    }

    // Runs indefinitely until cancelled.
    else {
      this.timeline.call(() => this.#generator?.stop({hard: true}), Infinity);
      this.timeline.label("end", Infinity);
    }
  }

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

  /** @override */
  async stop() {
    // Finite generators stop as normal since their duration was already extended to cover the fade-out.
    if ( this.duration ) return super.stop();
    // Infinite generators intercept the stop call and perform a soft stop instead. The underlying ParticleGenerator
    // will automatically clean up after all particles have despawned.
    this.#generator?.stop({hard: false});
    this.timeline?.complete();
  }

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

  /** @override */
  async _stop() {
    this.#generator?.stop({hard: true});
  }
}
