import { ConvolutionFile } from '@/interface'
import { sleep } from '@/utils'
import { remove } from 'lodash-es'
import axios from 'axios'

export interface Effect {
  createConvolver: (payload: ConvolutionFile) => Promise<void>
  clearConvolver: () => void
  startSpatial: () => void
  stopSpatial: () => void
  clearSpatial: () => void
  startInOut: (fade: boolean) => Promise<void>
  clearFade: () => void
}

enum NodeID {
  SourceNode,
  ConvolverNode,
  PannerNode,
  GainNode,
  BiquadFilterNode
}

type EffectNode = {
  node: AudioNode
  id: NodeID
}

class EffectNodeRender {
  private node: EffectNode[]
  private ctx: AudioContext

  constructor(ctx: AudioContext) {
    this.ctx = ctx
    this.node = []
  }

  static render(node: AudioNode, id: NodeID) {
    return {
      node,
      id
    }
  }

  insert(node: EffectNode) {
    if (!this.node.find(n => n.id === node.id)) {
      this.node.push(node)
    }
    return this
  }

  delete(id: NodeID) {
    remove(this.node, node => node.id === id)
    return this
  }

  output() {
    const { node, ctx } = this

    for (let i = 0; i < this.node.length; i++) {
      const cur = node[i],
        next = node[i + 1]

      cur.node.disconnect()
      if (next) {
        cur.node.connect(next.node)
      } else {
        cur.node.connect(ctx.destination)
      }
    }
  }
}

export class AudioEffect implements Effect {
  private context: AudioContext
  private source: MediaElementAudioSourceNode
  private audio: HTMLAudioElement
  private nodeRender: EffectNodeRender
  private gainNodeFade?: GainNode
  private convolver?: ConvolverNode
  private panner?: PannerNode
  private bigquadFilter?: BiquadFilterNode
  private convolverFile?: ConvolutionFile

  public stopSurround: boolean

  constructor(audio: HTMLAudioElement) {
    const AudioContext = window.AudioContext || window.webkitAudioContext
    if (!AudioContext) {
      new Error('AudioContext does not support')
    }
    this.audio = audio
    this.stopSurround = true
    this.context = new AudioContext()
    this.source = this.context.createMediaElementSource(this.audio)
    this.nodeRender = new EffectNodeRender(this.context)
    this.nodeRender.insert(
      EffectNodeRender.render(this.source, NodeID.SourceNode)
    )
  }

  public async getBuffer(url: string | Buffer) {
    let responseBuffer
    if (typeof url === 'string') {
      responseBuffer = await axios
        .get<ArrayBuffer>(url, {
          responseType: 'arraybuffer',
          withCredentials: false
        })
        .then(res => res.data)
    } else {
      responseBuffer = url
    }
    const decodeBuffer = await this.context.decodeAudioData(responseBuffer)

    return decodeBuffer
  }

  public async createConvolver(payload: ConvolutionFile) {
    if (this.convolverFile === payload || payload === '原唱') return
    this.convolver = this.context.createConvolver()
    this.convolverFile = payload
    const decodeBuffer = await this.getBuffer(
      'audio-effect/' + payload + '.wav'
    )
    this.convolver.buffer = decodeBuffer
    this.nodeRender
      .delete(NodeID.ConvolverNode)
      .insert(EffectNodeRender.render(this.convolver, NodeID.ConvolverNode))
      .output()
  }

  public clearConvolver() {
    this.convolverFile = '原唱'
    this.nodeRender.delete(NodeID.ConvolverNode).output()
  }

  public startTender() {
    const { currentTime } = this.context
    if (!this.bigquadFilter) {
      this.bigquadFilter = this.context.createBiquadFilter()
      this.bigquadFilter.type = 'bandpass'
      this.bigquadFilter.frequency.setValueAtTime(1000, currentTime)
    }

    this.nodeRender
      .insert(
        EffectNodeRender.render(this.bigquadFilter, NodeID.BiquadFilterNode)
      )
      .output()
  }

  public clearTender() {
    this.nodeRender.delete(NodeID.BiquadFilterNode).output()
  }

  public startSpatial() {
    this.stopSurround = false
    if (!this.panner) {
      this.panner = this.context.createPanner()
      this.panner.panningModel = 'HRTF'
      this.panner.distanceModel = 'linear'
    }
    const radius = 10

    this.nodeRender
      .insert(EffectNodeRender.render(this.panner, NodeID.PannerNode))
      .output()

    let index = 0
    const start = async () => {
      if (this.stopSurround) {
        return
      }
      await sleep(16)
      const x = Math.sin(index) * radius
      const y = Math.cos(index) * radius
      this.panner?.positionX.setValueAtTime(x, this.context.currentTime)
      this.panner?.positionZ.setValueAtTime(y, this.context.currentTime)
      index += 1 / 100
      requestAnimationFrame(start)
    }
    start()
  }

  public stopSpatial() {
    this.stopSurround = true
  }

  public clearSpatial() {
    this.nodeRender.delete(NodeID.PannerNode).output()
  }

  public async startInOut(isIn: boolean) {
    const { currentTime } = this.context
    if (!this.gainNodeFade) {
      this.gainNodeFade = this.context.createGain()
    }
    this.nodeRender
      .insert(EffectNodeRender.render(this.gainNodeFade, NodeID.GainNode))
      .output()
    this.gainNodeFade.gain.cancelScheduledValues(0)
    if (isIn) {
      this.gainNodeFade.gain.linearRampToValueAtTime(1.0, currentTime + 1)
    } else {
      this.gainNodeFade.gain.linearRampToValueAtTime(0, currentTime + 1)
    }
    await sleep(1100)
  }

  public clearFade() {
    this.nodeRender.delete(NodeID.GainNode).output()
  }

  public clearBasicEffect() {
    this.stopSpatial()
    this.clearSpatial()
    this.clearFade()
    this.clearTender()
  }
}