import { action, computed, makeObservable, observable } from "mobx";
import { audioLog } from "./AudioLog";
// import ImpulseResponseAudio from "../samples/impulse-responses/echohall.wav";

const DEFAULT_EQ_VALUE = 1;
const DEFAULT_VOLUME_VALUE = 0.5;

export default class AudioSource {
  analyzerNode?: AnalyserNode;
  audioContext?: AudioContext; // AudioContext;
  baseEq?: BiquadFilterNode; // to control low frequency (bass)
  bassEqValue: number;
  filter?: BiquadFilterNode;
  gainNode?: GainNode; // to control of volume
  liveFeedback: boolean;
  midEq?: BiquadFilterNode; // to control medium frequency
  midEqValue: number;
  source: any;
  trebleEq?: BiquadFilterNode; // to control high frequency
  trebleEqValue: number;
  volumeValue: number;

  constructor() {
    this.analyzerNode = undefined;
    this.audioContext = undefined;
    this.baseEq = undefined;
    this.bassEqValue = DEFAULT_EQ_VALUE;
    this.filter = undefined;
    this.gainNode = undefined;
    this.liveFeedback = false;
    this.midEq = undefined;
    this.midEqValue = DEFAULT_EQ_VALUE;
    this.source = null;
    this.trebleEq = undefined;
    this.trebleEqValue = DEFAULT_EQ_VALUE;
    this.volumeValue = DEFAULT_VOLUME_VALUE;

    this.initContext();

    makeObservable(this, {
      analyzerNode: observable,
      audioContext: observable,
      baseEq: observable,
      bassEqValue: observable,
      filter: observable,
      gainNode: observable,
      hasSource: computed,
      initContext: action,
      initCustomFilter: action,
      initWithMediaFilepath: action,
      initWithStream: action,
      liveFeedback: observable,
      getMediaStreamDestination: action,
      midEq: observable,
      midEqValue: observable,
      resetControls: action,
      setBassEq: action,
      setMidEq: action,
      setTrebleEq: action,
      setVolume: action,
      connectDestinationStream: action,
      source: observable,
      toggleLiveFeedback: action,
      trebleEq: observable,
      trebleEqValue: observable,
      volumeValue: observable,
    });
  }

  get analyzer(): AnalyserNode | undefined {
    return this.analyzerNode;
  }

  get hasSource() {
    return (
      this.audioContext && this.audioContext.state !== "suspended"
    );
  }

  get isSuspended() {
    return (
      this.audioContext && this.audioContext.state === "suspended"
    );
  }

  initContext = () => {
    if (!this.audioContext && typeof window !== "undefined") {
      this.audioContext = new AudioContext();
      audioLog.logAudioSource("Created audio context");
    }

    if (this.audioContext) {
      this.analyzerNode = new AnalyserNode(this.audioContext, {
        fftSize: 256,
      });

      this.gainNode = new GainNode(this.audioContext, {
        gain: this.volumeValue,
      });

      this.baseEq = new BiquadFilterNode(this.audioContext, {
        type: "lowshelf",
        frequency: 500,
        gain: this.bassEqValue,
      });

      this.midEq = new BiquadFilterNode(this.audioContext, {
        type: "peaking",
        frequency: 1500,
        Q: Math.SQRT1_2,
        gain: this.midEqValue,
      });

      this.trebleEq = new BiquadFilterNode(this.audioContext, {
        type: "highshelf",
        frequency: 3000,
        Q: Math.SQRT1_2,
        gain: this.trebleEqValue,
      });
    }
  };

  initCustomFilter = (
    type: BiquadFilterType,
    frequency?: number,
    Q?: number,
    gain?: number,
    detune?: number,
  ) => {
    if (this.audioContext) {
      this.filter = new BiquadFilterNode(this.audioContext, {
        type,
        frequency,
        Q,
        gain,
        detune,
      });
      audioLog.logAudioSource("Initialized custom filter");
    }
  };

  initWithMediaFilepath = async (
    filepath: string,
  ): Promise<AudioBuffer | null> => {
    if (this.audioContext) {
      const response = await fetch(filepath);
      const arrayBuffer = await response.arrayBuffer();
      if (arrayBuffer) {
        const audioBuffer = this.audioContext.decodeAudioData(
          arrayBuffer,
        );
        this.source = audioBuffer;
        audioLog.logAudioSource("Set audio buffer", audioBuffer);
        return this.source;
      }
    }
    return null;
  };

