import { Gain } from "../core/context/Gain";
import { ToneAudioNode, } from "../core/context/ToneAudioNode";
import { noOp } from "../core/util/Interface";
import { assert } from "../core/util/Debug";
/**
 * Base class for fire-and-forget nodes
 */
export class OneShotSource extends ToneAudioNode {
    constructor(options) {
        super(options);
        /**
         * The callback to invoke after the
         * source is done playing.
         */
        this.onended = noOp;
        /**
         * The start time
         */
        this._startTime = -1;
        /**
         * The stop time
         */
        this._stopTime = -1;
        /**
         * The id of the timeout
         */
        this._timeout = -1;
        /**
         * The public output node
         */
        this.output = new Gain({
            context: this.context,
            gain: 0,
        });
        /**
         * The output gain node.
         */
        this._gainNode = this.output;
        /**
         * Get the playback state at the given time
         */
        this.getStateAtTime = function (time) {
            const computedTime = this.toSeconds(time);
            if (this._startTime !== -1 &&
                computedTime >= this._startTime &&
                (this._stopTime === -1 || computedTime <= this._stopTime)) {
                return "started";
            }
            else {
                return "stopped";
            }
        };
        this._fadeIn = options.fadeIn;
        this._fadeOut = options.fadeOut;
        this._curve = options.curve;
        this.onended = options.onended;
    }
    static getDefaults() {
        return Object.assign(ToneAudioNode.getDefaults(), {
            curve: "linear",
            fadeIn: 0,
            fadeOut: 0,
            onended: noOp,
        });
    }
    /**
     * Start the source at the given time
     * @param  time When to start the source
     */
    _startGain(time, gain = 1) {
        assert(this._startTime === -1, "Source cannot be started more than once");
        // apply a fade in envelope
        const fadeInTime = this.toSeconds(this._fadeIn);
        // record the start time
        this._startTime = time + fadeInTime;
        this._startTime = Math.max(this._startTime, this.context.currentTime);
        // schedule the envelope
        if (fadeInTime > 0) {
            this._gainNode.gain.setValueAtTime(0, time);
            if (this._curve === "linear") {
                this._gainNode.gain.linearRampToValueAtTime(gain, time + fadeInTime);
            }
            else {
                this._gainNode.gain.exponentialApproachValueAtTime(gain, time, fadeInTime);
            }
        }
        else {
            this._gainNode.gain.setValueAtTime(gain, time);
        }
        return this;
    }
    /**
     * Stop the source node at the given time.
     * @param time When to stop the source
     */
    stop(time) {
        this.log("stop", time);
        this._stopGain(this.toSeconds(time));
        return this;
    }
    /**
     * Stop the source at the given time
     * @param  time When to stop the source
     */
    _stopGain(time) {
        assert(this._startTime !== -1, "'start' must be called before 'stop'");
        // cancel the previous stop
        this.cancelStop();
        // the fadeOut time
        const fadeOutTime = this.toSeconds(this._fadeOut);
        // schedule the stop callback
        this._stopTime = this.toSeconds(time) + fadeOutTime;
        this._stopTime = Math.max(this._stopTime, this.now());
        if (fadeOutTime > 0) {
            // start the fade out curve at the given time
            if (this._curve === "linear") {
                this._gainNode.gain.linearRampTo(0, fadeOutTime, time);
            }
            else {
                this._gainNode.gain.targetRampTo(0, fadeOutTime, time);
            }
        }
        else {
            // stop any ongoing ramps, and set the value to 0
            this._gainNode.gain.cancelAndHoldAtTime(time);
            this._gainNode.gain.setValueAtTime(0, time);
        }
        this.context.clearTimeout(this._timeout);
        this._timeout = this.context.setTimeout(() => {
            // allow additional time for the exponential curve to fully decay
            const additionalTail = this._curve === "exponential" ? fadeOutTime * 2 : 0;
            this._stopSource(this.now() + additionalTail);
            this._onended();
        }, this._stopTime - this.context.currentTime);
        return this;
    }
    /**
     * Invoke the onended callback
     */
    _onended() {
        if (this.onended !== noOp) {
            this.onended(this);
            // overwrite onended to make sure it only is called once
            this.onended = noOp;
            // dispose when it's ended to free up for garbage collection only in the online context
            if (!this.context.isOffline) {
                const disposeCallback = () => this.dispose();
                // @ts-ignore
                if (typeof window.requestIdleCallback !== "undefined") {
                    // @ts-ignore
                    window.requestIdleCallback(disposeCallback);
                }
                else {
                    setTimeout(disposeCallback, 1000);
                }
            }
        }
    }
    /**
     * Get the playback state at the current time
     */
    get state() {
        return this.getStateAtTime(this.now());
    }
    /**
     * Cancel a scheduled stop event
     */
    cancelStop() {
        this.log("cancelStop");
        assert(this._startTime !== -1, "Source is not started");
        // cancel the stop envelope
        this._gainNode.gain.cancelScheduledValues(this._startTime + this.sampleTime);
        this.context.clearTimeout(this._timeout);
        this._stopTime = -1;
        return this;
    }
    dispose() {
        super.dispose();
        this._gainNode.dispose();
        this.onended = noOp;
        return this;
    }
}
//# sourceMappingURL=OneShotSource.js.map