import {
  DEFAULT_AUDIO_FORMAT,
  type AudioFormat,
  type Playback,
  type StreamingPlayback,
} from "./utils.js"
import { listDevices, findDeviceByLabel, streamTone } from "./utils.js"

export class Player {
  #deviceId?: string
  #format: Required<AudioFormat>

  static async create({ label, format }: { label?: string; format?: AudioFormat } = {}) {
    const devices = await listDevices()
    const playbackDevices = devices.filter((d) => d.type === "playback")

    if (playbackDevices.length === 0) {
      throw new Error("No playback devices found")
    }

    let deviceId: string | undefined
    if (label) {
      const device = await findDeviceByLabel(label, "playback")
      deviceId = device.id
    }

    return new Player({ deviceId, format })
  }

  constructor({ deviceId, format }: { deviceId?: string; format?: AudioFormat }) {
    this.#deviceId = deviceId
    this.#format = {
      format: format?.format ?? DEFAULT_AUDIO_FORMAT.format,
      sampleRate: format?.sampleRate ?? DEFAULT_AUDIO_FORMAT.sampleRate,
      channels: format?.channels ?? DEFAULT_AUDIO_FORMAT.channels,
    }
  }

  #commonArgs(): string[] {
    const args = []
    if (this.#deviceId) {
      args.push("-D", this.#deviceId)
    }
    return args
  }

  async play(filePath: string, { repeat }: { repeat?: boolean } = {}): Promise<Playback> {
    const args = [...this.#commonArgs(), filePath]

    let proc = Bun.spawn(["aplay", ...args], { stdout: "pipe", stderr: "pipe" })

    const handle: Playback = {
      isPlaying: true,
      stop: async () => {
        if (!handle.isPlaying) return
        handle.isPlaying = false
        proc.kill()
        await proc.exited
      },
      finished: async () => {
        await proc.exited
      },
    }

    const loop = async () => {
      while (handle.isPlaying) {
        await proc.exited
        if (!handle.isPlaying) break

        if (repeat) {
          proc = Bun.spawn(["aplay", ...args], { stdout: "pipe", stderr: "pipe" })
        } else {
          handle.isPlaying = false
          break
        }
      }
    }

    loop()

    return handle
  }

  async playTone(frequencies: number[], duration: number): Promise<Playback> {
    if (duration !== Infinity && duration <= 0) {
      throw new Error("Duration must be greater than 0 or Infinity")
    }

    const args = [
      ...this.#commonArgs(),
      "-f",
      this.#format.format,
      "-r",
      this.#format.sampleRate.toString(),
      "-c",
      this.#format.channels.toString(),
      "-t",
      "raw",
      "-",
    ]

    const proc = Bun.spawn(["aplay", ...args], {
      stdin: "pipe",
      stdout: "pipe",
      stderr: "pipe",
    })

    // Start streaming tone in background
    streamTone(proc.stdin, frequencies, duration, this.#format)

    const handle: Playback = {
      isPlaying: true,
      stop: async () => {
        if (!handle.isPlaying) return
        handle.isPlaying = false
        try {
          proc.stdin.end()
        } catch (e) {}
        proc.kill()
        await proc.exited
      },
      finished: async () => {
        await proc.exited
      },
    }

    proc.exited.then(() => {
      handle.isPlaying = false
    })

    return handle
  }

  playStream(): StreamingPlayback {
    const args = [
      ...this.#commonArgs(),
      "-f",
      this.#format.format,
      "-r",
      this.#format.sampleRate.toString(),
      "-c",
      this.#format.channels.toString(),
      "-t",
      "raw",
      "-",
    ]

    const proc = Bun.spawn(["aplay", ...args], { stdin: "pipe", stdout: "pipe", stderr: "pipe" })

    let bufferFinishTime = performance.now()
    const format = this.#format
    const bytesPerSecond = format.sampleRate * format.channels * 2

    const handle: StreamingPlayback = {
      isPlaying: true,
      write: (chunk: Uint8Array) => {
        if (handle.isPlaying) {
          proc.stdin.write(chunk)
          const chunkDurationMs = (chunk.byteLength / bytesPerSecond) * 1000
          bufferFinishTime = Math.max(performance.now(), bufferFinishTime) + chunkDurationMs
        }
      },
      get bufferEmptyFor() {
        const now = performance.now()
        if (now > bufferFinishTime) {
          return now - bufferFinishTime
        }
        return 0
      },
      stop: async () => {
        if (!handle.isPlaying) return
        handle.isPlaying = false
        try {
          proc.stdin.end()
        } catch (e) {}
        proc.kill()
        await proc.exited
      },
    }

    proc.exited.then(() => {
      handle.isPlaying = false
    })

    return handle
  }
}
