'use strict'

import { AudioClipOptions } from './interfaces/AudioClipOptions';

import { Node } from './interfaces/Node';
import { Marker } from './interfaces/Marker';

import { AudioClipState } from './enums/AudioClipState'

/**
 * An audio clip represents a piece of audio, which is either an audio html element or an audio boffer, as
 * a playable clip with extra properties.
 */
export class AudioClip {
    /**
     * The name of the audio clip.
     * 
     * @private
     * 
     * @property {string}
     */
    private _name: string;

    /**
     * A reference to the audio to play.
     * 
     * @private
     * 
     * @property 
     */
    private _audio: any;

    /**
     * The audio buffer source of the clip.
     * 
     * @private
     * 
     * @property {AudioBufferSourceNode}
     */
    private _source!: AudioBufferSourceNode;

    /**
     * A reference to the options for this audio clip.
     * 
     * @private
     * 
     * @property {AudioClipOptions}
     */
    private _options: AudioClipOptions;

    /**
     * A reference to the gain node for this clip.
     * 
     * @private
     * 
     * @property {GainNode}
     */
    private _gain: GainNode;

    /**
     * The current state of this clip.
     * 
     * @private
     * 
     * @property {AudioClipState}
     */
    private _state: AudioClipState = AudioClipState.STOPPED;

    /**
     * The number of times this clip has been played.
     * 
     * @private
     * 
     * @property 
     */
    private _timesPlayed: number = 0;

    /**
     * The time that this clip start being played at.
     * 
     * @private
     * 
     * @property {number}
     */
    private _timeStartedAt: number = 0;

    /**
     * When the clip is paused, this will keep track of when it was paused so it can be resumed at that time.
     * 
     * @private
     * 
     * @property {number}
     */
    private _timePausedAt: number = 0;

    /**
     * The current time of the clip.
     * 
     * @private
     * 
     * @property {number}
     */
    private _currentTime: number = 0;

    /**
     * The duration of the clip.
     * 
     * @private
     * 
     * @property {number}
     */
    private _duration: number;

    /**
     * The volume of this audio clip.
     * 
     * @private
     * 
     * @property {number}
     * 
     * @default 100
     */
    private _volume: number = 100;

    /**
     * Keeps track of the previous volume of the clip.
     * 
     * @private
     * 
     * @property {number}
     */
    private _previousVolume: number = 1;

    /**
     * A reference to the nodes that have been added for this clip.
     * 
     * @private
     * 
     * @property {Array<Node>}
     */
    private _nodesref: Array<Node> = [];

    /**
     * A reference to the nodes that have been added in a way that allows them to be retrieved easily.
     * 
     * @private
     * 
     * @property {*}
     */
    private _nodes: any = {};

    /**
     * Indicates whether this audio clip is played on a loop or not.
     * 
     * @property {boolean}
     * 
     * @default false
     */
    loop: boolean = false;

    /**
     * @param {string} name The name of the audio clip.
     * @param {AudioBuffer} audio The AudioBuffer that contains the audio of the clip.
     * @param {AudioClipOptions} [options] The options passed to this audio clip.
     */
    constructor(name: string, audio: AudioBuffer, options: AudioClipOptions) {
        this._name = name;
        this._audio = audio;
        this._duration = audio.duration;
        this._options = options;

        this._gain = this._options.ctx.createGain();
        this._gain.connect(this._options.ctx.destination);

        if (this._options.trigger) this._setupTrigger();
        if (!this._options.markers) this._options.markers = [];
    }

    /**
     * Gets the name of the audio clip.
     * 
     * @returns {string}
     */
    get name(): string { return this._name; }

    /**
     * Gets the current state of the audio clip.
     * 
     * @returns {string}
     */
    get state(): string { return this._state; }

    /**
     * Gets the number of times that this clip has been played.
     * 
     * @returns {number}
     */
    get timesPlayed(): number { return this._timesPlayed; }

    /**
     * Gets the current time of the clip.
     * 
     * @returns {number}
     */
    get currentTime(): number {
        if (this._state === AudioClipState.PAUSED) return this._timePausedAt;
        if (this._state === AudioClipState.PLAYING) return this._options.ctx.currentTime - this._timeStartedAt;

        return 0;
    }

    /**
     * Gets the duration of the clip.
     * 
     * @returns {number}
     */
    get duration(): number { return this._duration; }

    /**
     * Gets the volume of this clip.
     * 
     * @returns {number}
     */
    get volume(): number { return this._volume; }

    /**
     * Sets the volume of this clip.
     * 
     * @param {number} vol The new volume of the clip.
     */
    set volume(vol: number) {
        this._volume = vol;
        this._gain.gain.setValueAtTime(this._volume / 100, this._options.ctx.currentTime);
    }

    /**
     * Gets the created nodes.
     * 
     * @returns {*}
     */
    get nodes(): any { return this._nodes; }

    /**
     * Adds a custom node from `app.nodes[nodeName]`.
     * 
     * @param {Node} node The node to add to this clip.
     * 
     * @example
     * 
     * const track = a2d.addAudio('track-1', track1Buffer);
     * 
     * const bf = a2d.nodes.biquadFilter();
     * track.addNode(bf);
     */
    addNode(node: Node) {
        this._nodes[node.name] = node.instance;
        this._nodesref.push(node);

        if (this._nodesref.length === 1) return;

        const latestNode: Node = this._nodesref[this._nodesref.length - 2];
        node.instance.connect(latestNode.instance);
    }

