import { AudioContext } from "three";

import { Application } from "./engine_application.js";

/** 
 * @internal 
 * Ensure the audio context is resumed if it gets suspended or interrupted */
export function ensureAudioContextIsResumed() {
    Application.registerWaitForInteraction(() => {
        // this is a fix for https://github.com/mrdoob/three.js/issues/27779 & https://linear.app/needle/issue/NE-4257
        const ctx = AudioContext.getContext();
        ctx.addEventListener("statechange", () => {
            setTimeout(() => {
                // on iOS the audiocontext can be interrupted: https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/state#resuming_interrupted_play_states_in_ios_safari
                const state = ctx.state as AudioContextState | "interrupted";
                if (state === "suspended" || state === "interrupted") {
                    ctx.resume()
                        .then(() => { console.log("AudioContext resumed successfully"); })
                        .catch((e) => { console.log("Failed to resume AudioContext: " + e); });
                }
            }, 500);
        });
    });
}


/**
 * Represents an audio clip that can be loaded and played independently.
 * The AudioClip class encapsulates the URL of the audio resource and provides
 * methods for playback control (play, pause, stop) and querying duration.
 */
export class AudioClip {

    /**
     * Creates a new AudioClip instance with the specified URL.
     * @param url The URL of the audio resource to load. This can be a path to an audio file or a MediaStream URL.
     */
    constructor(public readonly url: string) {
    }

    /** Whether the clip is currently playing.
     * @returns `true` if the clip is actively playing audio.
     */
    get isPlaying(): boolean {
        return this._audioElement !== undefined
            && !this._audioElement.paused
            && !this._audioElement.ended;
    }

    /**
     * The total duration of the audio clip in seconds.
     * Loads the audio metadata if not already available.
     * @returns A promise that resolves with the duration in seconds.
     */
    getDuration(): Promise<number> {
        if (this._duration !== undefined) {
            return Promise.resolve(this._duration);
        }
        return this.ensureAudioElement().then(audio => {
            this._duration = audio.duration;
            return audio.duration;
        });
    }

    /**
     * Plays the audio clip from the current position.
     * @returns A promise that resolves when playback finishes, or rejects on error.
     * If the clip is looping, the promise will never resolve on its own – call {@link stop} or {@link pause} to end playback.
     */
    // #region Play
    play(): Promise<void> {
        return this.ensureAudioElement().then(audio => {
            return new Promise<void>((resolve, reject) => {
                const onEnded = () => {
                    cleanup();
                    resolve();
                };
                const onError = () => {
                    cleanup();
                    reject(new Error(`Playback error for ${this.url}`));
                };
                const onPause = () => {
                    // pause/stop also resolve the promise
                    cleanup();
                    resolve();
                };
                const cleanup = () => {
                    audio.removeEventListener("ended", onEnded);
                    audio.removeEventListener("error", onError);
                    audio.removeEventListener("pause", onPause);
                };
                audio.addEventListener("ended", onEnded);
                audio.addEventListener("error", onError);
                audio.addEventListener("pause", onPause);
                audio.play().catch(err => {
                    cleanup();
                    reject(err);
                });
            });
        });
    }

    /**
     * Pauses playback at the current position.
     * Call {@link play} to resume.
     */
    // #region Pause/Stop
    pause(): void {
        this._audioElement?.pause();
    }

    /**
     * Stops playback and resets the position to the beginning.
     */
    stop(): void {
        if (this._audioElement) {
            this._audioElement.pause();
            this._audioElement.currentTime = 0;
        }
    }

    /** Whether the clip should loop when reaching the end. */
    get loop(): boolean { return this._loop; }
    set loop(value: boolean) {
        this._loop = value;
        if (this._audioElement) this._audioElement.loop = value;
    }

    /** Playback volume from 0 (silent) to 1 (full). */
    get volume(): number { return this._volume; }
    set volume(value: number) {
        this._volume = value;
        if (this._audioElement) this._audioElement.volume = value;
    }

    /** Current playback position in seconds. */
    get currentTime(): number { return this._audioElement?.currentTime ?? 0; }
    set currentTime(value: number) {
        if (this._audioElement) this._audioElement.currentTime = value;
    }

    /** Normalized playback progress from 0 to 1.
     * @returns The current playback position as a value between 0 and 1, or 0 if the duration is unknown.
     */
    get progress(): number {
        if (!this._audioElement || !this._duration) return 0;
        return this._audioElement.currentTime / this._duration;
    }

    /**
     * Seeks to a normalized position (0–1) in the clip.
     * @param position A value between 0 (start) and 1 (end).
     */
    // #region Seek
    seek(position: number): void {
        if (this._audioElement && this._duration) {
            this._audioElement.currentTime = Math.max(0, Math.min(1, position)) * this._duration;
        }
    }

    /** The underlying HTMLAudioElement, or `undefined` if not yet created.
     * Use this to connect the element to the Web Audio API via `createMediaElementSource()`.
     * @returns The HTMLAudioElement if the clip has been loaded or played, otherwise `undefined`.
     */
    get audioElement(): HTMLAudioElement | undefined { return this._audioElement; }

    private _audioElement?: HTMLAudioElement;
    private _duration?: number;
    private _loadPromise?: Promise<HTMLAudioElement>;
    private _loop: boolean = false;
    private _volume: number = 1;

    /** Lazily creates and loads the shared HTMLAudioElement. */
    private ensureAudioElement(): Promise<HTMLAudioElement> {
        if (this._audioElement && this._loadPromise) {
            return this._loadPromise;
        }
        const audio = this._audioElement ?? new Audio(this.url);
        this._audioElement = audio;
        audio.loop = this._loop;
        audio.volume = this._volume;

        if (audio.readyState >= HTMLMediaElement.HAVE_METADATA) {
            this._duration = audio.duration;
            this._loadPromise = Promise.resolve(audio);
            return this._loadPromise;
        }

        this._loadPromise = new Promise<HTMLAudioElement>((resolve, reject) => {
            const onLoaded = () => {
                cleanup();
                this._duration = audio.duration;
                resolve(audio);
            };
            const onError = (e: Event) => {
                cleanup();
                reject(new Error(`Failed to load audio clip from ${this.url}: ${e}`));
            };
            const cleanup = () => {
                audio.removeEventListener("loadedmetadata", onLoaded);
                audio.removeEventListener("error", onError);
            };
            audio.addEventListener("loadedmetadata", onLoaded);
            audio.addEventListener("error", onError);
        });
        return this._loadPromise;
    }
}