  /**
   * initWithStream initializes the stream and connects all sources nodes to that
   * stream
   * @param stream Media stream
   * @param output A flag for live feedback, if false, no live feedback will play
   */
  initWithStream = async (
    stream: MediaStream,
    liveFeedback?: boolean,
  ): Promise<MediaStreamAudioSourceNode | undefined> => {
    if (this.audioContext && this.isSuspended) {
      await this.audioContext.resume;
    }
    if (
      this.audioContext &&
      this.baseEq &&
      this.midEq &&
      this.trebleEq &&
      this.gainNode &&
      this.analyzerNode
    ) {
      this.source = this.audioContext.createMediaStreamSource(stream);
      audioLog.logAudioSource(
        "Source set to MediaStreamAudioSourceNode",
      );
      if (this.source && this.filter) {
        // connect to custom filter if there's any
        this.source.connect(this.filter);
        audioLog.logAudioSource("Connected to custom filters nodes");
      }
      if (this.source) {
        this.source
          .connect(this.baseEq)
          .connect(this.midEq)
          .connect(this.trebleEq)
          .connect(this.gainNode)
          .connect(this.analyzerNode);
        audioLog.logAudioSource("Connected audio nodes");
        if (liveFeedback === true) {
          this.toggleLiveFeedback(true);
        }
        return this.source;
      }
    } else {
      console.warn(
        "No audio context found - unable to initialize with stream",
      );
    }

    return undefined;
  };

  toggleLiveFeedback = async (value?: boolean) => {
    if (!this.source || !this.audioContext) {
      console.warn(
        "No source or audio context when toggling live feedback",
      );
      return;
    }
    if (value !== undefined) {
      this.liveFeedback = value;
    } else {
      this.liveFeedback = !this.liveFeedback;
    }

    if (this.source) {
      if (this.liveFeedback === true && this.audioContext) {
        this.source.connect(this.audioContext.destination);
        audioLog.logAudioSource(
          "Connected audio context to output destination",
        );
      }
      if (this.liveFeedback === false && this.audioContext) {
        this.source.disconnect(this.audioContext.destination);
        audioLog.logAudioSource(
          "Disconnect audio context from output destination",
        );
      }
    }
  };

  setVolume = (value: number) => {
    if (this.gainNode && this.audioContext) {
      this.volumeValue = value;
      this.gainNode.gain.setTargetAtTime(
        value,
        this.audioContext.currentTime,
        0.01,
      );
      audioLog.logAudioSource("Volume", value);
    } else {
      console.warn(
        "Unable to set volume: No gain node and audio context available",
      );
    }
  };

  setBassEq = (value: number) => {
    if (this.baseEq && this.audioContext) {
      this.bassEqValue = value;
      this.baseEq.gain.setTargetAtTime(
        value,
        this.audioContext.currentTime,
        0.01,
      );
    } else {
      console.warn(
        "Unable to set volume: No baseEq gain and audio context available",
      );
    }
  };

  setMidEq = (value: number) => {
    if (this.midEq && this.audioContext) {
      this.midEqValue = value;
      this.midEq.gain.setTargetAtTime(
        value,
        this.audioContext.currentTime,
        0.01,
      );
    } else {
      console.warn(
        "Unable to set volume: No midEq gain and audio context available",
      );
    }
  };

  setTrebleEq = (value: number) => {
    if (this.trebleEq && this.audioContext) {
      this.trebleEqValue = value;
      this.trebleEq.gain.setTargetAtTime(
        value,
        this.audioContext.currentTime,
        0.01,
      );
    } else {
      console.warn(
        "Unable to set volume: No trebleEq gain and audio context available",
      );
    }
  };

  getMediaStreamDestination = ():
    | MediaStreamAudioDestinationNode
    | undefined => {
    if (!this.audioContext) {
      console.warn(
        "No audio context found when trying to get media stream",
      );
      return undefined;
    }
    if (this.audioContext) {
      const destination = this.audioContext.createMediaStreamDestination();
      audioLog.logAudioSource(
        "media stream requested",
        destination.stream,
      );

      return destination;
    }

    return undefined;
  };

  connectDestinationStream = (
    stream: MediaStreamAudioDestinationNode,
  ) => {
    if (this.source) {
      this.source.connect(stream);
      audioLog.logAudioSource("Connected destination stream");
    }
  };

  resetControls = (
    volume?: number,
    midEqValue?: number,
    bassEqValue?: number,
    trebleEqValue?: number,
  ) => {
    this.setVolume(volume || DEFAULT_VOLUME_VALUE);
    this.setBassEq(bassEqValue || DEFAULT_EQ_VALUE);
    this.setMidEq(midEqValue || DEFAULT_EQ_VALUE);
    this.setTrebleEq(trebleEqValue || DEFAULT_EQ_VALUE);
  };

  addReverb = async () => {
    // if (this.audioContext) {
    //   const audioBuffer = this.source;
    //   this.source = this.audioContext.createBufferSource();
    //   this.source.buffer = audioBuffer;
    //   let convolver = this.audioContext.createConvolver();
    //   convolver.buffer = await this.audioContext.decodeAudioData(
    //     await (await fetch(ImpulseResponseAudio)).arrayBuffer(),
    //   );
    //   this.source
    //     .connect(convolver)
    //     .connect(this.gainNode)
    //     .connect(this.audioContext.destination);
    // }
  };

  static getAudioBuffer = async (
    blob: Blob,
  ): Promise<AudioBuffer | undefined> => {
    const url = URL.createObjectURL(blob);
    const audioContext = new AudioContext();
    const response = await fetch(url);
    const arrayBuffer = await response.arrayBuffer();
    if (arrayBuffer) {
      const audioBuffer = audioContext.decodeAudioData(arrayBuffer);
      return audioBuffer;
    }
    return new Promise((resolve) => resolve(undefined));
  };
}
