import { NodeSpec, NodeType, Node as ProsemirrorNode } from "prosemirror-model";
import {
  splitListItem,
  sinkListItem,
  liftListItem,
} from "prosemirror-schema-list";
import {
  Transaction,
  EditorState,
  Plugin,
  TextSelection,
} from "prosemirror-state";
import { findParentNodeClosestToPos } from "prosemirror-utils";
import { DecorationSet, Decoration } from "prosemirror-view";
import { MarkdownSerializerState } from "../lib/markdown/serializer";
import getParentListItem from "../queries/getParentListItem";
import isInList from "../queries/isInList";
import isList from "../queries/isList";
import { Dispatch } from "../types";
import Node from "./Node";

export default class ListItem extends Node {
  get name() {
    return "list_item";
  }

  get schema(): NodeSpec {
    return {
      content: "paragraph block*",
      defining: true,
      draggable: true,
      parseDOM: [{ tag: "li" }],
      toDOM: () => ["li", 0],
    };
  }

  get plugins() {
    return [
      new Plugin({
        state: {
          init() {
            return DecorationSet.empty;
          },
          apply: (
            tr: Transaction,
            set: DecorationSet,
            oldState: EditorState,
            newState: EditorState
          ) => {
            const action = tr.getMeta("li");
            if (!action && !tr.docChanged) {
              return set;
            }

            // Adjust decoration positions to changes made by the transaction
            set = set.map(tr.mapping, tr.doc);

            switch (action?.event) {
              case "mouseover": {
                const result = findParentNodeClosestToPos(
                  newState.doc.resolve(action.pos),
                  (node) =>
                    node.type.name === this.name ||
                    node.type.name === "checkbox_item"
                );

                if (!result) {
                  return set;
                }

                const list = findParentNodeClosestToPos(
                  newState.doc.resolve(action.pos),
                  (node) => isList(node, this.editor.schema)
                );

                if (!list) {
                  return set;
                }

                const start = list.node.attrs.order || 1;

                let listItemNumber = 0;
                list.node.content.forEach((li, _, index) => {
                  if (li === result.node) {
                    listItemNumber = index;
                  }
                });

                const counterLength = String(start + listItemNumber).length;

                return set.add(tr.doc, [
                  Decoration.node(
                    result.pos,
                    result.pos + result.node.nodeSize,
                    {
                      class: `hovering`,
                    },
                    {
                      hover: true,
                    }
                  ),
                  Decoration.node(
                    result.pos,
                    result.pos + result.node.nodeSize,
                    {
                      class: `counter-${counterLength}`,
                    }
                  ),
                ]);
              }
              case "mouseout": {
                const result = findParentNodeClosestToPos(
                  newState.doc.resolve(action.pos),
                  (node) =>
                    node.type.name === this.name ||
                    node.type.name === "checkbox_item"
                );

                if (!result) {
                  return set;
                }

                return set.remove(
                  set.find(
                    result.pos,
                    result.pos + result.node.nodeSize,
                    (spec) => spec.hover
                  )
                );
              }
              default:
            }

            return set;
          },
        },
        props: {
          decorations(state) {
            return this.getState(state);
          },
          handleDOMEvents: {
            mouseover: (view, event) => {
              const { state, dispatch } = view;
              const target = event.target as HTMLElement;
              const li = target?.closest("li");

              if (!li) {
                return false;
              }
              if (!view.dom.contains(li)) {
                return false;
              }
              const pos = view.posAtDOM(li, 0);
              if (!pos) {
                return false;
              }

              dispatch(
                state.tr.setMeta("li", {
                  event: "mouseover",
                  pos,
                })
              );
              return false;
            },
            mouseout: (view, event) => {
              const { state, dispatch } = view;
              const target = event.target as HTMLElement;
              const li = target?.closest("li");

              if (!li) {
                return false;
              }
              if (!view.dom.contains(li)) {
                return false;
              }
              const pos = view.posAtDOM(li, 0);
              if (!pos) {
                return false;
              }

              dispatch(
                state.tr.setMeta("li", {
                  event: "mouseout",
                  pos,
                })
              );
              return false;
            },
          },
        },
      }),
    ];
  }

  keys({ type }: { type: NodeType }) {
    return {
      Enter: splitListItem(type),
      Tab: sinkListItem(type),
      "Shift-Tab": liftListItem(type),
      "Mod-]": sinkListItem(type),
      "Mod-[": liftListItem(type),
      "Shift-Enter": (state: EditorState, dispatch: Dispatch) => {
        if (!isInList(state)) {
          return false;
        }
        if (!state.selection.empty) {
          return false;
        }

        const { tr, selection } = state;
        dispatch(tr.split(selection.to));
        return true;
      },
      "Alt-ArrowUp": (state: EditorState, dispatch: Dispatch) => {
        if (!state.selection.empty) {
          return false;
        }
        const result = getParentListItem(state);
        if (!result) {
          return false;
        }

        const [li, pos] = result;
        const $pos = state.doc.resolve(pos);

        if (
          !$pos.nodeBefore ||
          !["list_item", "checkbox_item"].includes($pos.nodeBefore.type.name)
        ) {
          return false;
        }

        const { tr } = state;
        const newPos = pos - $pos.nodeBefore.nodeSize;

        dispatch(
          tr
            .delete(pos, pos + li.nodeSize)
            .insert(newPos, li)
            .setSelection(TextSelection.near(tr.doc.resolve(newPos)))
        );
        return true;
      },
      "Alt-ArrowDown": (state: EditorState, dispatch: Dispatch) => {
        if (!state.selection.empty) {
          return false;
        }
        const result = getParentListItem(state);
        if (!result) {
          return false;
        }

        const [li, pos] = result;
        const $pos = state.doc.resolve(pos + li.nodeSize);

        if (
          !$pos.nodeAfter ||
          !["list_item", "checkbox_item"].includes($pos.nodeAfter.type.name)
        ) {
          return false;
        }

        const { tr } = state;
        const newPos = pos + li.nodeSize + $pos.nodeAfter.nodeSize;

        dispatch(
          tr
            .insert(newPos, li)
            .setSelection(TextSelection.near(tr.doc.resolve(newPos)))
            .delete(pos, pos + li.nodeSize)
        );
        return true;
      },
    };
  }

  toMarkdown(state: MarkdownSerializerState, node: ProsemirrorNode) {
    state.renderContent(node);
  }

  parseMarkdown() {
    return { block: "list_item" };
  }
}
