import { makeAutoObservable } from 'mobx';
import { List } from '../models/List';
import { IUtteranceOptions, Utterance } from './textToSpeech/Utterance';
import { getUniqueId } from '../utils/string/getUniqueId';

type TextToSpeechStatusType = 'speaking' | 'paused' | 'cancelled' | 'idle';
/**
 * A class that represents a text to speech synthesizer
 * It is a wrapper around the SpeechSynthesis class, that handles multiple utterances
 */
export class TextToSpeech {
  synthesizer: SpeechSynthesis | null = null;
  selectedVoiceIndex: number;
  utterances: List<Utterance>;
  currentUtterance: Utterance | null;
  textToSpeak: string | null = null;
  status: TextToSpeechStatusType;

  constructor() {
    this.selectedVoiceIndex = 0;
    this.synthesizer = window.speechSynthesis;
    this.utterances = new List<Utterance>();
    this.currentUtterance = null;
    this.textToSpeak = null;
    this.status = 'idle';

    const _cancel = this.cancel;
    window.addEventListener('beforeunload', function () {
      console.log('beforeunload');
      // Stop speech synthesis when refreshing the window
      _cancel();
    });

    makeAutoObservable(this);
  }

  get voiceList() {
    return this.synthesizer
      ?.getVoices()
      .filter(
        (voice) =>
          voice.lang.includes('pt') ||
          voice.lang.includes('en') ||
          voice.lang.includes('es')
      );
  }
  get voices() {
    const voices = this.voiceList;

    return (
      voices?.map((voice, index) => ({
        label: `${voice.name} (${voice.lang}) `,
        value: index,
        details: voice
      })) || []
    );
  }

  setSelectedVoiceIndex = (voiceIndex: number) => {
    this.selectedVoiceIndex = voiceIndex;
  };

  private init = () => {
    if ('speechSynthesis' in window) {
      this.synthesizer = window.speechSynthesis;
    }
  };

  /**
   * This method will immediately pause any utterances that are being spoken.
   */
  pause = () => {
    try {
      if (this.synthesizer?.speaking) {
        this.synthesizer?.pause();
        this.setStatus('paused');
      }
    } catch (error) {
      console.error(error);
    }
  };

  /**
   * This method will cause the browser to resume speaking an
   * utterance that was previously paused.
   */
  resume = () => {
    if (this.synthesizer?.paused) {
      try {
        this.synthesizer.resume();
        this.setStatus('speaking');
      } catch (error) {
        console.error(error);
      }
    }
  };

  /**
   * This method will immediately stop any utterances
   * that are being spoken
   */
  cancel = () => {
    try {
      if (this.synthesizer) {
        this.synthesizer?.cancel();
        this.setStatus('cancelled');
      }
    } catch (error) {
      console.error(error);
    }
  };

  /**
   * Helper method to set a status so the status is observable
   * @param status the status to set
   */
  private setStatus = (status: TextToSpeechStatusType) => {
    console.log('setting speech synthesis status', status);
    this.status = status;
  };

  /**
   * Speaks the text passed in as an argument or the textToSpeak property.
   * If there is a current utterance, it will be cancelled and a new one
   * will be created. If it speaking, it will be ignored.
   * If an id is passed, it will be used to identify the utterance and speak that utterance.
   * @param text the text to speak
   */
  speak = (text?: string, id?: string) => {
    const textToSpeak = text || this.textToSpeak || '';
    // first we check if the synthesizer is available
    if (this.synthesizer === null) {
      // if not, we try to initialize it
      this.init();
    }

    // get the current utterance
    const currentUtterance = this.currentUtterance;
    // there is a current utterance
    if (currentUtterance) {
      if (currentUtterance.id === id) {
        this.speakUtterance(currentUtterance, textToSpeak);
      } else {
        // if it is not the same, we cancel the current utterance
        // and switch to the utterance with the utterance id
        this.cancel();
        if (id) {
          this.speakUtteranceId(id, textToSpeak);
        } else {
          this.speakUtterance(currentUtterance, textToSpeak);
        }
      }
    } else {
      // there's no current utterance, let's create a new utterance
      // and try speaking again
      this.createUtterance(id, text);
      this.speak(text, id);
    }
  };

