import { makeAutoObservable, runInAction } from 'mobx';
import { getErrorMessage, keys } from '../..';

const defaultVoice: SpeechSynthesisVoice = {
  name: 'Alex',
  lang: 'en-US',
  localService: false,
  voiceURI: 'Alex',
  default: true
};

export interface IUtterance extends SpeechSynthesisUtterance {
  id: string;
}
export type IUtteranceStatus = 'idle' | 'speaking' | 'ended' | 'error';

export interface IUtteranceOptions {
  volume?: number;
  rate?: number;
  pitch?: number;
  lang?: string;
  voice?: SpeechSynthesisVoice;
  onstart?: () => void;
  onend?: () => void;
  onerror?: () => void;
  onpause?: () => void;
  onresume?: () => void;
  onmark?: () => void;
  onboundary?: () => void;
}
/**
 * A class that represents a speech synthesis utterance
 * It is a wrapper around the SpeechSynthesisUtterance class
 */
export class Utterance {
  id: string;
  utterance?: SpeechSynthesisUtterance;
  retryCount: number;
  error: string | null;
  status: IUtteranceStatus;
  userOptions: IUtteranceOptions;
  text: string;
  rate: number;
  volume: number;
  pitch: number;
  lang: string;
  voice: SpeechSynthesisVoice;
  mark: SpeechSynthesisEvent | null;
  /**
   * It marks the character position that has been read by the speech
   * synthesis engine.
   */
  readerIndex: number;
  read: string;
  unread: string;

  constructor(id: string, text: string, options: IUtteranceOptions = {}) {
    this.utterance = new SpeechSynthesisUtterance();
    this.id = id;
    this.retryCount = 0;
    this.error = null;
    this.status = 'idle';
    this.userOptions = options;
    this.text = '';
    this.rate = options?.rate || 1;
    this.volume = options?.volume || 1;
    this.pitch = options?.pitch || 1;
    this.lang = options?.lang || 'en-US';
    this.voice = options?.voice || defaultVoice;
    this.mark = null;
    this.readerIndex = 0;
    this.read = '';
    this.unread = text;

    this.setUtteranceOptions(options);
    this.setUtterance(options);
    this.setText(text);

    makeAutoObservable(this);
  }

  restart = () => {
    this.utterance = new SpeechSynthesisUtterance();
    const options = this.userOptions;
    this.setUtteranceOptions(options);
    this.setUtterance(options);
    this.setReaderIndex(0);
  };

  setVolume = (volume: number) => {
    if (this.utterance) {
      this.utterance.volume = volume;
      this.volume = volume;
    }
  };

  setRate = (rate: number) => {
    runInAction(() => {
      if (this.utterance) {
        this.utterance.rate = rate;
        this.rate = rate;
      }
    });
  };

  setMark = (mark: string) => {
    if (this.utterance) {
      this.utterance.text = mark;
      this.text = mark;
    }
  };

  setVoice = (voice: SpeechSynthesisVoice) => {
    if (this.utterance) {
      this.utterance.voice = voice;
      this.voice = voice;
    }
  };

  setPitch = (pitch: number) => {
    if (this.utterance) {
      this.utterance.pitch = pitch;
      this.pitch = pitch;
    }
  };

  setReaderIndex = (index: number) => {
    runInAction(() => {
      this.readerIndex = index;
    });
  };

  /**
   * Increments the retry count
   */
  setRetry = () => {
    this.retryCount = this.retryCount + 1;
  };

  private setUtteranceOptions = (options: IUtteranceOptions) => {
    try {
      keys(options).forEach((key) => {
        if (this.utterance) {
          // do not set the option if it is one of the options we set internally
          if (this.isNotOfOverwrittenValues(key)) {
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            const value = options[key];
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            this.utterance[key] = value;
          }
        }
      });
    } catch (error) {
      console.log({ error });
    }
  };

  private isNotOfOverwrittenValues = (option: string) => {
    return (
      !option.includes('onstart') &&
      !option.includes('onend') &&
      !option.includes('onerror') &&
      !option.includes('voice')
    );
  };

  private setUtterance = (options: IUtteranceOptions) => {
    const setError = this.setError;
    const setStatus = this.setStatus;
    const setReaderIndex = this.setReaderIndex;
    const setRead = this.setRead;
    const setUnread = this.setUnread;
    const read = this.read;

    if ('speechSynthesis' in window) {
      // Create new SpeechSynthesisUtterance object
      const utterance = this.utterance;
      if (utterance) {
        utterance.volume = options?.volume || 1;
        utterance.rate = options?.rate || 1;
        utterance.pitch = options?.pitch || 1;
        utterance.lang = options?.lang || 'en-US';
        // utterance.voice = options?.voice || defaultVoice;

        if (utterance) {
          utterance.onstart = () => {
            // console.log('onstart', { id: this.id });
            options.onstart && options.onstart();
            setStatus('speaking');
          };
          utterance.onerror = (error) => {
            // console.log('error', { id: this.id, error });
            options.onerror && options.onerror();
            setError(getErrorMessage(error));
            setStatus('error');
          };
          utterance.onend = () => {
            // console.log('ended', { id: this.id });
            options.onend && options.onend();
            setStatus('ended');
          };

          utterance.onmark = (event) => {
            // console.log('onmark', { id: this.id });
            options.onmark && options.onmark();
            console.log('onmark', event);
          };

          utterance.onboundary = (event) => {
            // console.log('onboundary', { id: this.id });
            options.onboundary && options.onboundary();
            const text = this.text;
            if (event.name === 'word') {
              setReaderIndex(event.charIndex + event.charLength);
              // mark the word as read
              const _read = text.slice(0, event.charIndex + event.charLength);
              const unreadText = text.slice(event.charIndex + event.charLength);
              setRead(read + _read);
              setUnread(unreadText);
            }
          };
        }
      }

      return utterance;
    }
  };

  setRead = (read: string) => {
    runInAction(() => {
      this.read = read;
    });
  };

  setUnread = (unread: string) => {
    runInAction(() => {
      this.unread = unread;
    });
  };
  /**
   * Sets the status of the utterance.
   * Possible values: idle, active, ended, error.
   * Use idle when the utterance has been created but not started.
   * Use active when the utterance is being spoken.
   * Use ended when the utterance has finished being spoken.
   * Use error when the utterance has encountered an error.
   * @param status IUtteranceStatus
   */
  private setStatus = (status: IUtteranceStatus) => {
    runInAction(() => {
      this.status = status;
    });
  };

  /**
   * Sets the error property in this utterance
   * @param error the error
   */
  private setError = (error: string) => {
    runInAction(() => {
      this.error = error;
    });
  };

  /**
   * Sets the text property in this utterance.
   * This is the text that will be spoken.
   * @param text the text to speak
   */
  setText = (text: string) => {
    runInAction(() => {
      this.text = text;

      if (this.utterance) {
        this.utterance.text = text;
      }
    });
  };
}
