import React, { useCallback, useState, useEffect } from "react";
import { WindowId, AppState, Dispatch } from "../types";
import * as Actions from "../actionCreators";
import * as Selectors from "../selectors";
import { connect } from "react-redux";

interface DispatchProps {
  setFocus(windowId: WindowId | null): void;
}
interface StateProps {
  focusedWindowId: WindowId | null;
}

interface OwnProps {
  onKeyDown?(e: KeyboardEvent): void;
  windowId: WindowId;
  children: React.ReactNode;
}

type Props = StateProps & DispatchProps & OwnProps;

function FocusTarget(props: Props) {
  const { onKeyDown, focusedWindowId, windowId, setFocus, children } = props;

  const focusHandler = useCallback(() => {
    if (windowId !== focusedWindowId) {
      setFocus(windowId);
    }
  }, [windowId, focusedWindowId, setFocus]);

  const [ref, setRef] = useState<HTMLDivElement | null>(null);

  useEffect(() => {
    if (ref == null || onKeyDown == null) {
      return;
    }
    ref.addEventListener("keydown", onKeyDown);
    return () => ref.removeEventListener("keydown", onKeyDown);
  }, [onKeyDown, windowId, focusedWindowId, ref]);

  // It's possible for a child component to gain focus and then become
  // unmounted. In that case, the browser will return focus to the `<body>`.
  // In the following hook, use a `MutationObserver` to watch for that behavior
  // and refocus the containing FocusTarget when it happens.
  //
  // I tried a number of other approaches using `focus/blur/focusin/focusout` on
  // various DOM nodes, and was unable to find a solution which would trigger in
  // this senario in Firefox. Therefore we use this `MutationObserver` approach.
  useEffect(() => {
    // Only create the `MutationObserver` within the currently focused target.
    if (ref == null || windowId !== focusedWindowId) {
      return;
    }

    const observer = new MutationObserver(mutations => {
      // In the common case we won't have focused the body, so we can do this
      // inexpensive check first to avoid calling the more expensive `O(n)`
      // check of the individual mutations.
      if (document.activeElement !== document.body) {
        return;
      }
      if (mutations.some(mutation => mutation.removedNodes.length > 0)) {
        ref.focus();
      }
    });

    observer.observe(ref, {
      subtree: true,
      attributes: false,
      childList: true,
    });

    return () => observer.disconnect();
  }, [windowId, focusedWindowId, ref]);

  return (
    <div
      ref={setRef}
      onMouseDown={focusHandler}
      onFocus={focusHandler}
      tabIndex={-1}
      style={{ height: "100%", width: "100%" }}
    >
      {children}
    </div>
  );
}

function mapStateToProps(state: AppState): StateProps {
  return {
    focusedWindowId: Selectors.getFocusedWindow(state),
  };
}

function mapDispatchToProps(dispatch: Dispatch): DispatchProps {
  return {
    setFocus: windowId => dispatch(Actions.setFocusedWindow(windowId)),
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(FocusTarget);
