import React, { useRef, useEffect, useReducer } from 'react';
import ReactDOM from 'react-dom';

import { makeStyles, createStyles } from '@material-ui/core/styles';
import { Howl } from 'howler';
import uEmojiParser from 'universal-emoji-parser';
import { Emote, emoteNameToUrl } from './Emotes';

const ipc = require('electron').ipcRenderer;

const DEFAULT_TIMEOUT = 4000;

const useStyles = makeStyles(() =>
  createStyles({
    root: {
      flexGrow: 1,
      backgroundColor: 'blue !important',
      height: '100vh',
      // padding: theme.spacing(4),
    },
    textTable: {
      display: 'table',
      height: '100%',
    },
    text: {
      color: 'white',
      fontSize: '3rem',
      textAlign: 'left',
      display: 'table-cell',
      verticalAlign: 'bottom',
    },
    bubble: {
      backgroundColor: localStorage.getItem('bubbleColor') || '#000',
      fontFamily: "'Baloo Da 2', cursive",
      padding: '20px',
      border: '3px solid #a9a9a9',
      borderRadius: '8px',
    },
    span: {
      display: 'block',
    },
    hidden: {
      display: 'none',
    },
  })
);

// eslint-disable-next-line react/display-name
const SpeechDisplay = React.forwardRef<HTMLSpanElement>((_props, ref) => {
  const classes = useStyles();
  return <span ref={ref} className={classes.span} />;
});

function uniqueHash() {
  const alpha =
    '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  const length = 8;
  let rtn = '';
  for (let i = 0; i < length; i += 1) {
    rtn += alpha.charAt(Math.floor(Math.random() * alpha.length));
  }
  return rtn;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function SpeechPhrase(props: any) {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const speechDisplay: any = useRef<HTMLSpanElement>(null);
  const { message } = props;

  // TODO Test for performance impact of reading settings on every input
  const speed = parseInt(localStorage.getItem('textSpeed') || '75', 10);
  const fontSize = parseInt(localStorage.getItem('fontSize') || '48', 10);
  const fontColor = localStorage.getItem('fontColor') || '#ffffff';
  const fontWeight = parseInt(localStorage.getItem('fontWeight') || '400', 10);
  const soundFileName = localStorage.getItem('soundFileName');
  const speechSound = new Howl({
    src: [`../assets/sounds/${soundFileName}`],
    volume: parseFloat(localStorage.getItem('volume') || '50') / 100,
  });
  const timeBetweenChars: number = 150 - speed;
  const emojiRegex = /:([^:]+):/g;
  const emojis = [...message.matchAll(emojiRegex)].filter(
    (e) => uEmojiParser.parse(e[0]) !== e[0]
  );
  const emotes = [...message.matchAll(/\w+/g)].filter(
    (e) => e[0] in emoteNameToUrl
  );

  // Account for the time to print a message so it doesn't disappear early
  const timeout: number =
    message.length * timeBetweenChars +
    (message.length > 0 ? DEFAULT_TIMEOUT : 0);

  useEffect(() => {
    speechDisplay.current.style.fontSize = fontSize;
    speechDisplay.current.style.color = fontColor;
    speechDisplay.current.style.fontWeight = fontWeight;

    // `i` is the message character index
    let i = 0;
    const typewriter = () => {
      if (i < message.length) {
        speechSound.stop();
        if (message.charAt(i) !== ' ') {
          speechSound.play();
        }

        // TODO: the reference object is initialized as null but sometimes comes
        // through as null here even though it is mounted on the component
        // hack to bypass this but should figure out why
        if (speechDisplay.current) {
          // Check whether this character is the start of an emoji or emote.
          const foundEmoji = emojis.find((emoji) => emoji.index === i);
          const foundEmote = emotes.find((emote) => emote.index === i);
          if (foundEmoji) {
            const emojiString = foundEmoji[0];
            speechDisplay.current.innerHTML += uEmojiParser.parse(emojiString);
            i += emojiString.length;
          } else if (foundEmote) {
            const emoteName = foundEmote[0];
            const emoteContainer = document.createElement('span');
            ReactDOM.render(<Emote emoteName={emoteName} />, emoteContainer);
            speechDisplay.current.appendChild(emoteContainer);
            i += emoteName.length;
          } else {
            // TODO: Doublecheck escaping.
            speechDisplay.current.innerHTML += message.charAt(i);
            i += 1;
          }
        }

        setTimeout(typewriter, timeBetweenChars);
      } else {
        setTimeout(() => {
          // TODO: the reference object is initialized as null but sometimes comes
          // through as null here even though it is mounted on the component
          // hack to bypass this but should figure out why
          if (speechDisplay.current) {
            speechDisplay.current.innerHTML = '';
          }
          props.dispatch({ type: 'shift' });
        }, timeout);
      }
    };
    setTimeout(typewriter, 0);
    // Only register timer interactions once per component regardless of state change
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return <SpeechDisplay ref={speechDisplay} />;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function reducer(state: any, action: any) {
  switch (action.type) {
    case 'push':
      return { phrases: [...state.phrases, action.phrase] };
    case 'shift':
      return { phrases: state.phrases.slice(1) };
    default:
      return state;
  }
}

export default function OBS() {
  const [state, dispatch] = useReducer(reducer, { phrases: [] });

  // Only register ipc speech callback once after component is mounted
  useEffect(() => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    ipc.on('speech', (_event: any, message: any) => {
      const key: string = uniqueHash();
      dispatch({ type: 'push', phrase: { message, key } });
    });
  }, []);

  const classes = useStyles();
  return (
    <div className={classes.root}>
      <div className={classes.textTable}>
        <div className={classes.text}>
          <div
            className={
              state.phrases.length <= 0 ? classes.hidden : classes.bubble
            }
          >
            {state.phrases.map((phrase: { message: string; key: string }) => {
              return (
                <SpeechPhrase
                  key={phrase.key}
                  message={phrase.message}
                  dispatch={dispatch}
                />
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
}