class AudioPlayer {
  static STATUS = {
    uninited: "uninited",
    initing: "initing", //正在初始化中
    inited: "inited",  //已初始化（timelin已设置，layout完成）
    resuming: "resuming", //恢复播放中
    resumed: "resumed", //播放中
    pausing: "pausing", //进入暂停中
    paused: "paused", //已暂停
    stopping: "stopping", //停止中
    stopped: "stopped", //已停止,
    error: "error", //有错误
  };

  constructor(audioContext, wasmFS) {
    this.isWx = typeof wx != "undefined";
    this.wasmFS = wasmFS;
    this.onStatusChanged = null;
    this.onTick = null;
    this.timeline = null;
    this.status = AudioPlayer.STATUS.uninited;
    this.anchorTime = 0; //timeline在audioContext时钟上的起点
    this.audioContext = audioContext;
    this.scriptNode = null;
  }

  createAudioContext() {
    if (this.isWx) return wx.createWebAudioContext();
    else return new (window.AudioContext || window.webkitAudioContext)();
  }

  loadAudioBuffer(src) {
    if (this.isWx) {
      if (src.substr(0, 4) == "http") {
        return this.loadAudioBufferFromNet(src);
      } else if (src.substr(0, 5) == "/tmp/") {
        return this.loadAudioBufferFromWasmFs(src);
      } else {
        return this.loadAudioBufferFromWxFs(src);
      }
    } else {
      if (src.substr(0, 5) == "/tmp/") {
        return this.loadAudioBufferFromWasmFs(src);
      } else {
        return this.loadAudioBufferFromNet(src);
      }
    }
  }

  loadAudioBufferFromNet(src) {
    let that = this;
    return new Promise((resolve, reject) => {
      if (this.isWx) {
        wx.downloadFile({
          url: src,
          success(res) {
            if (res.statusCode === 200) {
              that
                .loadAudioBufferFromWxFs(res.tempFilePath)
                .then((buffer) => {
                  resolve(buffer);
                })
                .catch((err) => {
                  reject(err);
                });
            }
          },
        });
      } else {
        let audioctx = this.audioContext;
        fetch(src)
          .then((response) => {
            response
              .arrayBuffer()
              .then((arrabuffer) => {
                audioctx.decodeAudioData(
                  arrabuffer,
                  (buffer) => {
                    console.log("decodebuffer=", buffer);
                    resolve(buffer);
                  },
                  (err) => {
                    reject(err);
                  }
                );
              })
              .catch((e) => {
                reject(e);
              });
          })
          .catch((e) => {
            reject(e);
          });
      }
    });
  }

  loadAudioBufferFromWxFs(src) {
    let audioctx = this.audioContext;
    return new Promise((resolve, reject) => {
      const fs = wx.getFileSystemManager();
      fs.readFile({
        filePath: src,
        success(res) {
          audioctx.decodeAudioData(
            res.data,
            (buffer) => {
              resolve(buffer);
            },
            (err) => {
              reject(err);
            }
          );
        },
        fail(res) {
          reject(res);
        },
      });
    });
  }

  loadAudioBufferFromWasmFs(src) {
    let audioctx = this.audioContext;
    return new Promise((resolve, reject) => {
      let fileData = this.wasmFS.readFile(src);
      audioctx.decodeAudioData(
        fileData.buffer,
        (buffer) => {
          resolve(buffer);
        },
        (err) => {
          reject(err);
        }
      );
    });
  }

  getCurrentTime() {
    return this.audioContext.currentTime - this.anchorTime;
  }

  setStatus(s) {
    if (this.status == s) return;
    let preStatus = this.status;
    this.status = s;
    if (this.onStatusChanged) {
      this.onStatusChanged(preStatus, this.status);
    }
  }

