/**
 * @typedef {Object} AppendDeltaMessage
 * @property {"appendDelta"} type
 * @property {ResponseAudioDelta} delta
 */

/**
 * @typedef {Object} DeleteItemMessage
 * @property {"deleteItem"} type
 * @property {string} item_id
 */

/**
 * @typedef {Object} ClearMessage
 * @property {"clear"} type
 */

/**
 * @typedef {Object} SetSourceRateMessage
 * @property {"setSourceRate"} type
 * @property {number} hz
 */

/**
 * @typedef {Object} NowPlayingMessage
 * @property {"nowPlaying"} type
 * @property {string|null} item_id
 * @property {number} played_ms
 */

/**
 * @typedef {Object} MuteMessage
 * @property {"mute"} type
 */

/**
 * @typedef {Object} UnmuteMessage
 * @property {"unmute"} type
 */

/**
 * @typedef {AppendDeltaMessage | DeleteItemMessage | ClearMessage | SetSourceRateMessage | MuteMessage | UnmuteMessage} PlaybackMessage
 */

/**
 * @typedef {Object} Chunk
 * @property {string} item_id
 * @property {Int16Array} data
 * @property {number} off
 */

class AudioPlaybackWorklet extends AudioWorkletProcessor {
    constructor() {
        super()
        /** @type {number} */ this.srcRate = 24000
        /** @type {number} */ this.dstRate = sampleRate
        /** @type {number} */ this.step = this.srcRate / this.dstRate
        /** @type {Chunk[]} */ this.queue = []
        /** @type {Chunk|null} */ this.cur = null
        /** @type {number} */ this.hold = 0
        /** @type {number} */ this.phase = 0
        /** @type {number|undefined} */ this._x0 = undefined
        /** @type {number|undefined} */ this._x1 = undefined
        /** @type {string|null} */ this._x0ItemId = null
        /** @type {string|null} */ this._x1ItemId = null
        /** @type {string|null} */ this._nextItemId = null
        /** @type {string|null} */ this.nowItemId = null
        /** @type {number} */ this.nowItemSamples = 0
        /** @type {number} */ this._notifyFrames = 0
        /** @type {boolean} */ this.muted = false
        this.port.onmessage = (e) => this._onMessage(/** @type {PlaybackMessage} */ (e.data))
    }

    /** @param {PlaybackMessage} msg */
    _onMessage(msg) {
        if (!msg || !msg.type) return
        if (msg.type === "appendDelta" && msg.delta && msg.delta.pcmInt16 instanceof Int16Array) {
            this.queue.push({ item_id: msg.delta.item_id, data: msg.delta.pcmInt16, off: 0 })
            return
        }
        if (msg.type === "deleteItem") {
            const id = msg.item_id
            this.queue = this.queue.filter((ch) => ch.item_id !== id)
            if (this.cur && this.cur.item_id === id) {
                this.cur = null
                this.hold = 0
            }
            if (this.nowItemId === id) {
                this.nowItemId = null
                this.nowItemSamples = 0
                this._postNowPlaying()
            }
            return
        }
        if (msg.type === "clear") {
            this.queue.length = 0
            this.cur = null
            this.hold = 0
            this.phase = 0
            this._x0 = undefined
            this._x1 = undefined
            this._x0ItemId = null
            this._x1ItemId = null
            this._nextItemId = null
            this.nowItemId = null
            this.nowItemSamples = 0
            this._notifyFrames = 0
            this._postNowPlaying()
            return
        }
        if (msg.type === "setSourceRate" && Number.isFinite(msg.hz) && msg.hz > 0) {
            this.srcRate = msg.hz | 0
            this.step = this.srcRate / this.dstRate
            return
        }
        if (msg.type === "mute") {
            this.muted = true
            return
        }
        if (msg.type === "unmute") {
            this.muted = false
            return
        }
    }

    /** @returns {boolean} */
    _ensureCurrent() {
        if (this.cur == null) {
            if (this.queue.length === 0) return false
            this.cur = this.queue.shift() || null
            if (this.cur == null) return false
        }
        return true
    }

    /** @returns {number} */
    _nextInt16() {
        for (;;) {
            if (!this._ensureCurrent()) {
                this._nextItemId = null
                this.hold = 0
                return 0
            }
            const d = this.cur.data
            const o = this.cur.off | 0
            if (o < d.length) {
                const s = d[o]
                this.cur.off = o + 1
                this.hold = s
                this._nextItemId = this.cur.item_id
                return s
            }
            this.cur = null
        }
    }

    _postNowPlaying() {
        /** @type {NowPlayingMessage} */
        const m = {
            type: "nowPlaying",
            item_id: this.nowItemId,
            played_ms: Math.max(0, Math.floor((this.nowItemSamples * 1000) / this.srcRate)),
        }
        this.port.postMessage(m)
    }

    /**
     * @param {Float32Array[][]} _inputs
     * @param {Float32Array[][]} outputs
     * @param {Record<string, Float32Array>} _parameters
     * @returns {boolean}
     */
    process(_inputs, outputs, _parameters) {
        const out = outputs[0]
        if (!out || out.length === 0) return true
        const ch0 = out[0]
        const N = ch0.length

        if (this._x1 === undefined) {
            this._x1 = this._nextInt16()
            this._x1ItemId = this._nextItemId
            this._x0 = this._x1
            this._x0ItemId = this._x1ItemId
            this.phase = 0
            this.nowItemId = this._x0ItemId
            this.nowItemSamples = 0
            this._postNowPlaying()
        }

        const common = () => {
            this.phase += this.step
            while (this.phase >= 1) {
                this.phase -= 1
                this._x0 = this._x1
                this._x0ItemId = this._x1ItemId
                this._x1 = this._nextInt16()
                this._x1ItemId = this._nextItemId

                if (this.nowItemId !== this._x0ItemId) {
                    this.nowItemId = this._x0ItemId
                    this.nowItemSamples = 0
                    this._postNowPlaying()
                }
                if (this.nowItemId) this.nowItemSamples += 1
            }
        }

        if (this.muted) {
            for (let i = 0; i < N; i++) {
                ch0[i] = 0
                for (let c = 1; c < out.length; c++) out[c][i] = 0
                common()
            }
        } else {
            for (let i = 0; i < N; i++) {
                const yI16 =
                    /** @type {number} */ (this._x0) +
                    this.phase * /** @type {number} */ (this._x1 - /** @type {number} */ (this._x0))
                const yF32 = Math.max(-1, Math.min(1, yI16 / 32768))
                ch0[i] = yF32
                for (let c = 1; c < out.length; c++) out[c][i] = yF32
                common()
            }
        }

        this._notifyFrames += N
        if (this._notifyFrames >= this.dstRate / 20) {
            this._postNowPlaying()
            this._notifyFrames = 0
        }
        return true
    }
}

registerProcessor("audio-playback-worklet", AudioPlaybackWorklet)
