import {
  action,
  computed,
  makeObservable,
  observable,
  runInAction
} from 'mobx';
import { audioLog } from './AudioLog';
import { translate } from './translate';

export interface MicrophoneOption {
  value: string;
  label: string;
}
export interface MicrophoneInfo {
  channelCount: {
    min: number | undefined;
    max: number | undefined;
    value: number | undefined;
  };
  sampleRate: {
    min: number | undefined;
    max: number | undefined;
    value: number | undefined;
  };
  sampleSize: {
    min: number | undefined;
    max: number | undefined;
    value: number | undefined;
  };
  latency: {
    min: number | undefined;
    max: number | undefined;
  };
  autoGainControl: boolean | undefined;
  echoCancellation: boolean | undefined;
  noiseSuppression: boolean | undefined;
}

export default class Microphone {
  devices: MediaDeviceInfo[];
  selectedDevice?: MediaStream;
  selectedDeviceId?: string; // selected microphone
  stream: MediaStream | undefined;
  track: MediaStreamTrack | undefined;

  constructor() {
    this.stream = undefined;
    this.devices = [];
    this.selectedDevice = undefined;
    this.selectedDeviceId = undefined;
    this.track = undefined;

    makeObservable(this, {
      currentMicrophone: computed,
      currentTrack: computed,
      devices: observable,
      init: action,
      isReady: computed,
      loadDefault: action,
      loadSystemDevices: action,
      microphoneDetails: computed,
      microphoneOptions: computed,
      onMicrophoneChange: action,
      resetMicData: action,
      selectedDevice: observable,
      selectedDeviceId: observable,
      setMediaStreamTrack: action,
      setSelectedDevice: action,
      stopStreaming: action,
      pauseStreaming: action,
      resumeStreaming: action,
      stream: observable,
      systemInputs: computed,
      track: observable,
      streamingStream: computed
    });
  }

  get isReady() {
    const isMicrophoneReady = this.stream !== undefined;
    isMicrophoneReady && audioLog.logMicrophone('Ready');
    return isMicrophoneReady;
  }

  get currentTrack() {
    return this.track;
  }

  get currentMicrophone() {
    const selectedDeviceId = this.selectedDeviceId;
    const selectedInput = this.microphoneOptions.filter(
      (option) => option.value === selectedDeviceId
    );
    if (selectedInput.length > 0) {
      return selectedInput[0];
    }
    return undefined;
  }

  get streamingStream(): MediaStream | undefined {
    if (this.track && this.track.enabled === true) {
      return this.stream;
    }
    return undefined;
  }

  get microphoneOptions(): MicrophoneOption[] {
    if (this.devices.length === 0) {
      return [
        {
          value: 'none',
          label: translate('noMic.label')
        }
      ];
    }

    return this.devices.map((d) => ({
      value: d.deviceId,
      label: d.label
    }));
  }

  get systemInputs() {
    return this.devices;
  }

  get microphoneDetails(): MicrophoneInfo {
    const track = this.stream?.getAudioTracks()[0];

    if (!track) {
      return {
        channelCount: {
          min: undefined,
          max: undefined,
          value: undefined
        },
        sampleRate: {
          min: undefined,
          max: undefined,
          value: undefined
        },
        sampleSize: {
          min: undefined,
          max: undefined,
          value: undefined
        },
        latency: {
          min: undefined,
          max: undefined
        },
        autoGainControl: false,
        echoCancellation: false,
        noiseSuppression: false
      };
    }
    const settings = track.getSettings();
    const capabilities = track.getCapabilities();

    return {
      channelCount: {
        min: capabilities.channelCount?.min,
        max: capabilities.channelCount?.max,
        value: settings.channelCount
      },
      sampleRate: {
        min: capabilities.sampleRate?.min,
        max: capabilities.sampleRate?.max,
        value: settings.sampleRate
      },
      sampleSize: {
        min: capabilities.sampleSize?.min,
        max: capabilities.sampleSize?.max,
        value: settings.sampleSize
      },
      latency: {
        // @ts-ignore
        min: capabilities.latency?.min,
        // @ts-ignore
        max: capabilities.latency?.max
      },

      autoGainControl: settings.autoGainControl,
      echoCancellation: settings.echoCancellation,
      noiseSuppression: settings.noiseSuppression
    };
  }

  // Loads all available input devices in the user's computer
  // and sets the default stream to the user's computer's default
  // selected input device (audio input/microphone)
  init = async (): Promise<boolean> => {
    const defaultLoaded = await this.loadDefault();
    const systemDevicesLoaded = await this.loadSystemDevices();
    if (defaultLoaded && systemDevicesLoaded) {
      return true; // default microphone and system devices loaded successfully
    }
    return false;
  };

