import { useApolloClient } from "@apollo/react-hooks";
import styled from "@emotion/styled";
import isHotkey from "is-hotkey";
import { inject, observer } from "mobx-react";
import React, { useEffect, useMemo } from "react";
import { createEditor, Editor, Node, Point, Range, Transforms } from "slate";
import { withHistory } from "slate-history";
import { Editable, ReactEditor, Slate, withReact } from "slate-react";
import { StoreProps } from "../../platform/SlideshowStore";
import withPlatform, { PlatformProps } from "../../platform/withPlatform";
import { deserialize, toggleBlock, toggleMark } from "./EditorToolbar";

export enum HOTKEYS {
  ENTER = "enter",
  TAB = "tab",
  SHIFT_TAB = "shift+tab",
  CMD_B = "mod+b",
  CMD_I = "mod+i",
  CMD_U = "mod+u",
  CMD_ALT_1 = "mod+alt+1",
  CMD_ALT_2 = "mod+alt+2",
  CMD_ALT_3 = "mod+alt+3",
  CMD_H = "mod+h",
}

const HOTKEY_MAP = {
  "mod+b": "bold",
  "mod+i": "italic",
  "mod+u": "underline",
  "mod+h": "tag",
  "mod+alt+1": "heading-one",
  "mod+alt+2": "heading-two",
  "mod+alt+3": "heading-three",
};

const MARKDOWN = {
  "*": "list-item",
  "-": "list-item",
  "+": "list-item",
  "#": "heading-one",
  "##": "heading-two",
  "###": "heading-three",
};

type Shortcut = { keys: string; callback: () => any };

const prefilledText = [{ type: "paragraph", children: [{ text: "" }] }];

type Props = {
  content: Node[];
  setContent: (content: Node[]) => any;
  shortcuts?: Shortcut[];
} & PlatformProps &
  StoreProps;

function OmniscientEditor(props: Props) {
  // NEEDS TO BE A REF, trying to keep track of it as state messes up the editor re-renders
  const isMouseDown = React.useRef(false);
  const highlightModalDiv = React.useRef<HTMLDivElement>(null);
  const client = useApolloClient();

  const editor = useMemo(
    () => withMarkdown(withHistory(withReact(createEditor()))),
    []
  );

  const focusEditor = () => {
    console.log("FOCUS");
    ReactEditor.focus(editor);
    Transforms.select(editor, Editor.end(editor, []));
  };

  useEffect(() => {
    window.addEventListener("editor-focus", focusEditor);
    return () => {
      window.removeEventListener("editor-focus", focusEditor);
    };
  });

  const editorOnChange = React.useRef(editor.onChange);

  // Need to have this ref to remember where the editor selection was before the modal opened
  const editorSelection = React.useRef(editor.selection);

  return (
    <Slate
      editor={editor}
      value={props.content}
      onChange={(value) => {
        props.setContent(value);
      }}
    >
      <Editable
        placeholder="Press enter to start typing"
        renderElement={(props) => {
          return <Element {...props} />;
        }}
        renderLeaf={(props: any) => {
          return (
            <Leaf
              {...({
                ...props,
                editor: editor,
                saveContent: null,
                store: props.store,
              } as any)}
            />
          );
        }}
        onKeyDown={(e) => {
          if (props.shortcuts) {
            for (const index in props.shortcuts) {
              const hotkey = props.shortcuts[index].keys;
              if (isHotkey(hotkey, e as any)) {
                handleHotkey(hotkey, props.shortcuts, editor, e);
                return;
              }
            }
          }
          for (const hotkey in HOTKEYS) {
            const keyboardHotkey = (HOTKEYS as any)[hotkey];
            if (isHotkey(keyboardHotkey, e as any)) {
              handleHotkey(keyboardHotkey, props.shortcuts, editor, e);
              return;
            }
          }
        }}
      />
    </Slate>
  );
}

export default withPlatform(inject("store")(observer(OmniscientEditor)));

// Markdown handling
const withMarkdown = (editor: ReactEditor) => {
  const { deleteBackward, insertText, insertData } = editor;

  editor.insertText = (text) => {
    const { selection } = editor;

    if (text === " " && selection && Range.isCollapsed(selection)) {
      const { anchor } = selection;
      const block = Editor.above(editor, {
        match: (n) => Editor.isBlock(editor, n),
      });
      const path = block ? block[1] : [];
      const start = Editor.start(editor, path);
      const range = { anchor, focus: start };
      const beforeText = Editor.string(editor, range);
      const type = (MARKDOWN as any)[beforeText];
      if (type) {
        Transforms.select(editor, range);
        Transforms.delete(editor);
        Transforms.setNodes(
          editor,
          {
            type: type,
          },
          { match: (n) => Editor.isBlock(editor, n) }
        );
        if (type === "list-item") {
          const list = { type: "bulleted-list", children: [] };
          Transforms.wrapNodes(editor, list, {
            match: (n) => n.type === "list-item",
          });
        }

        return;
      }
    }

    insertText(text);
  };

  editor.deleteBackward = (...args) => {
    const { selection } = editor;

    if (selection && Range.isCollapsed(selection)) {
      const match = Editor.above(editor, {
        match: (n) => Editor.isBlock(editor, n),
      });

      if (match) {
        const [block, path] = match;
        const start = Editor.start(editor, path);

        if (
          block.type === "list-item" &&
          path.length <= 2 &&
          Point.equals(selection.anchor, start)
        ) {
          Transforms.setNodes(editor, {
            type: "paragraph",
          });
          Transforms.liftNodes(editor, {
            match: (n) => n.type === "paragraph",
          });

          return;
        } else if (
          block.type !== "paragraph" &&
          Point.equals(selection.anchor, start)
        ) {
          Transforms.setNodes(editor, { type: "paragraph" });

          if (block.type === "list-item") {
            Transforms.unwrapNodes(editor, {
              match: (n) => n.type === "bulleted-list",
            });
          }

          return;
        }
      }

      deleteBackward(...args);
    }
  };

  editor.insertData = (data) => {
    const html = data.getData("text/html");
    const plaintext = data.getData("text/plain");

    if (html) {
      const parsed = new DOMParser().parseFromString(html, "text/html");
      const fragment = deserialize(parsed.body);
      Transforms.insertFragment(editor, fragment);
      return;
    }

    insertData(data);
  };

  return editor;
};

