import { assert } from "../../../../core/assert.js";
import { combine_hash } from "../../../../core/collection/array/combine_hash.js";
import Signal from "../../../../core/events/signal/Signal.js";
import { computeHashFloat } from "../../../../core/primitives/numbers/computeHashFloat.js";
import { number_compare_ascending } from "../../../../core/primitives/numbers/number_compare_ascending.js";
import { compareStrings } from "../../../../core/primitives/strings/compareStrings.js";
import { computeStringHash } from "../../../../core/primitives/strings/computeStringHash.js";
import { SoundTrackFlags } from "./SoundTrackFlags.js";
import { SoundTrackNodes } from "./SoundTrackNodes.js";

const DEFAULT_FLAGS = SoundTrackFlags.StartWhenReady;

export class SoundTrack {

    /**
     *
     * @type {AudioBuffer|null}
     */
    #buffer = null;

    /**
     *
     * @type {String|null}
     */
    url = null;

    /**
     *
     * @type {number}
     */
    time = 0;

    /**
     * @deprecated Not used
     * @type {String|null}
     */
    channel = "";

    /**
     * @private
     * @type {number}
     */
    __volume = 1;

    /**
     * In seconds
     * Obtained once clip is loaded
     * @type {number}
     */
    duration = -1;


    /**
     *
     * @type {number|SoundTrackFlags}
     */
    flags = DEFAULT_FLAGS;

    on = {
        /**
         * @type {Signal<this>}
         */
        ended: new Signal()
    };

    /**
     * @private
     * @type {SoundTrackNodes}
     */
    #nodes = null;

    /**
     *
     * @return {SoundTrackNodes}
     */
    get nodes() {
        return this.#nodes;
    }

    /**
     *
     * @param {AudioContext} context
     */
    initializeNodes(context) {
        this.#nodes = new SoundTrackNodes(context);
    }

    /**
     *
     * @param {AudioBuffer} v
     */
    set buffer(v) {
        this.#buffer = v;

        this.duration = v.duration;

        this.setFlag(SoundTrackFlags.Ready);
    }

    get buffer() {
        return this.#buffer;
    }