  // Load default device (microphone) and set default
  // Promise will return true once the default device has been selected
  loadDefault = async (): Promise<boolean> => {
    if (typeof window !== 'undefined') {
      if (window.navigator.mediaDevices) {
        return window.navigator.mediaDevices
          .getUserMedia({ audio: true })
          .then((device: any) => {
            runInAction(() => {
              this.setSelectedDevice(device, 'default');
            });

            return true;
          })
          .catch((err: any) => {
            console.error(err);
            return false;
          });
      }
    }
    return new Promise(() => false);
  };

  // Load all microphone options in the system
  loadSystemDevices = async (): Promise<boolean> => {
    if (typeof window !== 'undefined') {
      return window.navigator.mediaDevices
        .enumerateDevices()
        .then((devices: any) => {
          runInAction(() => {
            this.devices = devices.filter((d: any) => d.kind === 'audioinput');
          });
          audioLog.logMicrophone(
            'Input device list loaded and set successfully'
          );
          return true;
        })
        .catch((err: AsyncGenerator) => {
          console.error(err);
          return false;
        });
    }
    return new Promise(() => false);
  };

  setMediaStreamTrack = (stream: MediaStream | undefined): boolean => {
    if (stream) {
      const audioTracks = stream.getAudioTracks();
      this.track = audioTracks[0];
      this.stream = stream;
      audioLog.logMicrophone('Set MediaStream', stream);
      this.resumeStreaming();
      return true;
    } else {
      console.warn('Attempted to set media stream but stream was undefined');
    }
    return false;
  };

  onMicrophoneChange = async (
    newValue: MicrophoneOption | null | undefined
  ): Promise<boolean> => {
    if (!newValue) {
      return false;
    }
    const selectedDeviceId = newValue.value;
    if (typeof window !== 'undefined') {
      return window.navigator.mediaDevices
        .getUserMedia({ audio: { deviceId: newValue?.value ?? '' } })
        .then((device: MediaStream) => {
          this.setSelectedDevice(device, selectedDeviceId);
          return true;
        })
        .catch((err: any) => {
          console.error(err);
          return false;
        });
    }
    return false;
  };

  setSelectedDevice = (device: MediaStream, deviceId: string): void => {
    this.selectedDevice = device;
    this.selectedDeviceId = deviceId;
    audioLog.logMicrophone('Set selected device', device, deviceId);
  };

  startStream = async (): Promise<boolean> => {
    if (this.selectedDevice) {
      return new Promise((resolve) =>
        resolve(this.setMediaStreamTrack(this.selectedDevice))
      );
    } else {
      return this.loadDefault();
    }
  };

  stopStreaming = () => {
    if (this.track) {
      this.track.stop();
      audioLog.logMicrophone(
        'Stopped streaming microphone - To reuse it again, it needs to be reloaded'
      );
    } else {
      console.warn('Attempted to stop track but track was undefined');
    }
  };

  pauseStreaming = () => {
    // The enabled property on the MediaStreamTrack interface is a Boolean value which is true
    // if the track is allowed to render the source stream or false if it is not. This can be
    //  used to intentionally mute a track. When enabled, a track's data is output from the
    // source to the destination; otherwise, empty frames are output.
    if (this.track) {
      this.track.enabled = false;
    }
  };

  resumeStreaming = () => {
    if (this.track) {
      this.track.enabled = true;
    }
  };

  resetMicData = () => {
    this.stream = undefined;
    this.track = undefined;
    audioLog.logMicrophone('Stream reset');
  };

  printStreamInfo = () => {
    if (this.stream) {
      const audioTracks = this.stream.getAudioTracks();
      this.track = audioTracks[0];
      console.log('Num Audio Tracks', audioTracks.length);
      console.log('Capabilities', audioTracks[0].getCapabilities());
      console.log('Contraints', audioTracks[0].getConstraints());
      console.log('Settings', audioTracks[0].getSettings());
      const mimeTypes = [
        'audio/wav',
        'audio/mpeg',
        'audio/ogg',
        'audio/opus',
        'audio/webm',
        'audio/webm;codecs=opus',
        'audio/webm;codecs=pcm'
      ];
      mimeTypes.forEach((mimeType) =>
        console.log('format', mimeType, MediaRecorder.isTypeSupported(mimeType))
      );
    } else {
      console.log('Stream is undefined');
    }
  };
}
