import { useLayoutEffect, useRef, useEffect, useState } from "react";
import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { baseKeymap } from "prosemirror-commands";
import { keymap } from "prosemirror-keymap";
import { ySyncPlugin } from "y-prosemirror";
import * as Y from "yjs";
import * as base64 from "base64-js";
import { Replicache } from "replicache";
import { produce } from "immer";

import { schema } from "./schema";
import { TextBlockKeymap } from "./keymap";
import { inputrules } from "./inputRules";
import { highlightSelectionPlugin } from "./plugins";
import { autolink } from "./autolink-plugin";
import { useEditorStates } from "src/state/useEditorState";
import {
  useEntity,
  useReplicache,
  type ReplicacheMutators,
} from "src/replicache";
import { useHandlePaste } from "./useHandlePaste";
import { BlockProps } from "../Block";
import { useEntitySetContext } from "components/EntitySetProvider";

export function useMountProsemirror({ props }: { props: BlockProps }) {
  let { entityID, parent } = props;
  let rep = useReplicache();
  let mountRef = useRef<HTMLPreElement | null>(null);
  const repRef = useRef<Replicache<ReplicacheMutators> | null>(null);
  let value = useYJSValue(entityID);
  let entity_set = useEntitySetContext();
  let alignment =
    useEntity(entityID, "block/text-alignment")?.data.value || "left";
  let propsRef = useRef({ ...props, entity_set, alignment });
  let handlePaste = useHandlePaste(entityID, propsRef);

  const actionTimeout = useRef<number | null>(null);

  propsRef.current = { ...props, entity_set, alignment };
  repRef.current = rep.rep;

  useLayoutEffect(() => {
    if (!mountRef.current) return;

    const km = TextBlockKeymap(propsRef, repRef, rep.undoManager);
    const editor = EditorState.create({
      schema: schema,
      plugins: [
        ySyncPlugin(value),
        keymap(km),
        inputrules(propsRef, repRef),
        keymap(baseKeymap),
        highlightSelectionPlugin,
        autolink({
          type: schema.marks.link,
          shouldAutoLink: () => true,
          defaultProtocol: "https",
        }),
      ],
    });

    const view = new EditorView(
      { mount: mountRef.current },
      {
        state: editor,
        handlePaste,
        handleClickOn: (_view, _pos, node, _nodePos, _event, direct) => {
          if (!direct) return;
          if (node.nodeSize - 2 <= _pos) return;
          let mark =
            node
              .nodeAt(_pos - 1)
              ?.marks.find((f) => f.type === schema.marks.link) ||
            node
              .nodeAt(Math.max(_pos - 2, 0))
              ?.marks.find((f) => f.type === schema.marks.link);
          if (mark) {
            window.open(mark.attrs.href, "_blank");
          }
        },
        dispatchTransaction,
      },
    );

    const unsubscribe = useEditorStates.subscribe((s) => {
      let editorState = s.editorStates[entityID];
      if (editorState?.initial) return;
      if (editorState?.editor)
        editorState.view?.updateState(editorState.editor);
    });

    let editorState = useEditorStates.getState().editorStates[entityID];
    if (editorState?.editor && !editorState.initial)
      editorState.view?.updateState(editorState.editor);

    return () => {
      unsubscribe();
      view.destroy();
      useEditorStates.setState((s) => ({
        ...s,
        editorStates: {
          ...s.editorStates,
          [entityID]: undefined,
        },
      }));
    };

    function dispatchTransaction(this: EditorView, tr: any) {
      useEditorStates.setState((s) => {
        let oldEditorState = this.state;
        let newState = this.state.apply(tr);
        let addToHistory = tr.getMeta("addToHistory");
        let isBulkOp = tr.getMeta("bulkOp");
        let docHasChanges = tr.steps.length !== 0 || tr.docChanged;

        // Handle undo/redo history with timeout-based grouping
        if (addToHistory !== false && docHasChanges) {
          if (actionTimeout.current) window.clearTimeout(actionTimeout.current);
          else if (!isBulkOp) rep.undoManager.startGroup();

          if (!isBulkOp) {
            actionTimeout.current = window.setTimeout(() => {
              rep.undoManager.endGroup();
              actionTimeout.current = null;
            }, 200);
          }

          let setState = (s: EditorState) => () =>
            useEditorStates.setState(
              produce((draft) => {
                let view = draft.editorStates[entityID]?.view;
                if (!view?.hasFocus() && !isBulkOp) view?.focus();
                draft.editorStates[entityID]!.editor = s;
              }),
            );

          rep.undoManager.add({
            redo: setState(newState),
            undo: setState(oldEditorState),
          });
        }

        return {
          editorStates: {
            ...s.editorStates,
            [entityID]: {
              editor: newState,
              view: this as unknown as EditorView,
              initial: false,
              keymap: km,
            },
          },
        };
      });
    }
  }, [entityID, parent, value, handlePaste, rep]);
  return { mountRef, actionTimeout };
}

function useYJSValue(entityID: string) {
  const [ydoc] = useState(new Y.Doc());
  const docStateFromReplicache = useEntity(entityID, "block/text");
  let rep = useReplicache();
  const [yText] = useState(ydoc.getXmlFragment("prosemirror"));

  if (docStateFromReplicache) {
    const update = base64.toByteArray(docStateFromReplicache.data.value);
    Y.applyUpdate(ydoc, update);
  }

  useEffect(() => {
    if (!rep.rep) return;
    let timeout = null as null | number;
    const updateReplicache = async () => {
      const update = Y.encodeStateAsUpdate(ydoc);
      await rep.rep?.mutate.assertFact({
        //These undos are handled above in the Prosemirror context
        ignoreUndo: true,
        entity: entityID,
        attribute: "block/text",
        data: {
          value: base64.fromByteArray(update),
          type: "text",
        },
      });
    };
    const f = async (events: Y.YEvent<any>[], transaction: Y.Transaction) => {
      if (!transaction.origin) return;
      if (timeout) clearTimeout(timeout);
      timeout = window.setTimeout(async () => {
        updateReplicache();
      }, 300);
    };

    yText.observeDeep(f);
    return () => {
      yText.unobserveDeep(f);
    };
  }, [yText, entityID, rep, ydoc]);
  return yText;
}
