const SAMPLE_RATE = 24_000
const BUFFER_SIZE = 4_800

const AUDIO_PLAYBACK_WORKLET_URL = "/assets/audio-playback-worklet.js"
const AUDIO_PROCESSOR_WORKLET_URL = "/assets/audio-processor-worklet.js"

const uint8ToBase64 = (u8: Uint8Array): string => {
    let bin = ""
    for (let i = 0; i < u8.length; i++) bin += String.fromCharCode(u8[i])
    return btoa(bin)
}

const base64ToUint8 = (b64: string): Uint8Array => {
    const bin = atob(b64)
    const out = new Uint8Array(bin.length)
    for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
    return out
}

interface NowPlayingMessage {
    type: "nowPlaying"
    item_id: string
    played_ms: number
}

interface NowPlayingState {
    item_id: string | null
    played_ms: number
}

interface Player {
    init: (sampleRate?: number) => Promise<void>
    play: (delta) => void
    deleteItem: (item_id: string) => void
    stop: () => void
    setSourceRate: (hz: number) => void
    getNowPlaying: () => NowPlayingState
    destroy: () => Promise<void>
    mute: () => void
    unmute: () => void
    node?: AudioWorkletNode | null
}

const createPlayer = (defaultSampleRate = 48000): Player => {
    let ctx: AudioContext | null = null
    let node: AudioWorkletNode | null = null
    let nowItemId: string | null = null
    let playedMs = 0

    const isNowPlayingMessage = (m: unknown): m is NowPlayingMessage => {
        if (!m || typeof m !== "object") return false
        const x = m as Record<string, unknown>
        return x["type"] === "nowPlaying" && "played_ms" in x
    }

    const init = async (sampleRate = defaultSampleRate): Promise<void> => {
        ctx = new AudioContext({ sampleRate })
        await ctx.audioWorklet.addModule(AUDIO_PLAYBACK_WORKLET_URL)
        node = new AudioWorkletNode(ctx, "audio-playback-worklet")
        node.port.onmessage = (e: MessageEvent) => {
            const m = e.data
            if (isNowPlayingMessage(m)) {
                nowItemId = m.item_id
                playedMs = m.played_ms | 0
            }
        }
        node.connect(ctx.destination)
    }

    const play = (delta: ResponseAudioDelta): void => {
        if (!node) return
        const buf = delta.pcmInt16.buffer
        node.port.postMessage({ type: "appendDelta", delta }, [buf])
    }
    const deleteItem = (item_id: string): void => {
        node?.port.postMessage({ type: "deleteItem", item_id })
    }

    const stop = (): void => {
        node?.port.postMessage({ type: "clear" })
    }

    const setSourceRate = (hz: number): void => {
        node?.port.postMessage({ type: "setSourceRate", hz })
    }

    const getNowPlaying = (): NowPlayingState => {
        return { item_id: nowItemId, played_ms: playedMs }
    }
    const mute = (): void => {
        node?.port.postMessage({ type: "mute" })
    }
    const unmute = (): void => {
        node?.port.postMessage({ type: "unmute" })
    }

    const destroy = async (): Promise<void> => {
        if (!ctx) return
        try {
            await ctx.close()
        } finally {
            ctx = null
            node = null
            nowItemId = null
            playedMs = 0
        }
    }

    return { init, play, deleteItem, stop, setSourceRate, getNowPlaying, destroy, mute, unmute }
}

const createRecorder = (onChunk: (pcm: Int16Array) => void) => {
    let ctx: AudioContext | null = null
    let stream: MediaStream | null = null
    let source: MediaStreamAudioSourceNode | null = null
    let worklet: AudioWorkletNode | null = null

    const start = async () => {
        stream = await navigator.mediaDevices.getUserMedia({ audio: true })
        if (ctx) await ctx.close()
        ctx = new (window.AudioContext || (window as any).webkitAudioContext)({ sampleRate: SAMPLE_RATE })
        await ctx.audioWorklet.addModule(AUDIO_PROCESSOR_WORKLET_URL)

        source = ctx.createMediaStreamSource(stream)
        worklet = new AudioWorkletNode(ctx, "audio-processor-worklet")
        worklet.port.onmessage = (ev: MessageEvent<Int16Array>) => onChunk(ev.data)

        source.connect(worklet)
        worklet.connect(ctx.destination)
    }
    const stop = async () => {
        if (stream) {
            stream.getTracks().forEach((t) => t.stop())
            stream = null
        }
        if (ctx) {
            try {
                await ctx.close()
            } finally {
                ctx = null
            }
        }
        source = null
        worklet = null
    }
    return { start, stop }
}
export { uint8ToBase64, base64ToUint8, createPlayer, createRecorder,SAMPLE_RATE }
