import { useRef, useEffect, useCallback, useImperativeHandle } from 'react';

import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';

import { debounce, canUseDOM } from '@patternfly/react-core';

export interface XTermProps {
  /** The number of columns to resize to */
  cols?: number;
  /** The number of rows to resize to */
  rows?: number;
  fontFamily?: string;
  fontSize?: number;
  /** Terminal title has been changed. */
  onTitleChanged?: (title: string) => void;
  /** Data to be sent from terminal to backend; (data) => {} */
  onData?: (e: string) => void;
  /** A reference object to attach to the xterm */
  innerRef?: React.RefObject<any>;
}

export const XTerm: React.FunctionComponent<XTermProps> = ({
  cols = 80,
  rows = 25,
  fontFamily,
  fontSize,
  onTitleChanged,
  onData,
  innerRef
}) => {
  const terminalRef = useRef<Terminal>(null);
  const ref = useRef<HTMLDivElement>(null);

  useImperativeHandle(innerRef, () => ({
    focusTerminal() {
      if (terminalRef.current) {
        terminalRef.current.focus();
      }
    },
    /**
     * Backend sent data.
     *
     * @param {string} data String content to be writen into the terminal
     */
    onDataReceived: (data: string) => {
      if (terminalRef.current) {
        terminalRef.current.write(data);
      }
    },
    /**
     * Backend closed connection.
     *
     * @param {string} reason String error to be written into the terminal
     */
    onConnectionClosed: (reason: string) => {
      if (terminalRef.current) {
        terminalRef.current.write(`\x1b[31m${reason || 'disconnected'}\x1b[m\r\n`);
        terminalRef.current.refresh(terminalRef.current.rows, terminalRef.current.rows); // start to end row
      }
    }
  }));

  const onBeforeUnload = useCallback((event: any) => {
    // Firefox requires this when the page is in an iframe
    event.preventDefault();

    // see "an almost cross-browser solution" at
    // https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload
    event.returnValue = '';
    return '';
  }, []);

  const onFocusIn = () => {
    window.addEventListener('beforeunload', onBeforeUnload);
  };

  const onFocusOut = useCallback(() => {
    window.removeEventListener('beforeunload', onBeforeUnload);
  }, [onBeforeUnload]);

  useEffect(() => {
    const container = ref.current;
    if (!container) {
      return;
    }

    const fitAddon = new FitAddon();
    terminalRef.current = new Terminal({
      cols,
      rows,
      cursorBlink: true,
      fontFamily,
      fontSize,
      screenReaderMode: true
    });

    const onWindowResize = () => {
      const geometry = fitAddon.proposeDimensions();
      if (geometry && terminalRef.current) {
        terminalRef.current.resize(geometry.cols, geometry.rows);
      }
    };

    if (onData) {
      terminalRef.current.onData(onData);
    }

    if (onTitleChanged) {
      terminalRef.current.onTitleChange(onTitleChanged);
    }

    terminalRef.current.loadAddon(fitAddon);

    let resizeListener: (() => void) | null = null;

    // Defer open so the container is laid out and xterm's renderer has valid dimensions
    // before Viewport._innerRefresh runs (avoids "Cannot read properties of undefined (reading 'dimensions')")
    const rafId = requestAnimationFrame(() => {
      if (!container.isConnected || !terminalRef.current) {
        return;
      }
      terminalRef.current.open(container);
      resizeListener = !rows ? debounce(onWindowResize, 100) : null;
      if (resizeListener && canUseDOM) {
        window.addEventListener('resize', resizeListener);
      }
      if (!rows) {
        onWindowResize();
      }
      terminalRef.current?.focus();
    });

    return () => {
      cancelAnimationFrame(rafId);
      if (resizeListener && canUseDOM) {
        window.removeEventListener('resize', resizeListener);
      }
      terminalRef.current?.dispose();
      terminalRef.current = null;
      onFocusOut();
    };
  }, [cols, fontFamily, fontSize, onData, onFocusOut, onTitleChanged, rows]);

  // ensure react never reuses this div by keying it with the terminal widget
  // Workaround for xtermjs/xterm.js#3172
  return (
    <div
      ref={ref}
      role="list"
      onFocus={onFocusIn}
      onBlur={onFocusOut}
    />
  );
};
XTerm.displayName = 'XTerm';