  private speakUtteranceId = (id: string, text: string) => {
    // check if utterance exists
    let utterance = this.utterances.get(id);

    if (utterance) {
      utterance.setText(text);
      this.setCurrentUtterance(utterance);
    } else {
      utterance = this.createUtterance(id, text);
    }

    this.speakUtterance(utterance, text);
  };

  private speakUtterance = (utterance: Utterance, text: string) => {
    utterance.setText(text);
    const synth = this.synthesizer;
    if (!synth) {
      // if the synthesizer is not available, we try to initialize it
      this.init();
      // and try speaking again
      this.speakUtterance(utterance, text);
    } else {
      if (synth.speaking) {
        if (synth.paused) {
          this.resume();
          return;
        } else {
          // ignore as it is already speaking
          return;
        }
      }
      // let's cancel current speech synthesis
      this.cancel();
      // set the current utterance
      const utteranceInstance = utterance.utterance;

      utteranceInstance && synth.speak(utteranceInstance);
      this.setStatus('speaking');
    }
  };

  restart = (utteranceId: string) => {
    this.synthesizer?.cancel();
    const utterance = this.utterances.get(utteranceId);
    if (utterance) {
      utterance.restart();
      this.speak(utterance.text, utteranceId);
    }
  };

  setCurrentUtterance = (utterance: Utterance) => {
    this.currentUtterance = utterance;
  };

  /**
   * Method to initialize an utterance.
   * If the utterance already exists it sets it as the current utterance.
   * If it doesn't exist, it creates a new utterance and sets it as the current utterance.
   * @param {string} id the id of the utterance, must be unique
   */
  initUtterance = (id: string) => {
    const utterance = this.utterances.get(id);
    if (utterance) {
      this.setCurrentUtterance(utterance);
    } else {
      this.createUtterance(id);
    }
  };

  /**
   * Method to set the rate of the utterance.
   * When setting the rate, unfortunately, the speech synthesis API
   * does not allow us to set the rate of an utterance on the fly. So we
   * have to cancel the current utterance and create a new one with the new rate.
   */
  setRate = (id: string, rate: number) => {
    const wasPreviouslySpeaking = this.status === 'speaking';

    // get the utterance
    const utterance = this.utterances.get(id);
    if (wasPreviouslySpeaking && utterance?.status === 'speaking') {
      // cancel current utterance
      this.cancel();
    }
    if (utterance) {
      utterance.setRate(rate);
      // if it was previously speaking, we speak again
      if (wasPreviouslySpeaking) {
        this.speakUtteranceId(id, utterance.unread);
      }
    }
  };

  /**
   * A method that sets the voice of the speech synthesis.
   * When setting a different voice, unfortunately, the speech synthesis API
   * does not allow us to set the voice of an utterance on the fly. So we
   * have to cancel the current utterance and create a new one with the new
   * voice.
   */
  setVoice = (id: string, voice: SpeechSynthesisVoice) => {
    const wasPreviouslySpeaking = this.status === 'speaking';

    // get the utterance
    const utterance = this.utterances.get(id);
    if (wasPreviouslySpeaking && utterance?.status === 'speaking') {
      // cancel current utterance
      this.cancel();
    }
    if (utterance) {
      utterance.setVoice(voice);
      // if it was previously speaking, we speak again
      if (wasPreviouslySpeaking) {
        this.speakUtteranceId(id, utterance.unread);
      }
    }
  };
  /**
   * Method to create a new utterance and add it to the list of utterances.
   * In addition, it also sets the current utterance to the newly created one.
   * It is used privately
   * @param id
   * @param text
   * @param options
   * @returns
   */
  private createUtterance = (
    id?: string,
    text?: string,
    options?: IUtteranceOptions
  ): Utterance => {
    const utterance = new Utterance(
      id || getUniqueId(),
      text || '',
      options || {}
    );
    this.utterances.set(utterance);
    this.setCurrentUtterance(utterance);
    return utterance;
  };

  setTextToSpeak = (text: string) => {
    this.textToSpeak = text;
  };
}

export const textToSpeech = new TextToSpeech();
