import { Store } from 'redux';

import { RootState } from 'mweb/common/reducers/root';
import { chatUpdateMessages, chatReset } from 'mweb/chat/chatAction';
import { ChatEvent } from 'mweb/chat/events/baseChatEvent';
import { HOSTING, UNHOST } from 'mweb/chat/events/statusEvent';
import ChatBuffer, { ChatEventMessageHandler } from 'mweb/chat/chatBuffer';
import { WorkerMessage } from 'mweb/chat/chatService';
// chat WebWorker constructor via webpack's worker-loader
const ChatWorker = require('mweb/chat/chatWorker');

interface ChatWorkerInstance {
  postMessage: (message: WorkerMessage) => void;
  addEventListener: (
    messageType: 'message',
    messageHandler: ChatEventMessageHandler,
  ) => void;
  removeEventListener: (
    messageType: 'message',
    messageHandler: ChatEventMessageHandler,
  ) => void;
}

interface ChatConfig {
  store: Store<RootState>;
  chatTargetDetails: (
    state: RootState,
  ) => { name: string; id: string | undefined };
  hostingHandler: (channel: string, hostedChannel: string) => void;
  bufferSize: number;
}

interface ChatResources {
  worker: ChatWorkerInstance;
  buffer: ChatBuffer;
  rafHandle: number;
  type: 'ACTIVE';
}

interface NullChatResources {
  worker: null;
  buffer: null;
  rafHandle: null;
  type: 'NULL';
}

export function chatWorkerBridge({
  store,
  hostingHandler,
  chatTargetDetails,
  bufferSize,
}: ChatConfig): () => void {
  function initializeChatResources(): ChatResources {
    return {
      worker: new ChatWorker() as ChatWorkerInstance,
      buffer: new ChatBuffer(bufferSize),
      rafHandle: requestAnimationFrame(updateChatState),
      type: 'ACTIVE',
    };
  }

  function initializeNullChatResources(): NullChatResources {
    return {
      worker: null,
      buffer: null,
      rafHandle: null,
      type: 'NULL',
    };
  }

  let channelName: string;
  let channelID: string | undefined;
  let chatResources:
    | ChatResources
    | NullChatResources = initializeNullChatResources();

  function updateChatState(_time: number): void {
    if (!chatResources.buffer) {
      console.error('Called updateChatState with no chatBuffer defined');
      return;
    }
    if (chatResources.buffer.isDirty) {
      const chatBufferArray = chatResources.buffer.toArray();
      store.dispatch(chatUpdateMessages(chatBufferArray));
    }
    chatResources.rafHandle = requestAnimationFrame(updateChatState);
  }

  function hostingMonitor(message: { data: ChatEvent }): void {
    const event = message.data;
    if (event.type === HOSTING) {
      hostingHandler(event.channel, event.hostedChannel);
    } else if (event.type === UNHOST) {
      hostingHandler(event.channel, '');
    }
  }

  return () => {
    const { name, id } = chatTargetDetails(store.getState());
    // if either id or channel are missing, kill worker if exists
    if (!name || !id) {
      if (chatResources.type === 'ACTIVE') {
        chatResources.worker.postMessage({
          command: 'CHAT_WORKER_DISCONNECTED',
        });
        chatResources.worker.removeEventListener(
          'message',
          chatResources.buffer.consumeChatEvent,
        );
        chatResources.worker.removeEventListener('message', hostingMonitor);
        cancelAnimationFrame(chatResources.rafHandle);
        chatResources = initializeNullChatResources();
        store.dispatch(chatReset());
      }
    } else if (chatResources.type === 'NULL') {
      chatResources = initializeChatResources();
      // else if there isn't a chat worker, build one
      const polyfillElement = document.getElementById('polyfill-script');
      const polyfillURI =
        polyfillElement && polyfillElement.getAttribute('src');
      chatResources.worker.postMessage({
        command: 'CHAT_WORKER_CONNECTED',
        payload: {
          channelName: name,
          channelID: id,
          polyfillURI,
        },
      });
      chatResources.worker.addEventListener(
        'message',
        chatResources.buffer.consumeChatEvent,
      );
      chatResources.worker.addEventListener('message', hostingMonitor);
      chatResources.rafHandle = requestAnimationFrame(updateChatState);
    } else if (name !== channelName || id !== channelID) {
      // else if the target chat channel has changed, change channel
      cancelAnimationFrame(chatResources.rafHandle);
      chatResources.worker.removeEventListener(
        'message',
        chatResources.buffer.consumeChatEvent,
      );
      chatResources.buffer = new ChatBuffer(bufferSize);
      chatResources.worker.postMessage({
        command: 'CHAT_WORKER_CHANNEL_CHANGED',
        payload: {
          channelName: name,
          channelID: id,
        },
      });
      chatResources.worker.addEventListener(
        'message',
        chatResources.buffer.consumeChatEvent,
      );
      chatResources.rafHandle = requestAnimationFrame(updateChatState);
    }

    channelName = name;
    channelID = id;
  };
}