    /**
     * Plays this audio clip.
     * 
     * @param {string} marker The name of the marker of the part of the clip to play.
     * 
     * @example
     * 
     * const sfxMarkers = [{ name: 'walk', start: 1500, duration: 1000 }, { name: 'fall': start: 2500, duration: 1500 }];
     * const sfx = a2d.addAudio('sfx', sfxBuffer, { markers: sxfMarkers });
     * 
     * // play just the falling sound.
     * sfx.play('fall');
     */
    play(marker?: string) {
        const offset: number = this._timePausedAt;

        this._source = this._options.ctx.createBufferSource();
        this._source.buffer = this._audio;
        this._source.loop = this.loop;

        this._connectNodes();
        this._oncomplete();

        if (marker) {
            const clipMarker: (Marker | undefined) = this._options.markers?.find((m: Marker) => m.name === marker);
            if (!clipMarker) return;

            this._source.start(0, clipMarker.start / 1000, clipMarker.duration ? clipMarker.duration / 1000 : undefined);
            this._resetA2DMarkers(clipMarker);
        } else {
            this._source.start();
        }

        this._timeStartedAt = this._options.ctx.currentTime - offset;
        this._timePausedAt = 0;

        this._state = AudioClipState.PLAYING;
        this._timesPlayed++;
    }

    /**
     * Pause the currently playing audio.
     * 
     * @example
     * 
     * const sfx = a2d.addAudio('sfx', sfxBuffer);
     * sfx.play();
     * 
     * setTimeout(() => {
     *   sfx.pause();
     * }, 1000);
     */
    pause() {
        const elapsed: number = this._options.ctx.currentTime - this._timeStartedAt;

        this.stop();

        this._timePausedAt = elapsed
        this._options.markers?.push({ name: 'a2d-pause', start: this._timePausedAt * 1000 });

        this._state = AudioClipState.PAUSED;
    }

    /**
     * Resumes playing this clip from when it was paused.
     * 
     * @example
     * 
     * const sfx = a2d.addAudio('sfx', sfxBuffer);
     * sfx.play();
     * sfx.pause();
     * 
     * setTimeout(() => {
     *   sfx.resume();
     * }, 1000);
     */
    resume() {
        this.play('a2d-pause');
    }

    /**
     * Stops the playback of this audio.
     * 
     * @returns {AudioClip} Returns this for chaining.
     * 
     * @example
     * 
     * const sfx = a2d.addAudio('sfx', sfxBuffer);
     * 
     * sfx.play();
     * sfx.stop();
     */
    stop() {
        this._source.disconnect();
        this._source = this._options.ctx.createBufferSource();

        this._timePausedAt = 0;
        this._timeStartedAt = 0;

        this._state = AudioClipState.STOPPED;
    }

    /**
     * Seeks to a specific time in the clip.
     * 
     * @param {number} time The time, in milliseconds, to seek to.
     */
    seek(time: number) {
        if (!time) return;

        if (time > this.duration * 1000) {
            console.warn('The time to seek to is greater than the duration of the clip.');
            return;
        }

        if (this._state === AudioClipState.PLAYING) this.stop();

        this._options.markers?.push({ name: 'a2d-seek', start: time });
        this.play('a2d-seek');
    }

    /**
     * Mutes this clip.
     * 
     * @example
     * 
     * const sfx = a2d.addAudio('sfx', sfxBuffer);
     * 
     * sfx.play();
     * sfx.mute();
     */
    mute() {
        this._previousVolume = this.volume;
        this.volume = 0;
    }

    /**
     * Puts the volume back to the value it was at before the clip was muted.
     * 
     * @example
     * 
     * const sfx = a2d.addAudio('sfx', sfxBuffer);
     * sfx.play();
     * 
     * sfx.mute();
     * sfx.unmute();
     */
    unmute() {
        this.volume = this._previousVolume;
    }

    /**
     * Sets up an onclick event on a trigger element if one was provided in the options.
     * 
     * @private
     */
    private _setupTrigger() {
        const el: (HTMLElement | null) = document.querySelector(this._options.trigger!);
        if (!el) return;

        el.addEventListener('click', () => this.play());
    }

    /**
     * Connects the nodes that have been added through `addNode`.
     * 
     * @private
     */
    private _connectNodes() {
        if (this._nodesref.length > 0) {
            const firstNode: Node = this._nodesref[0];
            const latestNode: Node = this._nodesref[this._nodesref.length - 1]

            this._source.connect(latestNode.instance);

            firstNode.instance.connect(this._gain);
        } else {
            this._source.connect(this._gain);
        }
    }

    /**
     * Specify what happens when a clip is finished playing.
     * 
     * @private
     */
    private _oncomplete() {
        this._source.onended = () => {
            this._state = AudioClipState.STOPPED;
            this._source.onended = null;
        };
    }

    /**
     * Resets any markers set internally.
     * 
     * @private
     * 
     * @param {Marker} clipMarker The marker to check if should be removed.
     */
    private _resetA2DMarkers(clipMarker: Marker) {
        if (clipMarker.name.includes('a2d')) {
            this._options.markers = this._options.markers?.filter((marker: Marker) => {
                !marker.name.includes('a2d');
            });
        }
    }
}