  init(inputTimeline, suc, fail) {
    console.log("[audio_player] init");
    if (this.scriptNode) {
      this.scriptNode.disconnect();
      this.scriptNode = null;
    }
    this.unlayoutClips();
    this.timeline = null;
    this.setStatus(AudioPlayer.STATUS.uninited);

    if (!this.audioContext) {
      fail && fail("audioContext is null");
    }
    if (this.scriptNode) {
      fail && fail("script node is not empty");
    }

    console.log("[audio_player] when init, state=", this.audioContext.state)
    let that = this;
    if (this.audioContext.state != "suspended") {
      this.audioContext.suspend().then(()=>{
        console.log("[audio_player] suspend in init suc");
        that.initScriptNode();
        that.initTimeline(inputTimeline, suc, fail);
      }).catch((e)=>{
        console.log("[audio_player] suspend in init fail");
        fail && fail(e);
      });
    } else {
      that.initScriptNode();
      that.initTimeline(inputTimeline, suc, fail);
    }
  }

  initScriptNode() {
    const bufferSize = 1024;
    this.scriptNode = this.audioContext.createScriptProcessor(bufferSize, 1, 1);
    this.scriptNode.onaudioprocess = (event) => {
      if (this.status == AudioPlayer.STATUS.uninited || this.status == AudioPlayer.STATUS.stopped ) {
        return;
      }
      let timelineTime = this.getCurrentTime();
      if (this.status == AudioPlayer.STATUS.resumed) {
        this.onTick && this.onTick(timelineTime);
      }

      if (timelineTime >= this.timeline.duration) {
        this.stop(() => {
          this.unlayoutClips();
        }, () => {
          this.unlayoutClips();
        });
      }
    };
    this.scriptNode.connect(this.audioContext.destination);
  }

  initTimeline(inputTimeline, suc, fail) {
    this.timeline = { duration: inputTimeline.duration, clips: [] };
    let promises = inputTimeline.clips.map((clip) => {
      return this.loadAudioBuffer(clip.src);
    });
    Promise.all(promises)
      .then((buffers) => {
        buffers.forEach((buf, i) => {
          let clip = Object.assign({}, inputTimeline.clips[i]);
          clip.audioBuffer = buf;
          this.timeline.clips.push(clip);
        });

        this.setStatus(AudioPlayer.STATUS.inited);
        console.log("[audio_player] initTimeline suc");
        suc && suc();
      })
      .catch((err) => {
        console.log("[audio_player] initTimeline fail,err=", err);
        fail && fail(err);
      });
  }

  uninit() {
    console.log("[audio_player] uninit");
    if (this.scriptNode) {
      this.scriptNode.disconnect();
      this.scriptNode = null;
    }
    this.unlayoutClips();
    this.timeline = null;
    this.audioContext = null;
    this.setStatus(AudioPlayer.STATUS.uninited);
  }

  layoutClips(seekTime) {
    let audioContextCurrentTime = this.audioContext.currentTime;
    this.anchorTime = audioContextCurrentTime - seekTime;
    this.timeline.clips.forEach((clip) => {
      clip.bufferSourceNode = this.audioContext.createBufferSource();
      clip.bufferSourceNode.loop = false;
      clip.bufferSourceNode.buffer = clip.audioBuffer;
      clip.bufferSourceNode.playbackRate.value = clip.speed;
      clip.gainNode = this.audioContext.createGain();
      clip.gainNode.gain.value = clip.volume;
      clip.bufferSourceNode.connect(clip.gainNode);
      clip.gainNode.connect(this.audioContext.destination);
      clip.duration = clip.trimOut - clip.trimIn;
      if (clip.inPoint > seekTime) {
        //延迟播放
        clip.when = audioContextCurrentTime + clip.inPoint - seekTime;
        clip.offset = clip.trimIn;
      } else {
        //立即播放
        clip.when = 0;
        clip.offset = clip.trimIn + (seekTime - clip.inPoint);
      }
      console.log("111state=", this.audioContext.state);
      //真机上当audioContext为suspended状态时调用bufferSourceNode.start时会自动将audioContext状态切换为running, 在开发工具和web上不会，
      //所以当audiocontext没有running时不能立即调用start，等audiocontext调用resume时再start
      if (this.audioContext.state == "running") {
        clip.bufferSourceNode.start(clip.when, clip.offset, clip.duration);
        clip.started = true;
      } else {
        clip.started = false;
      }

      console.log("222state=", this.audioContext.state)
    });
  }

  unlayoutClips() {
    if (this.timeline) {
      this.timeline.clips.forEach((clip) => {
        if (clip.bufferSourceNode) {
          if (clip.started) {
            clip.bufferSourceNode.stop();
          }
          clip.bufferSourceNode.disconnect();
          clip.bufferSourceNode = null;
          clip.gainNode.disconnect();
          clip.gainNode = null;
        }
      });
      this.anchorTime = 0;
    }
  }