    /**
     *
     * @returns {BaseAudioContext}
     */
    get #context() {
        return this.nodes.volume.context;
    }

    start(startTime = 0) {
        //TODO: figure out a way to use AudioBuffer.playbackRate.value to control speed of playback
        const nodes = this.nodes;

        if (nodes.source !== null) {

            /*
             source is already used up, need to create a new one
             NOTE: AudioBufferSourceNode can only be used once, if we want to start playback again - we have to create a new one
             see https://stackoverflow.com/questions/59815825/how-to-skip-ahead-n-seconds-while-playing-track-using-web-audio-api
             */
            this.suspend();

        }

        nodes.source = this.#context.createBufferSource();

        //connect source to output
        nodes.source.connect(nodes.volume);

        const looping = this.getFlag(SoundTrackFlags.Loop);

        nodes.source.loop = looping;

        // Make the sound source use the buffer and start playing it.
        nodes.source.buffer = this.#buffer;

        nodes.source.onended = () => {
            if (!looping) {
                this.clearFlag(SoundTrackFlags.Playing);
                this.on.ended.send1(this);
            }
        }

        let when = startTime;

        if (this.time < 0) {
            // playback will start in the future
            when = -this.time;

            nodes.source.start(when, 0);
        } else {
            // playback will start from within the track's duration (already playing)

            nodes.source.start(when, this.time);
        }

        this.setFlag(SoundTrackFlags.Playing);
        this.clearFlag(SoundTrackFlags.Suspended);
    }

    suspend() {
        const nodes = this.#nodes;

        if (nodes.source !== null) {

            nodes.source.stop();
            nodes.source.disconnect();

            nodes.source = null;

        }

        this.setFlag(SoundTrackFlags.Suspended);
    }

    /**
     *
     * @param {SoundTrack} other
     * @returns number
     */
    compare(other) {
        const url = compareStrings(this.url, other.url);

        if (url !== 0) {
            return url;
        }

        const time = number_compare_ascending(this.time, other.time);

        if (time !== 0) {
            return time;
        }

        const channel = compareStrings(this.channel, other.channel);

        if (channel !== 0) {
            return channel;
        }

        const volume = number_compare_ascending(this.__volume, other.__volume);

        if (volume !== 0) {
            return volume;
        }

        const flags = number_compare_ascending(this.flags, other.flags);

        if (flags !== 0) {
            return flags;
        }

        return 0;
    }

    /**
     *
     * @param {number|SoundEmitterFlags} flag
     * @returns {void}
     */
    setFlag(flag) {
        this.flags |= flag;
    }

    /**
     *
     * @param {number|SoundEmitterFlags} flag
     * @returns {void}
     */
    clearFlag(flag) {
        this.flags &= ~flag;
    }

    /**
     *
     * @param {number|SoundEmitterFlags} flag
     * @param {boolean} value
     */
    writeFlag(flag, value) {
        if (value) {
            this.setFlag(flag);
        } else {
            this.clearFlag(flag);
        }
    }

    /**
     *
     * @param {number|SoundEmitterFlags} flag
     * @returns {boolean}
     */
    getFlag(flag) {
        return (this.flags & flag) === flag;
    }

    /**
     * Linearly transition volume to a target value over a certain duration.
     * Useful for fading sounds in and out of the mix.
     *
     * NOTE: volume property of the object is updated instantly, transition happens at the AudioNode level only
     *
     * @param {number} target target volume value
     * @param {number} duration How long the transition should take, in seconds
     * @param {number} [startAfter] when fading should start, see WebAudio docs on {@link AudioContext#currentTime}
     */
    setVolumeOverTime(target, duration, startAfter = 0) {
        // instantly update volume for consistency purposes wrt serialization
        this.__volume = target;

        const nodes = this.nodes;
        if (nodes !== null) {

            /**
             *
             * @type {GainNode}
             */
            const volume_node = nodes.volume;

            /**
             *
             * @type {AudioParam}
             */
            const gain = volume_node.gain;

            const current_value = gain.value;

            let start_time = startAfter;

            /**
             * @type {AudioContext}
             */
            const audioContext = volume_node.context;

            if (audioContext !== undefined) {
                start_time += audioContext.currentTime;
            }

            // cancel any scheduled values
            gain.cancelScheduledValues(0);

            gain.setValueCurveAtTime([current_value, target], start_time, duration);
        }
    }

    /**
     *
     * @param {number} v
     */
    set volume(v) {
        this.__volume = v;

        if (this.nodes !== null) {
            this.nodes.volume.gain.setValueAtTime(v, 0);
        }
    }

    /**
     *
     * @return {number}
     */
    get volume() {
        return this.__volume;
    }

    /**
     * @deprecated
     * @return {boolean}
     */
    get loop() {
        return this.getFlag(SoundTrackFlags.Loop);
    }

    /**
     * @deprecated
     * @param {boolean} v
     */
    set loop(v) {
        this.writeFlag(SoundTrackFlags.Loop, v);
    }

    /**
     * @deprecated
     * @return {boolean}
     */
    get playing() {
        return this.getFlag(SoundTrackFlags.Playing);
    }

    /**
     * @deprecated
     * @param {boolean} v
     */
    set playing(v) {
        this.writeFlag(SoundTrackFlags.Playing, v);
    }

    /**
     * @deprecated
     * @return {boolean}
     */
    get startWhenReady() {
        return this.getFlag(SoundTrackFlags.StartWhenReady);
    }

    /**
     * @deprecated
     * @param {boolean} v
     */
    set startWhenReady(v) {
        this.writeFlag(SoundTrackFlags.StartWhenReady, v);
    }

    /**
     *
     * @param {SoundTrack} other
     */
    copy(other) {
        this.url = other.url;
        this.time = other.time;
        this.volume = other.volume;
        this.flags = other.flags;
    }

    /**
     *
     * @param {SoundTrack} other
     * @returns {boolean}
     */
    equals(other) {
        return this.url === other.url
            && this.time === other.time
            && this.__volume === other.__volume
            && this.flags === other.flags
            ;
    }

    /**
     *
     * @return {number}
     */
    hash() {
        return combine_hash(
            computeStringHash(this.url),
            computeHashFloat(this.time),
            computeHashFloat(this.__volume),
            this.flags
        );
    }

    /**
     *
     * @return {SoundTrack}
     */
    clone() {
        const r = new SoundTrack();

        r.copy(this);

        return r;
    }

    toJSON() {
        return {
            url: this.url,
            loop: this.getFlag(SoundTrackFlags.Loop),
            time: this.time,
            volume: this.volume,
            playing: this.getFlag(SoundTrackFlags.Playing),
            startWhenReady: this.getFlag(SoundTrackFlags.StartWhenReady),
            usingAssetAlias: this.getFlag(SoundTrackFlags.UsingAliasURL),
            synchronized: this.getFlag(SoundTrackFlags.Synchronized)
        };
    }

    fromJSON(
        {
            url,
            loop = false,
            time = 0,
            volume = 1,
            playing = false,
            startWhenReady = true,
            usingAssetAlias = false,
            synchronized = false
        }
    ) {
        assert.isString(url, 'url');

        this.url = url;

        this.writeFlag(SoundTrackFlags.Loop, loop);

        this.writeFlag(SoundTrackFlags.UsingAliasURL, usingAssetAlias);

        this.time = time;

        this.volume = volume;

        this.writeFlag(SoundTrackFlags.Playing, playing);

        this.writeFlag(SoundTrackFlags.StartWhenReady, startWhenReady);

        this.writeFlag(SoundTrackFlags.Synchronized, synchronized);
    }

    static fromJSON(json) {
        const track = new SoundTrack();

        track.fromJSON(json);

        return track;
    }

}