// Hotkey Handling
const handleHotkey = (
  keypress: string,
  shortcuts: Shortcut[] | undefined,
  editor: Editor,
  event: any // some keyboard event`
) => {
  // Predefined shortcuts
  if (shortcuts) {
    for (const index in shortcuts) {
      const hotkey = shortcuts[index].keys;
      const callback = shortcuts[index].callback;
      if (hotkey === keypress) {
        callback();
        event.preventDefault();
        return;
      }
    }
  }

  // Editor's built in shortcuts
  switch (keypress) {
    case HOTKEYS.ENTER:
      handleEnter(editor, event);
      return;
    case HOTKEYS.TAB:
      handleTab(editor);
      event.preventDefault();
      return;
    case HOTKEYS.SHIFT_TAB:
      handleShiftTab(editor);
      event.preventDefault();
      return;
    case HOTKEYS.CMD_B:
    case HOTKEYS.CMD_I:
    case HOTKEYS.CMD_U:
      toggleMark(HOTKEY_MAP[keypress], editor);
      event.preventDefault();
      return;
    case HOTKEYS.CMD_ALT_1:
    case HOTKEYS.CMD_ALT_2:
    case HOTKEYS.CMD_ALT_3:
      toggleBlock(HOTKEY_MAP[keypress], editor);
      event.preventDefault();
      return;
  }
};

const handleEnter = (editor: Editor, event: any) => {
  const { selection } = editor;

  const match = Editor.above(editor, {
    match: (n) => Editor.isBlock(editor, n),
  });

  if (match && selection && Range.isCollapsed(selection)) {
    const [block, path] = match;
    const start = Editor.start(editor, path);

    if (
      block.type === "list-item" &&
      path.length <= 2 &&
      Point.equals(selection.anchor, start)
    ) {
      event.preventDefault();
      Transforms.setNodes(editor, {
        type: "paragraph",
      });
      Transforms.liftNodes(editor, {
        match: (n) => n.type === "paragraph",
      });
    } else if (
      block.type === "list-item" &&
      Point.equals(selection.anchor, start)
    ) {
      event.preventDefault();
      Transforms.liftNodes(editor, {
        match: (n) => n.type === "list-item",
      });
    }
  }
};

const handleTab = (editor: Editor) => {
  const match = Editor.above(editor, {
    match: (n) => Editor.isBlock(editor, n),
  });

  if (match) {
    const [block, path] = match;
    if (block.type === "list-item") {
      const list = { type: "bulleted-list", children: block.children };
      Transforms.wrapNodes(editor, list, {
        match: (n) => n.type === "list-item",
      });
    }
  }
};

const handleShiftTab = (editor: Editor) => {
  const match = Editor.above(editor, {
    match: (n) => Editor.isBlock(editor, n),
  });
  if (match) {
    const [block, path] = match;

    if (block.type === "list-item") {
      if (path.length <= 2) {
        Transforms.setNodes(editor, {
          type: "paragraph",
        });
        Transforms.liftNodes(editor, {
          match: (n) => n.type === "paragraph",
        });
      } else {
        Transforms.liftNodes(editor, {
          match: (n) => n.type === "list-item",
        });
      }
    }
  }
};

const Element = ({ attributes, children, element }: any) => {
  switch (element.type) {
    case "block-quote":
      return <blockquote {...attributes}>{children}</blockquote>;
    case "bulleted-list":
      return <Ul {...attributes}>{children}</Ul>;
    case "heading-one":
      return <h1 {...attributes}>{children}</h1>;
    case "heading-two":
      return <h2 {...attributes}>{children}</h2>;
    case "heading-three":
      return <h3 {...attributes}>{children}</h3>;
    case "list-item":
      return <li {...attributes}>{children}</li>;
    case "numbered-list":
      return <ol {...attributes}>{children}</ol>;
    default:
      return <P {...attributes}>{children}</P>;
  }
};

const Ul = styled.ul`
  margin-block-start: 0px;
  margin-block-end: 0px;
  list-style-type: disc;
`;

const P = styled.p`
  margin-block-start: 0.5em;
  margin-block-end: 0.5em;
`;

const Leaf = (props: any) => {
  const { attributes, leaf, editor, store } = props;
  let { children } = props;

  if (leaf.bold) {
    children = <strong>{children}</strong>;
  }

  if (leaf.code) {
    children = <code>{children}</code>;
  }

  if (leaf.italic) {
    children = <em>{children}</em>;
  }

  if (leaf.underline) {
    children = <u>{children}</u>;
  }

  return <span {...attributes}>{children}</span>;
};

const DraftHighlight = styled.span`
  background-color: gray;
`;
