import {AlphaField, AngleField, BooleanField, NumberField, StringField, SchemaField} from "@common/data/fields.mjs";
import VFXComponent from "../vfx-component.mjs";
import Sound from "@client/audio/sound.mjs";

/**
 * @import {VFXPositionalSoundData} from "../_types.mjs";
 */

/**
 * @typedef _VFXPositionalSoundData
 * @property {number} x             The x coordinate of the sound origin
 * @property {number} y             The y coordinate of the sound origin
 * @property {number} elevation     The elevation of the sound origin
 * @property {number} [angle]       The angle of the sound cone in degrees
 * @property {number} [rotation]    The direction of sound emission in degrees
 * @property {boolean} gmAlways     Whether the GM always hears the sound regardless of position (default true)
 * @property {{type: string, intensity: number}|null} [baseEffect]    Audio effect applied when the sound is not muffled
 * @property {{type: string, intensity: number}|null} [muffledEffect] Audio effect applied when the sound is muffled
 * @property {number} fade          Fade-in duration in milliseconds (default 0)
 * @property {number} [duration]    Component duration in milliseconds; defaults to the sound's natural duration
 * @property {string} channel       Audio channel for playback (default "environment")
 */

/**
 * @typedef {VFXPositionalSoundData & _VFXPositionalSoundData} VFXPositionalSoundComponentData
 */

/**
 * A component for playing positional sound effects that are synchronized with the VFX timeline.
 * This component provides serializable and persisted data storage around the `Sound#playAtPosition` API.
 *
 * @example A thunderclap at a specific location
 * ```js
 * const vfxConfig = {
 *   name: "thunderclap",
 *   components: {
 *     thunder: {
 *       type: "positionalSound",
 *       src: "assets/sounds/thunder.ogg",
 *       x: 3200,
 *       y: 2400,
 *       radius: 120,
 *       volume: 0.8,
 *       channel: "environment"
 *     }
 *   },
 *   timeline: [{component: "thunder"}]
 * };
 * const effect = new foundry.canvas.vfx.VFXEffect(vfxConfig);
 * effect.play();
 * ```
 * @extends {VFXComponent<VFXPositionalSoundComponentData>}
 */
export default class VFXPositionalSoundComponent extends VFXComponent {

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

  /** @inheritDoc */
  static defineSchema() {
    return {
      ...super.defineSchema(),
      channel: new StringField({required: true, blank: false, initial: "environment"}),
      duration: new NumberField({nullable: false}),
      easing: new BooleanField({initial: true}),
      elevation: new NumberField({required: true, nullable: false, initial: 0}),
      fade: new NumberField({required: true, nullable: false, initial: 0}),
      gmAlways: new BooleanField({required: false, initial: true}),
      angle: new AngleField({required: false}),
      baseEffect: new SchemaField({
        intensity: new NumberField({required: true, nullable: false, min: 1, max: 10}),
        type: new StringField({required: true, blank: false})
      }, {required: false, nullable: true, initial: null}),
      muffledEffect: new SchemaField({
        intensity: new NumberField({required: true, nullable: false, min: 1, max: 10}),
        type: new StringField({required: true, blank: false})
      }, {required: false, nullable: true, initial: null}),
      radius: new NumberField({required: true, nullable: false, initial: 60, positive: true}),
      rotation: new AngleField({required: false}),
      src: new StringField({required: true, blank: false}),
      volume: new AlphaField(),
      walls: new BooleanField({initial: true}),
      x: new NumberField({required: true, nullable: false}),
      y: new NumberField({required: true, nullable: false})
    };
  }

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

  /**
   * The Sound object used by this component.
   * @type {Sound|null}
   */
  #sound = null;

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

  /** @inheritDoc */
  async _load() {
    const context = game.audio[this.channel];
    this.#sound = new Sound(this.src, {context});
    await this.#sound.load();
  }

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

  /** @override */
  async _draw() {
    if ( this.#sound ) this.timeline.call(() => this.#playSound(), 0);
    const duration = this.duration ?? ((this.#sound?.duration ?? 0) * 1000);
    this.timeline.label("end", duration);
  }

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

  /** @override */
  async _stop() {
    await this.#sound?.stop();
  }

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

  /**
   * Play the positional sound for this component.
   * Failures to play are tolerated with a warning so as not to break the overall animation.
   * @returns {Promise<void>}
   */
  async #playSound() {
    const {elevation, easing, fade, gmAlways, radius, volume, walls, x, y} = this;
    const sourceData = {};
    if ( Number.isNumeric(this.angle) ) sourceData.angle = this.angle;
    if ( Number.isNumeric(this.rotation) ) sourceData.rotation = this.rotation;
    try {
      await this.#sound.playAtPosition({x, y, elevation}, radius, {
        baseEffect: this.baseEffect ?? undefined,
        easing,
        gmAlways,
        muffledEffect: this.muffledEffect ?? undefined,
        playbackOptions: fade ? {fade} : undefined,
        sourceData: Object.keys(sourceData).length ? sourceData : undefined,
        volume,
        walls
      });
    } catch(err) {
      console.warn(err);
    }
  }
}