  resume(suc, fail) {
    console.log("[audio_player] resume, status=", this.status, ",state=", this.audioContext.state);
    if (this.status == AudioPlayer.STATUS.resumed) {
      suc && suc();
    } else if (this.status == AudioPlayer.STATUS.paused || this.status == AudioPlayer.STATUS.inited || this.status == AudioPlayer.STATUS.stopped) {
      if (this.status == AudioPlayer.STATUS.inited || this.status == AudioPlayer.STATUS.stopped) {
        this.seekTo(0);
      }
      this.setStatus(AudioPlayer.STATUS.resuming)
      if (this.audioContext.state != "suspended") {
        console.log("[audio_player] when resume, state="+this.audioContext.state)
        throw "[audio_player] when resume, state="+this.audioContext.state;
      }
      this.audioContext
      .resume()
      .then(() => {
        console.log("[audio_player] resume suc")
        this.timeline.clips.forEach((clip)=>{
          if (!clip.started) {
            clip.bufferSourceNode.start(clip.when, clip.offset, clip.duration);
            clip.started = true;
          }
        })
        this.setStatus(AudioPlayer.STATUS.resumed);
        suc && suc();
      })
      .catch(() => {
        console.log("[audio_player] resume fail")
        this.setStatus(AudioPlayer.STATUS.error);
        fail && fail();
      });
    } 
    else {
      console.log("[audio_player] err: status is ", this.status, "cannot resume");
      fail && fail();
    }
  }

  pause(suc, fail) {
    console.log("[audio_player] pause");
    if (this.status == AudioPlayer.STATUS.paused || this.status == AudioPlayer.STATUS.inited ||
        this.status == AudioPlayer.STATUS.uninited || this.status == AudioPlayer.STATUS.stopped ) {
      suc && suc();
    } else if (this.status == AudioPlayer.STATUS.resumed) {
      if (this.audioContext.state != "running") {
        console.log("[audio_player] when pause, state="+this.audioContext.state)
      }
      this.setStatus(AudioPlayer.STATUS.pausing)
      this.audioContext
      .suspend()
      .then(() => {
        console.log("[audio_player] pause suc");
        if (this.status == AudioPlayer.STATUS.pausing) {
          this.setStatus(AudioPlayer.STATUS.paused);
        }
        suc && suc();
      })
      .catch((err) => {
        console.log("[audio_player] pause fail,", err);
        this.setStatus(AudioPlayer.STATUS.error);
        fail && fail();
      });
    } 
    else {
      console.log("[audio_player] err: audio player status is ", this.status, "cannot pause");
      fail && fail();
    }
  }

  stop(suc, fail) {
    console.log("[audio_player] stop");
    if (this.status == AudioPlayer.STATUS.stopped) {
      suc && suc();
    } else if (this.status == AudioPlayer.STATUS.paused || this.status == AudioPlayer.STATUS.inited) {
      this.setStatus(AudioPlayer.STATUS.stopped)
      suc && suc();
    } else if (this.status == AudioPlayer.STATUS.resumed) {
      if (this.audioContext.state != "running") {
        console.log("[audio_player] when stop, state="+this.audioContext.state)
      }
      this.setStatus(AudioPlayer.STATUS.stopping)
      this.audioContext
      .suspend()
      .then(() => {
        console.log("[audio_player] stop suc");
        if (this.status == AudioPlayer.STATUS.stopping) {
          this.setStatus(AudioPlayer.STATUS.stopped);
        }
        suc && suc();
      })
      .catch((err) => {
        console.log("[audio_player] stop fail,", err);
        this.setStatus(AudioPlayer.STATUS.error);
        fail && fail();
      });
    } 
    else {
      console.log("[audio_player] err: audio player status is ", this.status, "cannot stop");
      fail && fail();
    }
  }

  seekTo(targetTime) {
    console.log("[audio_player]seek to ", targetTime);
    this.unlayoutClips();
    this.layoutClips(targetTime);
  }
}

if (typeof wx != "undefined") module.exports = AudioPlayer;
