/*
This file is part of the Notesnook project (https://notesnook.com/)

Copyright (C) 2023 Streetwriters (Private) Limited

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

import React, {
  useEffect,
  useState,
  useRef,
  PropsWithChildren,
  Suspense,
  useLayoutEffect,
  useCallback
} from "react";
import { Box, Button, Flex, Progress, Text } from "@theme-ui/components";
import Properties from "../properties";
import {
  useEditorStore,
  SaveState,
  DefaultEditorSession,
  DeletedEditorSession,
  NewEditorSession,
  ReadonlyEditorSession,
  EditorSession,
  DocumentPreview,
  LockedEditorSession
} from "../../stores/editor-store";
import {
  useStore as useAppStore,
  store as appstore
} from "../../stores/app-store";
import { useStore as useUserStore } from "../../stores/user-store";
import { useStore as useSearchStore } from "../../stores/search-store";
import { AppEventManager, AppEvents } from "../../common/app-events";
import { FlexScrollContainer } from "../scroll-container";
import Tiptap, { OnChangeHandler } from "./tiptap";
import Header from "./header";
import { Attachment } from "../icons";
import { AttachmentProgress, insertAttachments } from "./picker";
import { AttachFilesDialog } from "../../dialogs/attach-files-dialog";
import { useEditorManager } from "./manager";
import {
  saveAttachment,
  downloadAttachment,
  previewImageAttachment
} from "../../common/attachments";
import { EVENTS } from "@notesnook/core";
import { db } from "../../common/db";
import Titlebox, { resizeTextarea } from "./title-box";
import Config from "../../utils/config";
import { ScopedThemeProvider } from "../theme-provider";
import { showToast } from "../../utils/toast";
import { Item, MaybeDeletedItem, isDeleted } from "@notesnook/core";
import { debounce, debounceWithId } from "@notesnook/common";
import { Freeze } from "react-freeze";
import { UnlockView } from "../unlock";
import DiffViewer from "../diff-viewer";
import TableOfContents from "./table-of-contents";
import { scrollIntoViewById } from "@notesnook/editor";
import { IEditor } from "./types";
import { EditorActionBar } from "./action-bar";
import { logger } from "../../utils/logger";
import { NoteLinkingDialog } from "../../dialogs/note-linking-dialog";
import { strings } from "@notesnook/intl";
import { onPageVisibilityChanged } from "../../utils/page-visibility";
import { Pane, SplitPane } from "../split-pane";
import { TITLE_BAR_HEIGHT } from "../title-bar";
import { isMobile } from "../../hooks/use-mobile";
import { ConfirmDialog } from "../../dialogs/confirm";

const PDFPreview = React.lazy(() => import("../pdf-preview"));

const autoSaveToast = { show: true, hide: () => {} };

export async function saveContent(
  noteId: string,
  ignoreEdit: boolean,
  content: string
) {
  logger.debug("saving content", {
    noteId,
    ignoreEdit,
    length: content.length
  });
  await Promise.race([
    useEditorStore.getState().saveSessionContent(noteId, ignoreEdit, {
      type: "tiptap",
      data: content
    }),
    new Promise((_, reject) =>
      setTimeout(
        () => reject(new Error(strings.savingNoteTakingTooLong())),
        30 * 1000
      )
    )
  ]).catch((e) => {
    const { hide } = showToast(
      "error",
      (e as Error).message,
      [
        {
          text: strings.dismiss(),
          onClick: () => hide()
        }
      ],
      0
    );
  });
}
const deferredSave = debounceWithId(saveContent, 100);

export default function TabsView() {
  const tabs = useEditorStore((store) => store.tabs);
  const documentPreview = useEditorStore((store) => store.documentPreview);
  const activeTabId = useEditorStore((store) => store.activeTabId);
  const activeSession = useEditorStore((store) => store.getActiveSession());
  const arePropertiesVisible = useEditorStore(
    (store) => store.arePropertiesVisible
  );
  const isTOCVisible = useEditorStore((store) => store.isTOCVisible);
  const [dropRef, overlayRef] = useDragOverlay();

  return (
    <>
      <ScopedThemeProvider
        scope="editor"
        ref={dropRef}
        sx={{
          bg: "background",
          flex: 1,
          overflow: "hidden",
          display: "flex",
          flexDirection: "column"
        }}
      >
        <Flex
          className="editor-action-bar"
          sx={{
            zIndex: 2,
            height: TITLE_BAR_HEIGHT,
            bg: "background-secondary"
            // borderBottom: "1px solid var(--border)"
          }}
        >
          <EditorActionBar />
        </Flex>
        <SplitPane
          style={{
            position: "relative"
          }}
          direction="vertical"
          autoSaveId={"editor-panels"}
        >
          <Pane id="editor-panel" className="editor-pane">
            {tabs.map((tab) => {
              const session = useEditorStore
                .getState()
                .getSession(tab.sessionId);
              if (!session) return null;
              return (
                <Freeze key={session.id} freeze={tab.id !== activeTabId}>
                  {session.type === "locked" ? (
                    <UnlockNoteView session={session} />
                  ) : session.type === "conflicted" ||
                    session.type === "diff" ? (
                    <DiffViewer session={session} />
                  ) : (
                    <MemoizedEditorView session={session} />
                  )}
                </Freeze>
              );
            })}
          </Pane>

          {documentPreview ? (
            <Pane id="pdf-preview-panel" initialSize={435} minSize={435}>
              <ScopedThemeProvider
                scope="editorSidebar"
                id="editorSidebar"
                sx={{
                  display: "flex",
                  flexDirection: "column",
                  overflow: "hidden",
                  borderLeft: "1px solid var(--border)",
                  height: "100%",
                  bg: "background"
                }}
              >
                {documentPreview.url ? (
                  <Suspense
                    fallback={
                      <DownloadAttachmentProgress hash={documentPreview.hash} />
                    }
                  >
                    <PDFPreview
                      fileUrl={documentPreview.url}
                      hash={documentPreview.hash}
                      onClose={() =>
                        useEditorStore.setState({
                          documentPreview: undefined
                        })
                      }
                    />
                  </Suspense>
                ) : (
                  <DownloadAttachmentProgress hash={documentPreview.hash} />
                )}
              </ScopedThemeProvider>
            </Pane>
          ) : null}

          {isTOCVisible && activeSession ? (
            <Pane id="table-of-contents-pane" initialSize={300} minSize={300}>
              <TableOfContents sessionId={activeSession.id} />
            </Pane>
          ) : null}
          {arePropertiesVisible &&
            activeSession &&
            activeSession.type !== "new" &&
            activeSession.type !== "locked" && (
              <Pane id="properties-pane" initialSize={250} minSize={250}>
                <Properties sessionId={activeSession.id} />
              </Pane>
            )}
        </SplitPane>
        <DropZone overlayRef={overlayRef} />
      </ScopedThemeProvider>
    </>
  );
}

const MemoizedEditorView = React.memo(EditorView, (prev, next) => {
  const baseConditions =
    prev.session.id === next.session.id &&
    prev.session.type === next.session.type &&
    prev.session.needsHydration === next.session.needsHydration &&
    prev.session.activeBlockId === next.session.activeBlockId &&
    prev.session.activeSearchResultId === next.session.activeSearchResultId &&
    prev.session.nonce === next.session.nonce;

  if (
    "attachmentsLength" in prev.session &&
    "attachmentsLength" in next.session
  ) {
    return (
      baseConditions &&
      prev.session.attachmentsLength === next.session.attachmentsLength
    );
  }

  if ("note" in prev.session && "note" in next.session) {
    return (
      baseConditions &&
      prev.session.note.spellcheck === next.session.note.spellcheck
    );
  }

  return baseConditions;
});
function EditorView({
  session
}: {
  session:
    | DefaultEditorSession
    | NewEditorSession
    | ReadonlyEditorSession
    | DeletedEditorSession;
}) {
  const lastChangedTime = useRef<number>(0);
  const root = useRef<HTMLDivElement>(null);

  const isFocusMode = useAppStore((store) => store.isFocusMode);
  const editor = useEditorManager((store) => store.editors[session.id]?.editor);

  useEffect(() => {
    const event = db.eventManager.subscribe(
      EVENTS.syncItemMerged,
      async (item?: MaybeDeletedItem<Item>) => {
        if (
          session.type === "new" ||
          !editor ||
          !session.note ||
          !item ||
          isDeleted(item) ||
          (item.type !== "tiptap" && item.type !== "note") ||
          !appstore.get().isRealtimeSyncEnabled
        ) {
          console.log("ignoring real time sync");
          return;
        }

        const isContent =
          item.type === "tiptap" && item.noteId === session.note.id;
        const isNote = item.type === "note" && item.id === session.note.id;
        if (isContent && lastChangedTime.current < item.dateModified) {
          if (!item.locked) return editor.updateContent(item.data);

          const result = await db.vault
            .decryptContent(item)
            .catch(() => db.eventManager.publish(EVENTS.vaultLocked));
          if (!result) return;
          editor.updateContent(result.data);
        } else if (isNote && session.note.title !== item.title) {
          AppEventManager.publish(AppEvents.changeNoteTitle, {
            sessionId: session.id,
            title: item.title,
            preventSave: true
          });
        }
      }
    );
    return () => {
      event.unsubscribe();
    };
  }, [editor, session]);

  useLayoutEffect(() => {
    editor?.focus();

    const unsub = useSearchStore.subscribe(
      (store) => store.isSearching,
      (value) => {
        if (!value) element?.classList.remove("searching");
      }
    );

    const element = root.current;
    element?.classList.add("active", "searching");
    return () => {
      element?.classList.remove("active", "searching");
      unsub();
    };
  }, [editor]);

  const getContent = useCallback(() => {
    return session.content?.data;
  }, [session.content?.data]);

  if (session.needsHydration) return null;

  return (
    <Flex
      ref={root}
      id="editorContainer"
      sx={{
        position: "relative",
        alignSelf: "stretch",
        overflow: "hidden",
        flex: 1,
        flexDirection: "column",
        background: "background"
      }}
    >
      <div className="dialogContainer" />
      <Editor
        id={session.id}
        nonce={1}
        content={getContent}
        session={session}
        onPreviewDocument={(preview) =>
          useEditorStore.setState({ documentPreview: preview })
        }
        onContentChange={() => (lastChangedTime.current = Date.now())}
        onSelectionChange={() => root.current?.classList.remove("searching")}
        onSave={(content, ignoreEdit) => {
          const currentSession = useEditorStore
            .getState()
            .getSession(session.id);
          const noteId =
            currentSession && "note" in currentSession
              ? currentSession.note.id
              : null;
          const sessions = noteId
            ? useEditorStore.getState().getSessionsForNote(noteId)
            : [currentSession];

          const currentSessionId = session.id;
          const data = content();
          for (const session of sessions) {
            if (
              session?.type !== "default" &&
              session?.type !== "readonly" &&
              session?.type !== "new"
            )
              continue;
            if (!session.content) session.content = { type: "tiptap", data };
            else session.content.data = data;

            // update content in other tabs
            if (session.id !== currentSessionId) {
              const editor = useEditorManager.getState().getEditor(session.id);
              editor?.editor?.updateContent(data);
            }
          }

          logger.debug("scheduling save", {
            id: session.id,
            length: data.length
          });
          deferredSave(session.id, session.id, ignoreEdit, data);
        }}
        options={{
          readonly: session?.type === "readonly" || session?.type === "deleted",
          focusMode: isFocusMode,
          spellcheck:
            "note" in session ? session.note.spellcheck : session.type === "new"
        }}
      />
    </Flex>
  );
}

type DownloadAttachmentProgressProps = {
  hash: string;
};
function DownloadAttachmentProgress(props: DownloadAttachmentProgressProps) {
  const { hash } = props;

  const [progress, setProgress] = useState(0);
  useEffect(() => {
    const event = AppEventManager.subscribe(
      AppEvents.UPDATE_ATTACHMENT_PROGRESS,
      (progress: AttachmentProgress) => {
        if (progress.hash === hash) {
          setProgress(Math.round((progress.loaded / progress.total) * 100));
        }
      }
    );

    return () => {
      event.unsubscribe();
    };
  }, [hash]);

  return (
    <Flex
      sx={{
        flex: 1,
        alignItems: "center",
        justifyContent: "center",
        flexDirection: "column"
      }}
    >
      <Text variant="title">
        {strings.downloadingAttachments()} ({progress}%)
      </Text>
      <Progress
        value={progress}
        max={100}
        sx={{ width: ["90%", "35%"], mt: 1 }}
      />
      <Button
        variant="secondary"
        mt={2}
        onClick={() => {
          const note = useEditorStore.getState().getActiveNote();
          useEditorStore.setState({ documentPreview: undefined });
          if (note) db.fs().cancel(note.id).catch(console.error);
        }}
      >
        {strings.cancel()}
      </Button>
    </Flex>
  );
}

type EditorOptions = {
  headless?: boolean;
  readonly?: boolean;
  focusMode?: boolean;
  spellcheck?: boolean;
  onRequestFocus?: () => void;
};
type EditorProps = {
  id: string;
  session: EditorSession;
  content: () => string | undefined;
  nonce?: number;
  options?: EditorOptions;
  onContentChange?: () => void;
  onSelectionChange?: () => void;
  onSave?: OnChangeHandler;
  onPreviewDocument?: (preview: DocumentPreview) => void;
};
export function Editor(props: EditorProps) {
  const {
    id,
    session,
    content,
    onSave,
    onSelectionChange,
    nonce,
    options,
    onContentChange,
    onPreviewDocument
  } = props;
  const { readonly, headless, spellcheck } = options || {
    headless: false,
    readonly: false,
    focusMode: false,
    spellcheck: false
  };
  const saveSessionContentIfNotSaved = useEditorStore(
    (store) => store.saveSessionContentIfNotSaved
  );
  const setEditorSaveState = useEditorStore((store) => store.setSaveState);
  useScrollToBlock(session);
  useScrollToSearchResult(session);

  useEffect(() => {
    if (!autoSaveToast.show) {
      autoSaveToast.hide();
    }

    const event = AppEventManager.subscribe(
      AppEvents.UPDATE_ATTACHMENT_PROGRESS,
      ({ hash, loaded, total }: AttachmentProgress) => {
        const editor = useEditorManager.getState().getEditor(id)?.editor;
        editor?.sendAttachmentProgress(
          hash,
          Math.round((loaded / total) * 100)
        );
      }
    );

    return () => {
      event.unsubscribe();
    };
  }, [id]);

  useEffect(() => {
    const unsub = onPageVisibilityChanged((_, hidden) => {
      if (hidden) {
        saveSessionContentIfNotSaved(id);
      }
    });
    return () => unsub();
  }, []);

  useEffect(() => {
    return () => {
      const editor = useEditorManager.getState().getEditor(id)?.editor;
      const selection = editor?.getSelection();
      if (selection) Config.set(`${id}:selection`, selection);
    };
  }, [id]);

  return (
    <EditorChrome {...props}>
      <Tiptap
        id={id}
        isHydrating={!!session.needsHydration}
        nonce={nonce}
        readonly={readonly}
        spellcheck={spellcheck}
        content={content}
        downloadOptions={{
          corsHost: Config.get("corsProxy", "https://cors.notesnook.com")
        }}
        onLoad={(editor) => {
          editor = editor || useEditorManager.getState().getEditor(id)?.editor;
          if (editor) restoreSelection(editor, id);
          restoreScrollPosition(session);
        }}
        onSelectionChange={({ from, to }) => {
          Config.set(`${id}:selection`, { from, to });
          onSelectionChange?.();
        }}
        onContentChange={onContentChange}
        onChange={onSave}
        onDownloadAttachment={(attachment) => saveAttachment(attachment.hash)}
        onPreviewAttachment={async (data) => {
          try {
            const { hash, type } = data;
            const attachment = await db.attachments.attachment(hash);
            if (!attachment)
              throw new Error("No attachment found with hash: " + hash);
            if (attachment.mimeType.startsWith("image/") || type === "image") {
              await previewImageAttachment(attachment);
            } else if (
              onPreviewDocument &&
              type === "file" &&
              attachment.mimeType.startsWith("application/pdf")
            ) {
              onPreviewDocument({ hash });
              const blob = await downloadAttachment(hash, "blob", id);
              if (!blob) {
                useEditorStore.setState({ documentPreview: undefined });
                return;
              }
              onPreviewDocument({ url: URL.createObjectURL(blob), hash });
            } else {
              logger.info("Attachment cannot be previewed", {
                hash,
                type,
                mimeType: attachment?.mimeType
              });
              showToast("error", strings.attachmentPreviewFailed());
            }
          } catch (e) {
            logger.error(e, "Failed to preview attachment", { data });
            showToast(
              "error",
              (e as Error).message || strings.attachmentPreviewFailed()
            );
          }
        }}
        onInsertAttachment={async (type) => {
          if (!useUserStore.getState().isLoggedIn) {
            ConfirmDialog.show({
              title: strings.notLoggedIn(),
              message: strings.loginToUploadAttachments(),
              positiveButtonText: strings.okay()
            });
            return;
          }

          const mime = type === "file" ? "*/*" : "image/*";
          await insertAttachments(mime, (attachments) => {
            const editor = useEditorManager.getState().getEditor(id)?.editor;
            if (!editor) return;
            editor?.attachFiles(...attachments);
          });
        }}
        onGetAttachmentData={async (attachment) => {
          logger.debug("Getting attachment data", {
            hash: attachment.hash,
            type: attachment.type
          });

          const result = await downloadAttachment(
            attachment.hash,
            attachment.type === "web-clip" ? "text" : "base64",
            id?.toString()
          );

          if (!result)
            logger.debug("Got no result after downloading attachment", {
              hash: attachment.hash,
              type: attachment.type
            });

          return result;
        }}
        onAttachFiles={async (files) => {
          await AttachFilesDialog.show({
            files,
            onDone: (attachments) => {
              const editor = useEditorManager.getState().getEditor(id)?.editor;
              if (!editor) return;
              editor?.attachFiles(...attachments);
            }
          });
        }}
        onInsertInternalLink={async (attributes) => {
          const link = await NoteLinkingDialog.show({ attributes });
          return link || undefined;
        }}
        onAutoSaveDisabled={() => {
          setEditorSaveState(id, SaveState.NotSaved);
          if (autoSaveToast.show === false) return;
          const { hide } = showToast(
            "error",
            "Auto-save is disabled for large notes. Press Ctrl + S to save.",
            [
              {
                text: "Dismiss",
                onClick: () => {
                  hide();
                }
              }
            ],
            Infinity
          );
          autoSaveToast.show = false;
          autoSaveToast.hide = hide;
        }}
      >
        {headless ? null : (
          <>
            <Titlebox id={id} readonly={readonly || false} />
            <Header id={id} readonly={readonly || false} />
          </>
        )}
      </Tiptap>
    </EditorChrome>
  );
}

function EditorChrome(props: PropsWithChildren<EditorProps>) {
  const { id, options, children } = props;
  const { focusMode, headless, onRequestFocus } = options || {
    headless: false,
    readonly: false,
    focusMode: false
  };
  const editorMargins = useEditorStore((store) => store.editorMargins);
  const editorContainerRef = useRef<HTMLElement>(null);
  const editorScrollRef = useRef<HTMLElement>(null);

  useEffect(() => {
    if (!editorScrollRef.current) return;
    function onResize(
      entries: ResizeObserverEntry[],
      _observer: ResizeObserver
    ) {
      const editor = editorContainerRef.current?.querySelector(
        ".ProseMirror"
      ) as HTMLElement | undefined;
      const parent = editorScrollRef.current?.getBoundingClientRect();
      const child = editorContainerRef.current?.getBoundingClientRect();
      if (!parent || !child || !editor || entries.length <= 0) return;

      const CONTAINER_MARGIN = isMobile() ? 10 : 30;
      const negativeSpace = Math.abs(
        parent.left - child.left - CONTAINER_MARGIN
      );

      requestAnimationFrame(() => {
        if (!isMobile()) {
          editor.style.marginLeft = `-${negativeSpace}px`;
          editor.style.marginRight = `-${negativeSpace}px`;

          editor.style.paddingLeft = `${negativeSpace}px`;
          editor.style.paddingRight = `${negativeSpace}px`;
        }
      });

      const editorTitle = document.querySelector(`#editor-title-${id}`);
      if (editorTitle instanceof HTMLTextAreaElement) {
        resizeTextarea(editorTitle);
      }
    }
    const observer = new ResizeObserver(onResize);
    observer.observe(editorScrollRef.current);
    return () => {
      observer.disconnect();
    };
  }, []);

  if (headless) return <>{children}</>;

  return (
    <>
      <FlexScrollContainer
        id={`editorScroll_${id}`}
        scrollRef={editorScrollRef}
        style={{
          display: "flex",
          flexDirection: "column",
          flex: 1
        }}
        suppressScrollX
        onScroll={debounce((e) => {
          if (e.target instanceof HTMLElement) {
            const scrollTop = e.target.scrollTop;
            Config.set(`${id}:scroll-position`, scrollTop);
          }
        }, 500)}
      >
        <Flex
          ref={editorContainerRef}
          variant="columnFill"
          className="editor"
          sx={{
            alignSelf: ["stretch", focusMode ? "center" : "stretch", "center"],
            maxWidth: editorMargins ? "min(100%, 850px)" : "auto",
            width: "100%"
          }}
          pl={[2, 6]}
          pr={[2, 6]}
          onClick={onRequestFocus}
        >
          {children}
        </Flex>
      </FlexScrollContainer>
    </>
  );
}

type DropZoneProps = {
  overlayRef: React.MutableRefObject<HTMLElement | undefined>;
};
function DropZone(props: DropZoneProps) {
  const { overlayRef } = props;

  return (
    <Box
      ref={overlayRef}
      id="drag-overlay"
      sx={{
        position: "absolute",
        width: "100%",
        height: "100%",
        bg: "backdrop",
        zIndex: 3,
        alignItems: "center",
        justifyContent: "center",
        display: "none"
      }}
      onDrop={async (e) => {
        try {
          const { activeEditorId } = useEditorManager.getState();
          if (!e.dataTransfer.files?.length || !activeEditorId) return;

          e.preventDefault();
          await AttachFilesDialog.show({
            files: Array.from(e.dataTransfer.files),
            onDone: (attachments) => {
              const editor = useEditorManager
                .getState()
                .getEditor(activeEditorId)?.editor;
              if (!editor) return;
              editor?.attachFiles(...attachments);
            }
          });
        } catch (e) {
          logger.error(e as Error, "Failed to attach file from drag and drop");
          showToast("error", strings.failedToAttachFile());
        }
      }}
    >
      <Flex
        sx={{
          border: "2px dashed var(--border)",
          borderRadius: "default",
          p: 70,
          flexDirection: "column",
          pointerEvents: "none"
        }}
      >
        <Attachment size={72} />
        <Text variant={"heading"} sx={{ color: "icon", mt: 2 }}>
          {strings.dropFilesToAttach()}
        </Text>
      </Flex>
    </Box>
  );
}

function useDragOverlay() {
  const dropElementRef = useRef<HTMLDivElement>(null);
  const overlayRef = useRef<HTMLElement>();

  useEffect(() => {
    const dropElement = dropElementRef.current;
    const overlay = overlayRef.current;

    if (!dropElement || !overlay) return;

    function showOverlay(e: DragEvent) {
      if (!overlay || !isFile(e)) return;

      overlay.style.display = "flex";
    }

    function hideOverlay() {
      if (!overlay) return;
      overlay.style.display = "none";
    }

    function allowDrag(e: DragEvent) {
      if (!e.dataTransfer || !isFile(e)) return;

      e.dataTransfer.dropEffect = "copy";
      e.preventDefault();
    }

    function handleKeyDown(e: KeyboardEvent) {
      if (e.key === "Escape") {
        hideOverlay();
      }
    }

    dropElement.addEventListener("dragenter", showOverlay);
    overlay.addEventListener("drop", hideOverlay);
    overlay.addEventListener("dragenter", allowDrag);
    overlay.addEventListener("dragover", allowDrag);
    overlay.addEventListener("dragleave", hideOverlay);
    overlay.addEventListener("click", hideOverlay);
    document.addEventListener("keydown", handleKeyDown);
    return () => {
      dropElement.removeEventListener("dragenter", showOverlay);
      overlay.removeEventListener("drop", hideOverlay);
      overlay.removeEventListener("dragenter", allowDrag);
      overlay.removeEventListener("dragover", allowDrag);
      overlay.removeEventListener("dragleave", hideOverlay);
      overlay.removeEventListener("click", hideOverlay);
      document.removeEventListener("keydown", handleKeyDown);
    };
  }, []);

  return [dropElementRef, overlayRef] as const;
}

function useScrollToBlock(session: EditorSession) {
  const blockId = useEditorStore(
    (store) => store.getSession(session.id)?.activeBlockId
  );
  useEffect(() => {
    if (!blockId) return;
    scrollIntoViewById(blockId);
    useEditorStore.getState().updateSession(session.id, [session.type], {
      activeBlockId: undefined
    });
  }, [session.id, session.type, blockId]);
}

function useScrollToSearchResult(session: EditorSession) {
  const id = useEditorStore(
    (store) => store.getSession(session.id)?.activeSearchResultId
  );

  useEffect(() => {
    if (id === undefined) return;
    const scrollContainer = document.getElementById(
      `editorScroll_${session.id}`
    );
    scrollContainer?.closest(".active")?.classList.add("searching");
    const element = scrollContainer?.querySelector(`nn-search-result#${id}`);
    setTimeout(
      () =>
        element?.scrollIntoView({
          block: "center",
          behavior: "instant"
        }),
      100
    );
    useEditorStore.getState().updateSession(session.id, [session.type], {
      activeSearchResultId: undefined
    });
  }, [session.id, session.type, id]);
}

function isFile(e: DragEvent) {
  return (
    e.dataTransfer &&
    (e.dataTransfer.files?.length > 0 ||
      e.dataTransfer.types?.some((a) => a === "Files"))
  );
}

function restoreScrollPosition(session: EditorSession) {
  if (session?.activeBlockId) return scrollIntoViewById(session.activeBlockId);

  const scrollContainer = document.getElementById(`editorScroll_${session.id}`);
  const scrollPosition = Config.get(`${session.id}:scroll-position`, 0);
  if (scrollContainer) {
    if (scrollContainer.scrollHeight < scrollPosition) {
      const observer = new ResizeObserver(() => {
        if (scrollContainer.scrollHeight >= scrollPosition) {
          observer.disconnect();
          requestAnimationFrame(
            () => (scrollContainer.scrollTop = scrollPosition)
          );
          clearTimeout(timeout);
        }
      });
      observer.observe(scrollContainer);
      // eslint-disable-next-line no-var
      var timeout = setTimeout(() => {
        observer.disconnect();
      }, 30 * 1000);
    } else
      requestAnimationFrame(() => (scrollContainer.scrollTop = scrollPosition));
  }
}

function restoreSelection(editor: IEditor, id: string) {
  setTimeout(() => {
    editor.focus({
      position: Config.get(`${id}:selection`)
    });
  });
}

type UnlockNoteViewProps = { session: LockedEditorSession };
function UnlockNoteView(props: UnlockNoteViewProps) {
  const { session } = props;
  const root = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    const element = root.current;
    element?.classList.add("active");
    return () => {
      element?.classList.remove("active");
    };
  }, []);

  return (
    <div
      style={{
        display: "flex",
        overflow: "hidden",
        alignItems: "center",
        flex: 1
      }}
      ref={root}
    >
      <UnlockView
        buttonTitle={strings.openNote()}
        subtitle={strings.enterPasswordToUnlockNote()}
        title={session.note.title}
        unlock={async (password) => {
          const note = await db.vault.open(session.note.id, password);
          if (!note || !note.content)
            throw new Error("note with this id does not exist.");

          const tags = await db.notes.tags(note.id);
          useEditorStore.getState().addSession({
            type: session.note.readonly ? "readonly" : "default",
            locked: true,
            id: session.id,
            note: session.note,
            saveState: SaveState.Saved,
            sessionId: `${Date.now()}`,
            tags,
            tabId: session.tabId,
            content: note.content
          });
        }}
      />
    </div>
  );
}
