/*
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/>.
*/

/* eslint-disable no-case-declarations */
/* eslint-disable @typescript-eslint/no-var-requires */
import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common";
import { ItemReference, parseInternalLink } from "@notesnook/core";
import type { Attachment } from "@notesnook/editor";
import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { getDefaultPresets } from "@notesnook/editor/dist/cjs/toolbar/tool-definitions";
import { strings } from "@notesnook/intl";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useCallback, useEffect, useRef } from "react";
import * as ScopedStorage from "react-native-scoped-storage";
import {
  BackHandler,
  Keyboard,
  KeyboardEventListener,
  NativeEventSubscription,
  Platform,
  useWindowDimensions
} from "react-native";
import { WebViewMessageEvent } from "react-native-webview";
import { DatabaseLogger, db } from "../../../common/database";
import downloadAttachment from "../../../common/filesystem/download-attachment";
import { AuthMode } from "../../../components/auth/common";
import { Properties } from "../../../components/properties";
import EditorTabs from "../../../components/sheets/editor-tabs";
import { Issue } from "../../../components/sheets/github/issue";
import LinkNote from "../../../components/sheets/link-note";
import PaywallSheet from "../../../components/sheets/paywall";
import TableOfContents from "../../../components/sheets/toc";
import { DDS } from "../../../services/device-detection";
import {
  ToastManager,
  eSendEvent,
  eSubscribeEvent,
  eUnSubscribeEvent,
  presentSheet
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import SettingsService from "../../../services/settings";
import useNavigationStore from "../../../stores/use-navigation-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useTagStore } from "../../../stores/use-tag-store";
import { useUserStore } from "../../../stores/use-user-store";
import {
  eClearEditor,
  eCloseFullscreenEditor,
  eEditorTabFocused,
  eOnEnterEditor,
  eOnExitEditor,
  eOnLoadNote,
  eOpenFullscreenEditor,
  eOpenPublishNoteDialog,
  eUnlockWithBiometrics,
  eUnlockWithPassword
} from "../../../utils/events";
import { openLinkInBrowser } from "../../../utils/functions";
import { fluidTabsRef } from "../../../utils/global-refs";
import { sleep } from "../../../utils/time";
import AddReminder from "../../add-reminder";
import ManageTags from "../../manage-tags";
import RelationsList from "../../relations-list";
import { useDragState } from "../../settings/editor/state";
import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { editorState, openInternalLink } from "./utils";
import filesystem from "../../../common/filesystem";
import ReactNativeBlobUtil from "react-native-blob-util";
import { ShareComponent } from "../../../components/sheets/export-notes/share";

const publishNote = async () => {
  const user = useUserStore.getState().user;
  if (!user) {
    ToastManager.show({
      heading: strings.loginRequired(),
      context: "global",
      func: () => {
        Navigation.navigate("Auth", {
          mode: AuthMode.login
        });
      },
      actionText: "Login"
    });
    return;
  }

  if (!user?.isEmailConfirmed) {
    ToastManager.show({
      heading: strings.emailNotConfirmed(),
      context: "global"
    });
    return;
  }
  const noteId = useTabStore
    .getState()
    .getNoteIdForTab(useTabStore.getState().currentTab!);

  if (noteId) {
    const note = await db.notes?.note(noteId);
    const locked = note && (await db.vaults.itemExists(note));
    if (locked) {
      ToastManager.show({
        heading: strings.lockedNotesPublishFailed(),
        type: "error",
        context: "global"
      });
      return;
    }
    if (editorState().isFocused) {
      editorState().isFocused = true;
    }
    eSendEvent(eOpenPublishNoteDialog, note);
  }
};

const showActionsheet = async () => {
  const noteId = useTabStore
    .getState()
    .getNoteIdForTab(useTabStore.getState().currentTab!);
  if (noteId) {
    const note = await db.notes?.note(noteId);
    Properties.present(note, false);
  } else {
    ToastManager.show({
      heading: strings.noNoteProperties(),
      type: "success",
      context: "global"
    });
  }
};

type ContentMessage = { html: string; ignoreEdit: boolean };

export const useEditorEvents = (
  editor: useEditorType,
  { readonly: editorPropReadonly, noHeader, noToolbar }: Partial<EditorProps>
) => {
  const features = useAreFeaturesAvailable([
    "callout",
    "outlineList",
    "taskList",
    "importCsvToTable",
    "exportTableAsCsv"
  ]);

  const deviceMode = useSettingStore((state) => state.deviceMode);
  const fullscreen = useSettingStore((state) => state.fullscreen);
  const corsProxy = useSettingStore((state) => state.settings.corsProxy);
  const loading = useSettingStore((state) => state.isAppLoading);
  const [dateFormat, timeFormat, defaultLineHeight, dayFormat] =
    useSettingStore((state) => [
      state.dateFormat,
      state.timeFormat,
      state.settings.defaultLineHeight,
      state.dayFormat
    ]);
  const handleBack = useRef<NativeEventSubscription>(undefined);
  const loggedIn = useUserStore((state) => !!state.user);
  const { fontScale } = useWindowDimensions();

  const doubleSpacedLines = useSettingStore(
    (state) => state.settings?.doubleSpacedLines
  );
  const defaultFontSize = useSettingStore(
    (state) => state.settings.defaultFontSize
  );
  const defaultFontFamily = useSettingStore(
    (state) => state.settings.defaultFontFamily
  );
  const markdownShortcuts = useSettingStore(
    (state) => state.settings.markdownShortcuts
  );

  const tools = useDragState((state) => state.data);
  useEffect(() => {
    const handleKeyboardDidShow: KeyboardEventListener = () => {
      editor.commands.keyboardShown(true);
      editor.postMessage(NativeEvents.keyboardShown, undefined);
    };
    const handleKeyboardDidHide: KeyboardEventListener = () => {
      editor.commands.keyboardShown(false);
    };
    const subscriptions = [
      Keyboard.addListener("keyboardDidShow", handleKeyboardDidShow),
      Keyboard.addListener("keyboardDidHide", handleKeyboardDidHide)
    ];
    return () => {
      subscriptions.forEach((subscription) => subscription.remove());
    };
  }, [editor, editor.commands, editor.postMessage]);
  useEffect(() => {
    if (loading) return;
    if (typeof defaultFontFamily === "object") {
      SettingsService.set({
        defaultFontFamily: (defaultFontFamily as any).id
      });
    }

    editor.commands.setSettings({
      deviceMode: deviceMode || "mobile",
      fullscreen: fullscreen || false,
      premium: false,
      readonly: false,
      tools: tools || getDefaultPresets().default,
      noHeader: noHeader,
      noToolbar: noToolbar,
      doubleSpacedLines: doubleSpacedLines,
      corsProxy: corsProxy,
      fontSize:
        typeof defaultFontSize === "string"
          ? parseInt(defaultFontSize)
          : defaultFontSize,
      fontFamily: SettingsService.get().defaultFontFamily,
      dateFormat: db.settings?.getDateFormat(),
      timeFormat: db.settings?.getTimeFormat(),
      dayFormat: db.settings?.getDayFormat(),
      fontScale,
      markdownShortcuts,
      features,
      loggedIn,
      defaultLineHeight
    });
  }, [
    fullscreen,
    editor.loading,
    deviceMode,
    tools,
    editor.commands,
    doubleSpacedLines,
    noHeader,
    noToolbar,
    corsProxy,
    defaultFontSize,
    defaultFontFamily,
    dateFormat,
    timeFormat,
    dayFormat,
    loading,
    fontScale,
    markdownShortcuts,
    loggedIn,
    defaultLineHeight,
    features
  ]);

  const onBackPress = useCallback(async () => {
    const editorHandledBack = await editor.commands.handleBack();
    if (!editorHandledBack) {
      logger.info("editor handled back event", editorHandledBack);
      return;
    }
    editorState().currentlyEditing = false;
    // editor.reset(); Notes remain open.
    editor.commands?.blur(useTabStore.getState().currentTab!);
    setTimeout(async () => {
      if (deviceMode !== "mobile" && fullscreen) {
        if (fullscreen) {
          eSendEvent(eCloseFullscreenEditor);
        }
        return;
      }

      if (deviceMode === "mobile") {
        editorState().movedAway = true;
        fluidTabsRef.current?.goToPage("home");
      }

      setTimeout(() => {
        Navigation.queueRoutesForUpdate();
      }, 500);
    }, 1);
  }, [editor, deviceMode, fullscreen]);

  const onHardwareBackPress = useCallback(() => {
    if (fluidTabsRef.current?.page() === "editor") {
      if (
        useNavigationStore.getState().currentRoute === "ManageTags" ||
        useNavigationStore.getState().currentRoute === "LinkNotebooks" ||
        useNavigationStore.getState().currentRoute === "AddReminder"
      ) {
        Navigation.goBack();
      } else {
        onBackPress();
      }
      return true;
    }
  }, [onBackPress]);

  const onEnterEditor = useCallback(async () => {
    if (!DDS.isTab) {
      handleBack.current = BackHandler.addEventListener(
        "hardwareBackPress",
        onHardwareBackPress
      );
    }
  }, [onHardwareBackPress]);

  const onClearEditorSessionRequest = useCallback(
    async (value: string) => {
      if (editorState()?.isAwaitingResult) return;

      if (value === "removeHandler") {
        if (handleBack.current) {
          handleBack.current.remove();
        }
        return;
      }
      if (value === "addHandler") {
        if (handleBack.current) {
          handleBack.current.remove();
        }

        handleBack.current = BackHandler.addEventListener(
          "hardwareBackPress",
          onHardwareBackPress
        );
        return;
      }
      if (editorState().currentlyEditing) {
        await onBackPress();
      }
    },
    [onBackPress, onHardwareBackPress]
  );

  useEffect(() => {
    if (fullscreen && DDS.isTab) {
      handleBack.current = BackHandler.addEventListener(
        "hardwareBackPress",
        onHardwareBackPress
      );
    }

    return () => {
      if (handleBack.current && DDS.isTab) {
        handleBack.current.remove();
      }
    };
  }, [fullscreen, onHardwareBackPress]);

  useEffect(() => {
    const onExitEditor = () => {
      if (handleBack.current) {
        handleBack.current.remove();
      }
    };

    eSubscribeEvent(eOnEnterEditor, onEnterEditor);
    eSubscribeEvent(eOnExitEditor, onExitEditor);
    eSubscribeEvent(
      eClearEditor + editor.editorId,
      onClearEditorSessionRequest
    );
    return () => {
      eUnSubscribeEvent(eClearEditor, onClearEditorSessionRequest);
      eUnSubscribeEvent(eOnEnterEditor, onEnterEditor);
      eUnSubscribeEvent(eOnExitEditor, onExitEditor);
    };
  }, [editor.editorId, onClearEditorSessionRequest, onEnterEditor]);

  const onMessage = useCallback(
    async (event: WebViewMessageEvent) => {
      const data = event.nativeEvent.data;
      const editorMessage = JSON.parse(data) as EditorMessage<any>;

      if (editorMessage.hasTimeout && editorMessage.resolverId) {
        editor.postMessage(NativeEvents.resolve, {
          data: true,
          resolverId: editorMessage.resolverId
        });
      }

      if (editorMessage.type === EditorEvents.load) {
        DatabaseLogger.log("Editor is ready");
        editor.onLoad();
        return;
      }

      if (editorMessage.type === EditorEvents.back) {
        return onBackPress();
      }

      if (
        editorMessage.sessionId !== editor.sessionId.current &&
        editorMessage.type !== NativeEvents.status
      ) {
        return;
      }

      const noteId = useTabStore
        .getState()
        .getNoteIdForTab(editorMessage.tabId);

      switch (editorMessage.type) {
        case EditorEvents.content:
          DatabaseLogger.log("EditorEvents.content");
          editor.saveContent({
            type: editorMessage.type,
            content: editorMessage.value.html as string,
            noteId: noteId,
            tabId: editorMessage.tabId,
            ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit,
            pendingChanges: editorMessage.value?.pendingChanges
          });
          break;
        case EditorEvents.title:
          DatabaseLogger.log("EditorEvents.title");
          editor.saveContent({
            type: editorMessage.type,
            title: editorMessage.value?.title as string,
            noteId: noteId,
            tabId: editorMessage.tabId,
            ignoreEdit: false,
            pendingChanges: editorMessage.value?.pendingChanges
          });
          break;
        case EditorEvents.logger:
          logger.info("[EDITOR LOG]", editorMessage.value);
          break;
        case EditorEvents.dbLogger:
          if (editorMessage.value.error) {
            DatabaseLogger.error(
              editorMessage.value.error,
              editorMessage.value.error,
              {
                message: "[EDITOR_ERROR]" + editorMessage.value.message
              }
            );
          } else {
            DatabaseLogger.info("[EDITOR_LOG]" + editorMessage.value.message);
          }
          break;
        case EditorEvents.contentchange:
          editor.onContentChanged(editorMessage.noteId);
          break;
        case EditorEvents.selection:
          break;
        case EditorEvents.reminders:
          if (!noteId) {
            ToastManager.show({
              heading: strings.createNoteFirst(),
              type: "success"
            });
            return;
          }
          const note = await db.notes.note(noteId);
          if (!note) return;
          RelationsList.present({
            item: note as any,
            referenceType: "reminder",
            relationType: "from",
            title: strings.dataTypesPluralCamelCase.reminder(),
            onAdd: async () => {
              const reminderFeature =
                await isFeatureAvailable("activeReminders");
              if (!reminderFeature.isAllowed) {
                ToastManager.show({
                  type: "info",
                  message: reminderFeature.error,
                  actionText: strings.upgrade(),
                  func: () => {
                    PaywallSheet.present(reminderFeature);
                  }
                });
                return;
              }
              AddReminder.present(undefined, note);
            }
          });
          break;
        case EditorEvents.newtag:
          if (!noteId) {
            ToastManager.show({
              heading: strings.createNoteFirst(),
              type: "success"
            });
            return;
          }
          ManageTags.present([noteId]);
          break;
        case EditorEvents.tag:
          if (editorMessage.value) {
            if (!noteId) return;
            const note = await db.notes.note(noteId);
            if (!note) return;

            db.relations
              .unlink(editorMessage.value as ItemReference, note)
              .then(async () => {
                useTagStore.getState().refresh();
                useRelationStore.getState().update();
                await editor.commands.setTags(note);
                Navigation.queueRoutesForUpdate();
              });
          }
          break;
        case EditorEvents.filepicker:
          editorState().isAwaitingResult = true;
          const { pick } = require("./picker").default;
          pick({
            type: editorMessage.value,
            noteId: noteId,
            tabId: editorMessage.tabId
          });
          setTimeout(() => {
            editorState().isAwaitingResult = false;
          }, 1000);
          break;
        case EditorEvents.download: {
          const downloadAttachment =
            require("../../../common/filesystem/download-attachment").default;
          downloadAttachment((editorMessage.value as Attachment)?.hash, true);
          break;
        }
        case EditorEvents.getLinkData: {
          const url = (editorMessage.value as any)?.url as string;
          const link = parseInternalLink(url);
          if (!link) return;
          switch (link.type) {
            case "note":
            case "notebook":
            case "tag": {
              const table =
                link.type === "note"
                  ? "notes"
                  : link.type === "notebook"
                    ? "notebooks"
                    : "tags";
              const item = await db
                .sql()
                .selectFrom(table)
                .where("id", "=", link.id)
                .select("title")
                .executeTakeFirst();

              editor.postMessage(NativeEvents.resolve, {
                resolverId: editorMessage.resolverId,
                data: {
                  type: link.type,
                  title: item?.title || ""
                }
              });
              break;
            }
            case "color": {
              const color = await db
                .sql()
                .selectFrom("colors")
                .where("id", "=", link.id)
                .select(["title", "colorCode"])
                .executeTakeFirst();

              editor.postMessage(NativeEvents.resolve, {
                resolverId: editorMessage.resolverId,
                data: {
                  type: "color",
                  title: color?.title || "",
                  metadata: {
                    colorCode: color?.colorCode || ""
                  }
                }
              });
            }
          }

          break;
        }
        case EditorEvents.getAttachmentData: {
          const data = (editorMessage.value as any)?.attachment as Attachment;

          const attachment = await db.attachments.attachment(data.hash);

          if (!attachment) {
            editor.postMessage(NativeEvents.resolve, {
              resolverId: editorMessage.resolverId,
              data: undefined
            });
            break;
          }
          DatabaseLogger.log(
            `Getting attachment data: ${attachment.mimeType} ${attachment.hash} ${data.type}`
          );
          downloadAttachment(attachment.hash, true, {
            base64:
              data.type === "image" || attachment.mimeType?.startsWith("audio"),
            text: data.type === "web-clip",
            silent: true,
            groupId: editor.note.current?.id,
            cache: true
          } as any)
            .then((data: any) => {
              console.log(
                "Got attachment data:",
                !!data,
                editorMessage.resolverId
              );
              editor.postMessage(NativeEvents.resolve, {
                resolverId: editorMessage.resolverId,
                data
              });
            })
            .catch((e) => {
              DatabaseLogger.error(e);
              editor.postMessage(NativeEvents.resolve, {
                resolverId: editorMessage.resolverId,
                data: undefined
              });
            });

          break;
        }

        case EditorEvents.pro:
          if (editor.state.current?.isFocused) {
            editor.state.current.isFocused = true;
          }
          if (editorMessage.value.feature === "insertAttachment") {
            ToastManager.show({
              type: "info",
              message: strings.loginRequired()
            });
          } else {
            PaywallSheet.present(
              await isFeatureAvailable(editorMessage.value.feature)
            );
          }

          break;
        case EditorEvents.monograph:
          publishNote();
          break;
        case EditorEvents.properties:
          showActionsheet();
          break;
        case EditorEvents.scroll:
          editorState().scrollPosition = editorMessage.value;
          break;
        case EditorEvents.fullscreen:
          editorState().isFullscreen = true;
          eSendEvent(eOpenFullscreenEditor);
          break;
        case EditorEvents.link:
          if (editorMessage.value.startsWith("nn://")) {
            openInternalLink(editorMessage.value);
            console.log(
              "Opening note from internal link:",
              editorMessage.value
            );
          } else {
            openLinkInBrowser(editorMessage.value as string);
          }
          break;

        case EditorEvents.previewAttachment: {
          const hash = (editorMessage.value as Attachment)?.hash;
          const attachment = await db.attachments?.attachment(hash);
          if (!attachment) return;
          if (attachment.mimeType.startsWith("image/")) {
            eSendEvent("ImagePreview", editorMessage.value);
          } else {
            eSendEvent("PDFPreview", attachment);
          }
          break;
        }
        case EditorEvents.copyToClipboard: {
          Clipboard.setString(editorMessage.value as string);
          break;
        }
        case EditorEvents.saveScroll: {
          useTabStore.getState().updateTab(editorMessage.tabId, {
            session: {
              ...editorMessage.value
            }
          });
          break;
        }
        case EditorEvents.newNote: {
          eSendEvent(eOnLoadNote, {
            tabId: editorMessage.tabId,
            newNote: true
          });
          break;
        }
        case EditorEvents.tabsChanged: {
          // useTabStore.setState({
          //   tabs: (editorMessage.value as any)?.tabs,
          //   currentTab: (editorMessage.value as any)?.currentTab
          // });
          //
          break;
        }
        case EditorEvents.toc:
          TableOfContents.present(editorMessage.value);
          break;
        case EditorEvents.showTabs: {
          EditorTabs.present();
          break;
        }
        case EditorEvents.error: {
          presentSheet({
            component: (
              <Issue
                defaultBody={editorMessage.value.stack}
                defaultTitle={editorMessage.value.message}
                issueTitle={editorMessage.value.message}
              />
            )
          });
          break;
        }
        case EditorEvents.tabFocused: {
          eSendEvent(eEditorTabFocused, editorMessage.tabId);
          if (!editor.state.current.initialLoadCalled) break;
          if (editorMessage.noteId) {
            // Wait for next tick to ensure the tab is not changed.
            await sleep(1);
            if (useTabStore.getState().currentTab !== editorMessage.tabId)
              return;
            if (!useSettingStore.getState().isAppLoading) {
              const note = await db.notes.note(editorMessage.noteId);
              if (note) {
                eSendEvent(eOnLoadNote, {
                  item: note,
                  tabId: editorMessage.tabId,
                  loadedFromEditor: true
                });
              }
            } else {
              const unsub = useSettingStore.subscribe(async (state) => {
                if (!state.isAppLoading) {
                  unsub();
                  const note = await db.notes.note(editorMessage.noteId);
                  if (note) {
                    eSendEvent(eOnLoadNote, {
                      item: note,
                      tabId: editorMessage.tabId,
                      loadedFromEditor: true
                    });
                  }
                }
              });
            }
          }

          break;
        }
        case EditorEvents.createInternalLink: {
          LinkNote.present(
            editorMessage.value.attributes,
            editorMessage.resolverId as string
          );
          break;
        }

        case EditorEvents.unlock: {
          eSendEvent(eUnlockWithPassword, {
            ...editorMessage.value,
            resolverId: editorMessage.resolverId
          });
          break;
        }

        case EditorEvents.goBack: {
          useTabStore.getState().goBack();
          break;
        }

        case EditorEvents.goForward: {
          useTabStore.getState().goForward();
          break;
        }

        case EditorEvents.unlockWithBiometrics: {
          eSendEvent(eUnlockWithBiometrics);
          break;
        }

        case EditorEvents.disableReadonlyMode: {
          const noteId = editorMessage.value;
          if (noteId) {
            await db.notes.readonly(false, noteId);
            editor.note.current[noteId] = await db.notes?.note(noteId);
            useTabStore
              .getState()
              .updateTab(useTabStore.getState().currentTab!, {
                session: {
                  readonly: false
                }
              });
            setTimeout(() => {
              Navigation.queueRoutesForUpdate();
            });
          }
          break;
        }
        case EditorEvents.downloadCsv: {
          try {
            const csv = editorMessage.value;
            let filePath: string;
            let fileName: string;
            if (Platform.OS === "android") {
              const file = await ScopedStorage.createDocument(
                "table.csv",
                "text/csv",
                csv,
                "utf8"
              );
              if (!file) return;
              filePath = file.path || file.uri;
              fileName = file.name;
            } else {
              const path = await filesystem.checkAndCreateDir("/");
              let possibleFileName = "table.csv";
              let counter = 1;

              // Check if file exists and find available filename
              while (
                await ReactNativeBlobUtil.fs.exists(path + possibleFileName)
              ) {
                possibleFileName = `table (${counter}).csv`;
                counter++;
              }
              await ReactNativeBlobUtil.fs.writeFile(
                path + possibleFileName,
                csv,
                "utf8"
              );
              filePath = path + possibleFileName;
              fileName = possibleFileName;
            }

            await sleep(500);
            presentSheet({
              title: "Table saved to csv",
              paragraph: strings.fileSaved(fileName, Platform.OS),
              icon: "download",
              context: "global",
              component: (
                <ShareComponent uri={filePath} name={fileName} padding={12} />
              )
            });
          } catch (e) {
            ToastManager.show({
              type: "info",
              message: "Could not save table to csv"
            });
            DatabaseLogger.error(e as Error);
          }

          break;
        }

        default:
          break;
      }
      eSendEvent(editorMessage.type, editorMessage);
    },
    [editor, onBackPress]
  );

  return onMessage;
};
