import type { InputEvent, InputOptions } from "./types"

type WorkerMessage =
  | { type: "ready"; initialValue: 0 | 1 }
  | { type: "event"; value: 0 | 1; timestamp: bigint }
  | { type: "error"; message: string }

export class Input {
  #worker: Worker
  #callbacks = new Set<(event: InputEvent) => void>()
  #closed = false
  #lastValue: 0 | 1 = 0

  constructor(chipPath: string, offset: number, options: InputOptions = {}) {
    const pull = options.pull ?? "up"
    const debounce = options.debounce ?? 0
    const edge = options.edge ?? "both"

    this.#worker = new Worker(new URL("./input-worker.ts", import.meta.url).href)

    this.#worker.onmessage = (msg: MessageEvent<WorkerMessage>) => {
      if (this.#closed) return

      const data = msg.data

      if (data.type === "ready") {
        this.#lastValue = data.initialValue
      } else if (data.type === "event") {
        this.#lastValue = data.value
        for (const callback of this.#callbacks) {
          callback({ value: data.value, timestamp: data.timestamp })
        }
      } else if (data.type === "error") {
        console.error(`GPIO Input Error (pin ${offset}):`, data.message)
      }
    }

    this.#worker.postMessage({ chipPath, offset, pull, debounce, edge })
  }

  get value(): 0 | 1 {
    if (this.#closed) throw new Error("Input is closed")
    return this.#lastValue
  }

  onChange(callback: (event: InputEvent) => void): () => void {
    if (this.#closed) throw new Error("Input is closed")
    this.#callbacks.add(callback)
    return () => this.#callbacks.delete(callback)
  }

  async waitForValue(targetValue: 0 | 1, timeout?: number): Promise<void> {
    if (this.#closed) throw new Error("Input is closed")

    if (this.#lastValue === targetValue) return

    return new Promise((resolve, reject) => {
      let timeoutId: ReturnType<typeof setTimeout> | undefined

      const cleanup = () => {
        if (timeoutId) clearTimeout(timeoutId)
        unsubscribe()
      }

      const unsubscribe = this.onChange((event) => {
        if (event.value === targetValue) {
          cleanup()
          resolve()
        }
      })

      if (!timeout) return

      timeoutId = setTimeout(() => {
        cleanup()
        reject(new Error(`Timeout waiting for value ${targetValue}`))
      }, timeout)
    })
  }

  close() {
    if (this.#closed) return
    this.#closed = true

    this.#callbacks.clear()
    this.#worker.onmessage = null
    this.#worker.terminate()
  }

  [Symbol.dispose]() {
    this.close()
  }
}
