import * as React from 'react';
import { Terminal, ITerminalOptions, ITerminalAddon } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import debounce from 'lodash.debounce';
import compileColor from './colorCode';

export interface IQPXTermProps {
  forwardedRef?: React.MutableRefObject<QPXterm>;
  className?: string;
  options?: ITerminalOptions;
  addons?: Array<ITerminalAddon>;
  style?: React.CSSProperties;

  /**
   * Adds an event listener for when a binary event fires. This is used to
   * enable non UTF-8 conformant binary messages to be sent to the backend.
   * Currently this is only used for a certain type of mouse reports that
   * happen to be not UTF-8 compatible.
   * The event value is a JS string, pass it to the underlying pty as
   * binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`.
   */
  onBinary?(data: string): void;

  /**
   * Adds an event listener for the cursor moves.
   */
  onCursorMove?(): void;

  /**
   * Adds an event listener for when a data event fires. This happens for
   * example when the user types or pastes into the terminal. The event value
   * is whatever `string` results, in a typical setup, this should be passed
   * on to the backing pty.
   */
  onData?(data: string): void;

  /**
   * Adds an event listener for when a key is pressed. The event value contains the
   * string that will be sent in the data event as well as the DOM event that
   * triggered it.
   */
  onKey?(event: { key: string; domEvent: KeyboardEvent }): void;

  /**
   * Adds an event listener for when a line feed is added.
   */
  onLineFeed?(): void;

  /**
   * Adds an event listener for when a scroll occurs. The event value is the
   * new position of the viewport.
   * @returns an `IDisposable` to stop listening.
   */
  onScroll?(newPosition: number): void;

  /**
   * Adds an event listener for when a selection change occurs.
   */
  onSelectionChange?(): void;

  /**
   * Adds an event listener for when rows are rendered. The event value
   * contains the start row and end rows of the rendered area (ranges from `0`
   * to `Terminal.rows - 1`).
   */
  onRender?(event: { start: number; end: number }): void;

  /**
   * Adds an event listener for when the terminal is resized. The event value
   * contains the new size.
   */
  onResize?(event: { cols: number; rows: number }): void;

  /**
   * Adds an event listener for when an OSC 0 or OSC 2 title change occurs.
   * The event value is the new title.
   */
  onTitleChange?(newTitle: string): void;

  /**
   * Attaches a custom key event handler which is run before keys are
   * processed, giving consumers of xterm.js ultimate control as to what keys
   * should be processed by the terminal and what keys should not.
   *
   * @param event The custom KeyboardEvent handler to attach.
   * This is a function that takes a KeyboardEvent, allowing consumers to stop
   * propagation and/or prevent the default action. The function returns
   * whether the event should be processed by xterm.js.
   */
  customKeyEventHandler?(event: KeyboardEvent): boolean;

  onDidMount?(terminal: QPXterm): void;
}

export default class QPXterm extends React.Component<IQPXTermProps> {
  /**
   * The ref for the containing element.
   */
  terminalRef: React.RefObject<HTMLDivElement>;

  /**
   * XTerm.js Terminal object.
   */
  terminal: Terminal;
  fitAddon: FitAddon;
  resizeObserver: ResizeObserver;

  constructor(props: IQPXTermProps) {
    super(props);

    this.terminalRef = React.createRef();
    this.fitAddon = new FitAddon();

    const alignTerminal = debounce(() => this.fitAddon.fit(), 300);

    this.resizeObserver = new ResizeObserver(entries => {
      if (entries.length !== 1) {
        throw new Error('Invalid Container length');
      }

      alignTerminal();
    });

    // Bind Methods
    this.onData = this.onData.bind(this);
    this.onCursorMove = this.onCursorMove.bind(this);
    this.onKey = this.onKey.bind(this);
    this.onBinary = this.onBinary.bind(this);
    this.onLineFeed = this.onLineFeed.bind(this);
    this.onScroll = this.onScroll.bind(this);
    this.onSelectionChange = this.onSelectionChange.bind(this);
    this.onRender = this.onRender.bind(this);
    this.onResize = this.onResize.bind(this);
    this.onTitleChange = this.onTitleChange.bind(this);

    // Setup the XTerm terminal.
    this.terminal = new Terminal(this.props.options);

    // Create Listeners
    this.terminal.onBinary(this.onBinary);
    this.terminal.onCursorMove(this.onCursorMove);
    this.terminal.onData(this.onData);
    this.terminal.onKey(this.onKey);
    this.terminal.onLineFeed(this.onLineFeed);
    this.terminal.onScroll(this.onScroll);
    this.terminal.onSelectionChange(this.onSelectionChange);
    this.terminal.onRender(this.onRender);
    this.terminal.onResize(this.onResize);
    this.terminal.onTitleChange(this.onTitleChange);

    // Add Custom Key Event Handler
    if (this.props.customKeyEventHandler) {
      this.terminal.attachCustomKeyEventHandler(
        this.props.customKeyEventHandler,
      );
    }
  }

  componentDidMount() {
    if (this.terminalRef.current && this.terminal) {
      // Creates the terminal within the container element.
      this.terminal.open(this.terminalRef.current);

      this.terminal.loadAddon(this.fitAddon);
      this.fitAddon.fit();

      // Load addons if the prop exists.
      if (this.props.addons) {
        this.props.addons.forEach(addon => {
          this.terminal?.loadAddon(addon);
        });
      }

      this.resizeObserver.observe(this.terminalRef.current);
      this.props.onDidMount?.(this);
    }
  }

  componentWillUnmount() {
    // When the component unmounts dispose of the terminal and all of its listeners.
    this.terminal?.dispose();
    this.fitAddon.dispose();
    if (this.terminalRef.current) {
      this.resizeObserver.unobserve(this.terminalRef.current);
    }
  }

  private onBinary(data: string) {
    if (this.props.onBinary) this.props.onBinary(data);
  }

  private onCursorMove() {
    if (this.props.onCursorMove) this.props.onCursorMove();
  }

  private onData(data: string) {
    if (this.props.onData) this.props.onData(data);
  }

  private onKey(event: { key: string; domEvent: KeyboardEvent }) {
    if (this.props.onKey) this.props.onKey(event);
  }

  private onLineFeed() {
    if (this.props.onLineFeed) this.props.onLineFeed();
  }

  private onScroll(newPosition: number) {
    if (this.props.onScroll) this.props.onScroll(newPosition);
  }

  private onSelectionChange() {
    if (this.props.onSelectionChange) this.props.onSelectionChange();
  }

  private onRender(event: { start: number; end: number }) {
    if (this.props.onRender) this.props.onRender(event);
  }

  private onResize(event: { cols: number; rows: number }) {
    if (this.props.onResize) this.props.onResize(event);
  }

  private onTitleChange(newTitle: string) {
    if (this.props.onTitleChange) this.props.onTitleChange(newTitle);
  }

  public appendMessage(values: string[], callback?: () => void) {
    for (const value of values) {
      this.terminal.writeln(value, callback);
    }
  }

  public setMessage(
    values: string[],
    options?: {
      useTemplate?: boolean;
      callback?: () => void;
    },
  ) {
    this.terminal.clear();

    if (options?.useTemplate) {

      this.appendMessage(
        values.map(value => compileColor(value)),
        options?.callback,
      );
    } else {
      this.appendMessage(values, options?.callback);
    }
  }

  render() {
    return (
      <div
        role={'qp-xtermjs'}
        className={this.props.className}
        style={this.props.style ?? { overflow: 'hidden' }}
        ref={this.terminalRef}
      />
    );
  }
}
