interface CrunkerConstructorOptions {
   sampleRate: number
   concurrentNetworkRequests: number
}

type CrunkerInputTypes = string | File | Blob

export default class Crunker {
   private readonly _sampleRate: number
   private readonly _concurrentNetworkRequests: number
   private readonly _context: AudioContext

   constructor({ sampleRate, concurrentNetworkRequests = 200 }: Partial<CrunkerConstructorOptions> = {}) {
      this._context = this._createContext(sampleRate)
      sampleRate ||= this._context.sampleRate
      this._sampleRate = sampleRate
      this._concurrentNetworkRequests = concurrentNetworkRequests
   }
   private _createContext(sampleRate = 44_100): AudioContext {
      window.AudioContext = window.AudioContext || (window as any).webkitAudioContext || (window as any).mozAudioContext
      return new AudioContext({ sampleRate })
   }
   /**
    *转换url等类型为buffer
    */
   async fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<AudioBuffer[]> {
      const buffers: AudioBuffer[] = []
      const groups = Math.ceil(filepaths.length / this._concurrentNetworkRequests)
      for (let i = 0; i < groups; i++) {
         const group = filepaths.slice(i * this._concurrentNetworkRequests, (i + 1) * this._concurrentNetworkRequests)
         buffers.push(...(await this._fetchAudio(...group)))
      }
      return buffers
   }
   private async _fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<AudioBuffer[]> {
      return await Promise.all(
         filepaths.map(async filepath => {
            let buffer: ArrayBuffer
            if (filepath instanceof File || filepath instanceof Blob) {
               buffer = await filepath.arrayBuffer()
            } else {
               buffer = await fetch(filepath).then(response => {
                  if (response.headers.has("Content-Type") && !response.headers.get("Content-Type")!.includes("audio/")) {
                     console.warn(
                        `Crunker: Attempted to fetch an audio file, but its MIME type is \`${
                           response.headers.get("Content-Type")!.split(";")[0]
                        }\`. We'll try and continue anyway. (file: "${filepath}")`
                     )
                  }
                  return response.arrayBuffer()
               })
            }
            /* 这里有个坑 safa浏览器老一点的版本不支持decodeAudioData返回promise 所以用这种老式写法 */
            return await new Promise((res, rej) => {
               this._context.decodeAudioData(
                  buffer,
                  buffer => {
                     res(buffer)
                  },
                  err => {
                     rej(err)
                  }
               )
            })
         })
      )
   }
   /**
    * 根据时间合并音频
    */
   mergeAudioBuffers(buffers: AudioBuffer[], times: number[]): AudioBuffer {
      if (buffers.length !== times.length) {
         throw new Error("buffer数量和times数量必须一致")
      }
      const output = this._context.createBuffer(this._maxNumberOfChannels(buffers), this._sampleRate * this._maxDuration(buffers), this._sampleRate)
      buffers.forEach((buffer, index) => {
         for (let channelNumber = 0; channelNumber < buffer.numberOfChannels; channelNumber++) {
            const outputData = output.getChannelData(channelNumber)
            const bufferData = buffer.getChannelData(channelNumber)
            const offsetNum = Math.round(times[index] * this._sampleRate) //时间偏差
            for (let i = buffer.getChannelData(channelNumber).length - 1; i >= 0; i--) {
               outputData[i + offsetNum] += bufferData[i]
               // 当合并大于1或者小于-1的时候可能会爆音  所以这里取最大值和最小值
               if (outputData[i + offsetNum] > 1) {
                  outputData[i + offsetNum] = 1
               }
               if (outputData[i + offsetNum] < -1) {
                  outputData[i + offsetNum] = -1
               }
            }
            output.getChannelData(channelNumber).set(outputData)
         }
      })

      return output
   }
   /**
    * 根据buffer导出audio标签
    */
   exportAudioElement(buffer: AudioBuffer, type = "audio/mp3"): HTMLAudioElement {
      const recorded = this._interleave(buffer)
      const dataview = this._writeHeaders(recorded, buffer.numberOfChannels, buffer.sampleRate)
      const audioBlob = new Blob([dataview], { type })
      return this._renderAudioElement(audioBlob)
   }
   /**
    * 计算音频前面的空白
    */
   calculateSilenceDuration(buffer: AudioBuffer) {
      const threshold = 0.01 // 静音阈值，低于此值的部分认为是静音
      const sampleRate = buffer.sampleRate
      const channelData = buffer.getChannelData(0) // 只处理单声道数据
      let silenceDuration = 0
      for (let i = 0; i < channelData.length; i++) {
         if (Math.abs(channelData[i]) > threshold) {
            break
         }
         silenceDuration++
      }
      // 将样本数转换为秒
      silenceDuration = silenceDuration / sampleRate
      return silenceDuration
   }
   private _maxNumberOfChannels(buffers: AudioBuffer[]): number {
      return Math.max(...buffers.map(buffer => buffer.numberOfChannels))
   }
   private _maxDuration(buffers: AudioBuffer[]): number {
      return Math.max(...buffers.map(buffer => buffer.duration))
   }
   private _interleave(input: AudioBuffer): Float32Array {
      if (input.numberOfChannels === 1) {
         return input.getChannelData(0)
      }
      const channels = []
      for (let i = 0; i < input.numberOfChannels; i++) {
         channels.push(input.getChannelData(i))
      }
      const length = channels.reduce((prev, channelData) => prev + channelData.length, 0)
      const result = new Float32Array(length)
      let index = 0
      let inputIndex = 0
      while (index < length) {
         channels.forEach(channelData => {
            result[index++] = channelData[inputIndex]
         })
         inputIndex++
      }
      return result
   }
   private _renderAudioElement(blob: Blob): HTMLAudioElement {
      const audio = document.createElement("audio")
      audio.src = this._renderURL(blob)
      audio.load()
      return audio
   }
   private _renderURL(blob: Blob): string {
      return (window.URL || window.webkitURL).createObjectURL(blob)
   }
   private _writeHeaders(buffer: Float32Array, numOfChannels: number, sampleRate: number): DataView {
      const bitDepth = 16
      const bytesPerSample = bitDepth / 8
      const sampleSize = numOfChannels * bytesPerSample
      const fileHeaderSize = 8
      const chunkHeaderSize = 36
      const chunkDataSize = buffer.length * bytesPerSample
      const chunkTotalSize = chunkHeaderSize + chunkDataSize
      const arrayBuffer = new ArrayBuffer(fileHeaderSize + chunkTotalSize)
      const view = new DataView(arrayBuffer)
      this._writeString(view, 0, "RIFF")
      view.setUint32(4, chunkTotalSize, true)
      this._writeString(view, 8, "WAVE")
      this._writeString(view, 12, "fmt ")
      view.setUint32(16, 16, true)
      view.setUint16(20, 1, true)
      view.setUint16(22, numOfChannels, true)
      view.setUint32(24, sampleRate, true)
      view.setUint32(28, sampleRate * sampleSize, true)
      view.setUint16(32, sampleSize, true)
      view.setUint16(34, bitDepth, true)
      this._writeString(view, 36, "data")
      view.setUint32(40, chunkDataSize, true)
      return this._floatTo16BitPCM(view, buffer, fileHeaderSize + chunkHeaderSize)
   }
   private _floatTo16BitPCM(dataview: DataView, buffer: Float32Array, offset: number): DataView {
      for (let i = 0; i < buffer.length; i++, offset += 2) {
         const tmp = Math.max(-1, Math.min(1, buffer[i]))
         dataview.setInt16(offset, tmp < 0 ? tmp * 0x8000 : tmp * 0x7fff, true)
      }
      return dataview
   }
   private _writeString(dataview: DataView, offset: number, header: string): void {
      for (let i = 0; i < header.length; i++) {
         dataview.setUint8(offset + i, header.charCodeAt(i))
      }
   }
}
