import type { SerialTaskExecuteContext } from '../handler'
import type { PrivateCustomEventName } from '../types'

import { SerialHandler } from '../handler'
import { createEventBus } from '../utils'

const $bus = createEventBus<PrivateCustomEventName>()

class AudioActuator extends SerialHandler<AudioBuffer, never> {
  private static FADE_DURATION = 0.018

  private audioContext: AudioContext = new AudioContext()

  private bufferSource: AudioBufferSourceNode | null = null

  private gainNode: GainNode | null = null

  private volume = 1

  public execute(
    context: SerialTaskExecuteContext<AudioBuffer, never>,
  ): void {
    if (context.isLastExecute) {
      this.taskCompletedCallback()
      return
    }

    this.bufferSource = this.audioContext.createBufferSource()
    this.bufferSource.buffer = context.taskItem.original!

    this.gainNode = this.audioContext.createGain()
    this.bufferSource.connect(this.gainNode)
    this.gainNode.connect(this.audioContext.destination)

    this.gainNode.gain.setValueAtTime(this.volume, this.audioContext.currentTime)
    this.gainNode.gain.linearRampToValueAtTime(1, this.audioContext.currentTime + AudioActuator.FADE_DURATION)

    this.gainNode.gain.setValueAtTime(1, this.audioContext.currentTime + context.taskItem.original!.duration - AudioActuator.FADE_DURATION)
    this.gainNode.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + context.taskItem.original!.duration)

    this.bufferSource.start()
    this.bufferSource.addEventListener('ended', () => {
      this.taskCompletedCallback()
    })
  }

  protected onFinish(): void {
    if (this.bufferSource) {
      this.bufferSource.stop()
      this.bufferSource = null
    }
    if (this.gainNode) {
      this.gainNode.disconnect()
      this.gainNode = null
    }
    if (this.audioContext) {
      this.audioContext.suspend()
      this.audioContext = new AudioContext()
    }
    $bus.emit('_audioActuatorFinish')
  }

  protected onBeforeFirstExecute(): void {
    $bus.emit('_audioActuatorBeforeFirstExecute')
  }

  public setVolume(volume: number): void {
    if (this.gainNode) {
      this.gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime)
    }
    this.volume = volume
  }
}

export default AudioActuator
