import ExcalidrawView from "../ExcalidrawView";
import { App, Notice, TFile } from "obsidian";
import {
  AppState,
  ExcalidrawImperativeAPI,
} from "@zsviczian/excalidraw/types/excalidraw/types";
import {
  DEVICE,
  IMAGE_TYPES,
  REG_LINKINDEX_INVALIDCHARS,
  viewportCoordsToSceneCoords,
} from "src/constants/constants";
import {
  internalDragModifierType,
  isWinCTRLorMacCMD,
  localFileDragModifierType,
  modifierKeyTooltipMessages,
  webbrowserDragModifierType,
} from "src/utils/modifierkeyHelper";
import {
  errorlog,
  hyperlinkIsImage,
  hyperlinkIsYouTubeLink,
} from "src/utils/utils";
import { InsertPDFModal } from "src/shared/Dialogs/InsertPDFModal";
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import { getEA } from "src/core";
import {
  insertEmbeddableToView,
  insertImageToView,
} from "src/utils/excalidrawViewUtils";
import { t } from "src/lang/helpers";
import ExcalidrawPlugin from "src/core/main";
import {
  getInternalLinkOrFileURLLink,
  getURLImageExtension,
  importFileToVault,
} from "src/utils/fileUtils";
import { ScriptEngine } from "src/shared/Scripts";
import { UniversalInsertFileModal } from "src/shared/Dialogs/UniversalInsertFileModal";
import { Position } from "src/types/excalidrawViewTypes";
import { setStyle } from "src/utils/styleUtils";
import { ObsidianDraggable } from "src/types/types";
import type React from "react";

/*
static getDropAction(event: DragEvent): string {
  // Get modifier action
}

static parseDropData(event: DragEvent): DropData {
  // Parse drop data into clean format
}

static handleInternalDrop(data: DropData, context: DropContext): boolean {
  // Handle Obsidian internal file drops
}

static handleExternalFileDrop(data: DropData, context: DropContext): boolean {
  // Handle external file drops
}

static handleTextDrop(data: DropData, context: DropContext): boolean {
  // Handle text/url drops
}*/

export class DropManager {
  private view: ExcalidrawView;
  private app: App;
  private draginfoDiv: HTMLDivElement;

  constructor(view: ExcalidrawView) {
    this.view = view;
    this.app = this.view.app;
  }

  public destroy() {
    if (this.draginfoDiv) {
      this.ownerDocument.body.removeChild(this.draginfoDiv);
      delete this.draginfoDiv;
    }
  }

  get ownerDocument(): Document {
    return this.view.ownerDocument;
  }

  get currentPosition(): Position {
    return this.view.currentPosition;
  }

  set currentPosition(pos: Position) {
    this.view.currentPosition = pos;
  }

  get excalidrawAPI(): ExcalidrawImperativeAPI {
    return this.view.excalidrawAPI;
  }

  get plugin(): ExcalidrawPlugin {
    return this.view.plugin;
  }

  get file(): TFile {
    return this.view.file;
  }

  public onDrop(event: React.DragEvent<HTMLDivElement>): boolean {
    if (this.draginfoDiv) {
      this.ownerDocument.body.removeChild(this.draginfoDiv);
      delete this.draginfoDiv;
    }
    const api = this.excalidrawAPI;
    if (!api) {
      return false;
    }
    const st: AppState = api.getAppState();
    this.currentPosition = viewportCoordsToSceneCoords(
      { clientX: event.clientX, clientY: event.clientY },
      st,
    );
    const draggable = this.app.dragManager
      .draggable as unknown as ObsidianDraggable;
    const internalDragAction = internalDragModifierType(event);
    const externalDragAction = webbrowserDragModifierType(event);
    const localFileDragAction = localFileDragModifierType(event);

    //Call Excalidraw Automate onDropHook
    const onDropHook = (
      type: "file" | "text" | "unknown",
      files: TFile[],
      text: string,
    ): boolean => {
      this.app.workspace.setActiveLeaf(this.view.leaf); //ensure the view is active, so ModalContainerObserver is active
      if (this.view.getHookServer().onDropHook) {
        try {
          return this.view.getHookServer().onDropHook({
            ea: this.view.getHookServer(), //the ExcalidrawAutomate object
            event, //React.DragEvent<HTMLDivElement>
            draggable, //Obsidian draggable object
            type, //"file"|"text"
            payload: {
              files, //TFile[] array of dropped files
              text, //string
            },
            excalidrawFile: this.file, //the file receiving the drop event
            view: this.view, //the excalidraw view receiving the drop
            pointerPosition: this.currentPosition, //the pointer position on canvas at the time of drop
          });
        } catch (e: unknown) {
          new Notice("On drop hook error. See console log for details");
          errorlog({ where: "ExcalidrawView.onDrop", error: e });
          return false;
        }
      } else {
        return false;
      }
    };

    //---------------------------------------------------------------------------------
    // Obsidian internal drag event
    //---------------------------------------------------------------------------------
    switch (draggable?.type) {
      case "link":
      case "file":
        if (!onDropHook("file", [draggable.file], null)) {
          const file: TFile = draggable.file;
          //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/422
          if (file.path.match(REG_LINKINDEX_INVALIDCHARS)) {
            new Notice(t("FILENAME_INVALID_CHARS"), 4000);
            return false;
          }
          if (
            ["image", "image-fullsize"].contains(internalDragAction) &&
            (IMAGE_TYPES.contains(file.extension) ||
              file.extension === "md" ||
              file.extension.toLowerCase() === "pdf")
          ) {
            if (file.extension.toLowerCase() === "pdf") {
              const insertPDFModal = new InsertPDFModal(this.plugin, this.view);
              insertPDFModal.open(file);
            } else {
              void (async () => {
                const ea: ExcalidrawAutomate = getEA(this.view);
                ea.selectElementsInView([
                  await insertImageToView(
                    ea,
                    this.currentPosition,
                    file,
                    !(internalDragAction === "image-fullsize"),
                  ),
                ]);
                ea.destroy();
              })();
            }
            return false;
          }

          if (internalDragAction === "embeddable") {
            void (async () => {
              const ea: ExcalidrawAutomate = getEA(this.view);
              ea.selectElementsInView([
                await insertEmbeddableToView(ea, this.currentPosition, file),
              ]);
              ea.destroy();
            })();
            return false;
          }

          //internalDragAction === "link"
          void this.view.addText(
            `[[${this.app.metadataCache.fileToLinktext(
              draggable.file,
              this.file.path,
              true,
            )}]]`,
          );
        }
        return false;
      case "files":
        if (!onDropHook("file", draggable.files, null)) {
          void (async () => {
            if (["image", "image-fullsize"].contains(internalDragAction)) {
              const ea: ExcalidrawAutomate = getEA(this.view);
              ea.canvas.theme = api.getAppState().theme as string;
              let counter: number = 0;
              const ids: string[] = [];
              for (const f of draggable.files) {
                if (IMAGE_TYPES.contains(f.extension) || f.extension === "md") {
                  ids.push(
                    await ea.addImage(
                      this.currentPosition.x + counter * 50,
                      this.currentPosition.y + counter * 50,
                      f,
                      !(internalDragAction === "image-fullsize"),
                    ),
                  );
                  counter++;
                  await ea.addElementsToView(false, false, true);
                  ea.selectElementsInView(ids);
                }
                if (f.extension.toLowerCase() === "pdf") {
                  const insertPDFModal = new InsertPDFModal(
                    this.plugin,
                    this.view,
                  );
                  insertPDFModal.open(f);
                }
              }
              ea.destroy();
              return;
            }

            if (internalDragAction === "embeddable") {
              const ea: ExcalidrawAutomate = getEA(this.view);
              let column: number = 0;
              let row: number = 0;
              const ids: string[] = [];
              for (const f of draggable.files) {
                ids.push(
                  await insertEmbeddableToView(
                    ea,
                    {
                      x: this.currentPosition.x + column * 500,
                      y: this.currentPosition.y + row * 550,
                    },
                    f,
                  ),
                );
                column = (column + 1) % 3;
                if (column === 0) {
                  row++;
                }
              }
              ea.destroy();
              return false;
            }

            //internalDragAction === "link"
            for (const f of draggable.files) {
              await this.view.addText(
                `[[${this.app.metadataCache.fileToLinktext(
                  f,
                  this.file.path,
                  true,
                )}]]`,
                undefined,
                false,
              );
              this.currentPosition.y += st.currentItemFontSize * 2;
            }
            await this.view.save(false);
          })();
        }
        return false;
    }

    //---------------------------------------------------------------------------------
    // externalDragAction
    //---------------------------------------------------------------------------------
    if (event.dataTransfer.types.includes("Files")) {
      if (event.dataTransfer.types.includes("text/plain")) {
        const text: string = event.dataTransfer.getData("text");
        if (text && onDropHook("text", null, text)) {
          return false;
        }
        if (
          text &&
          externalDragAction === "image-url" &&
          hyperlinkIsImage(text)
        ) {
          void this.view.addImageWithURL(text);
          return false;
        }
        if (text && externalDragAction === "link") {
          if (
            this.plugin.settings.oEmbedAllowed &&
            text.match(/^https:\/\/\S*$/)
          ) {
            void this.view.addTextWithOEmbed(text);
            return false;
          }
          void this.view.addText(text);
          return false;
        }
        if (text && externalDragAction === "embeddable") {
          const ea = getEA(this.view);
          void insertEmbeddableToView(
            ea,
            this.currentPosition,
            undefined,
            text,
          ).then(() => ea.destroy());
          return false;
        }
      }

      if (event.dataTransfer.types.includes("text/html")) {
        const html = event.dataTransfer.getData("text/html");
        const src = html.match(/src=["']([^"']*)["']/);
        if (
          src &&
          externalDragAction === "image-url" &&
          hyperlinkIsImage(src[1])
        ) {
          void this.view.addImageWithURL(src[1]);
          return false;
        }
        if (src && externalDragAction === "link") {
          if (
            this.plugin.settings.oEmbedAllowed &&
            src[1].match(/^https:\/\/\S*$/)
          ) {
            void this.view.addTextWithOEmbed(src[1]);
            return false;
          }
          void this.view.addText(src[1]);
          return false;
        }
        if (src && externalDragAction === "embeddable") {
          const ea = getEA(this.view);
          void insertEmbeddableToView(
            ea,
            this.currentPosition,
            undefined,
            src[1],
          ).then(() => ea.destroy());
          return false;
        }
      }

      if (
        event.dataTransfer.types.length >= 1 &&
        ["image-url", "image-import", "embeddable"].contains(
          localFileDragAction,
        )
      ) {
        const files = Array.from(event.dataTransfer.files || []);

        for (let i = 0; i < files.length; i++) {
          // Try multiple ways to get file path
          const file = files[i];
          let path = file?.path;

          if (!path && file && DEVICE.isDesktop) {
            //https://www.electronjs.org/docs/latest/breaking-changes#removed-filepath
            const webUtils = window.electron.webUtils;
            if (webUtils && webUtils.getPathForFile) {
              path = webUtils.getPathForFile(file);
            }
          }
          if (!path) {
            new Notice(t("ERROR_CANT_READ_FILEPATH"), 6000);
            return true; //excalidraw to continue processing
          }
          const link = getInternalLinkOrFileURLLink(
            path,
            this.plugin,
            event.dataTransfer.files[i].name,
            this.file,
          );
          const { x, y } = this.currentPosition;
          const pos = { x: x + i * 300, y: y + i * 300 };
          if (link.isInternal) {
            if (localFileDragAction === "embeddable") {
              const ea = getEA(this.view);
              void insertEmbeddableToView(ea, pos, link.file).then(() =>
                ea.destroy(),
              );
            } else {
              if (link.file.extension === "pdf") {
                const insertPDFModal = new InsertPDFModal(
                  this.plugin,
                  this.view,
                );
                insertPDFModal.open(link.file);
              }
              const ea = getEA(this.view);
              void insertImageToView(ea, pos, link.file).then(() =>
                ea.destroy(),
              );
            }
          } else {
            const extension = getURLImageExtension(link.url);
            if (localFileDragAction === "image-import") {
              if (IMAGE_TYPES.contains(extension)) {
                void (async () => {
                  const droppedFilename = event.dataTransfer.files[i].name;
                  const fileToImport =
                    await event.dataTransfer.files[i].arrayBuffer();
                  const maybeFile = this.app.metadataCache.getFirstLinkpathDest(
                    droppedFilename,
                    this.file.path,
                  );
                  if (maybeFile && maybeFile instanceof TFile) {
                    const action = await ScriptEngine.suggester(
                      this.app,
                      [
                        "Use the file already in the Vault instead of importing",
                        "Overwrite existing file in the Vault",
                        "Import the file with a new name",
                      ],
                      ["Use", "Overwrite", "Import"],
                      `A file with the same name/path already exists in the Vault\n${
                        maybeFile.path
                      }`,
                    );
                    if (action !== "Import") {
                      if (action === "Overwrite") {
                        await this.app.vault.modifyBinary(
                          maybeFile,
                          fileToImport,
                        );
                      }
                      const ea = getEA(this.view);
                      await insertImageToView(ea, pos, maybeFile);
                      ea.destroy();
                      return false;
                    }
                  }
                  const file = await importFileToVault(
                    this.app,
                    droppedFilename,
                    fileToImport,
                    this.view.file,
                    this.view,
                  );
                  const ea = getEA(this.view);
                  await insertImageToView(ea, pos, file);
                  ea.destroy();
                })();
              } else if (extension === "excalidraw") {
                return true; //excalidraw to continue processing
              } else {
                void (async () => {
                  const file = await importFileToVault(
                    this.app,
                    event.dataTransfer.files[i].name,
                    await event.dataTransfer.files[i].arrayBuffer(),
                    this.view.file,
                    this.view,
                  );
                  const modal = new UniversalInsertFileModal(
                    this.plugin,
                    this.view,
                  );
                  modal.open(file, pos);
                })();
              }
            } else if (
              localFileDragAction === "embeddable" ||
              !IMAGE_TYPES.contains(extension)
            ) {
              const ea = getEA(this.view);
              void insertEmbeddableToView(ea, pos, null, link.url).then(() =>
                ea.destroy(),
              );
              if (localFileDragAction !== "embeddable") {
                new Notice("Not imported to vault. Embedded with local uri.");
              }
            } else {
              const ea = getEA(this.view);
              void insertImageToView(ea, pos, link.url).then(() =>
                ea.destroy(),
              );
            }
          }
        }
        return false;
      }

      if (
        event.dataTransfer.types.length >= 1 &&
        localFileDragAction === "link"
      ) {
        const ea = getEA(this.view);
        for (let i = 0; i < event.dataTransfer.files.length; i++) {
          const file = event.dataTransfer.files[i];
          let path = file?.path;
          const name = file?.name;
          if (!path && file && DEVICE.isDesktop) {
            //https://www.electronjs.org/docs/latest/breaking-changes#removed-filepath
            const webUtils = window.electron.webUtils;
            if (webUtils && webUtils.getPathForFile) {
              path = webUtils.getPathForFile(file);
            }
          }
          if (!path || !name) {
            new Notice(t("ERROR_CANT_READ_FILEPATH"), 6000);
            ea.destroy();
            return true; //excalidraw to continue processing
          }
          const link = getInternalLinkOrFileURLLink(
            path,
            this.plugin,
            name,
            this.file,
          );
          const id = ea.addText(
            this.currentPosition.x + i * 40,
            this.currentPosition.y + i * 20,
            link.isInternal ? link.link : `📂 ${name}`,
          );
          if (!link.isInternal) {
            ea.getElement(id).link = link.link;
          }
        }
        void ea.addElementsToView().then(() => ea.destroy());
        return false;
      }

      return true;
    }

    if (
      event.dataTransfer.types.includes("text/plain") ||
      event.dataTransfer.types.includes("text/uri-list") ||
      event.dataTransfer.types.includes("text/html")
    ) {
      const html = event.dataTransfer.getData("text/html");
      const src = html.match(/src=["']([^"']*)["']/);
      const htmlText = src ? src[1] : "";
      const textText = event.dataTransfer.getData("text");
      const uriText = event.dataTransfer.getData("text/uri-list");

      let text: string = src ? htmlText : textText;
      if (!text || text === "") {
        text = uriText;
      }
      if (!text || text === "") {
        return true;
      }
      if (!onDropHook("text", null, text)) {
        if (
          text &&
          externalDragAction === "embeddable" &&
          /^(blob:)?(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(text)
        ) {
          return true;
        }
        if (
          text &&
          externalDragAction === "image-url" &&
          hyperlinkIsYouTubeLink(text)
        ) {
          void this.view.addYouTubeThumbnail(text);
          return false;
        }
        if (
          uriText &&
          externalDragAction === "image-url" &&
          hyperlinkIsYouTubeLink(uriText)
        ) {
          void this.view.addYouTubeThumbnail(uriText);
          return false;
        }
        if (
          text &&
          externalDragAction === "image-url" &&
          hyperlinkIsImage(text)
        ) {
          void this.view.addImageWithURL(text);
          return false;
        }
        if (
          uriText &&
          externalDragAction === "image-url" &&
          hyperlinkIsImage(uriText)
        ) {
          void this.view.addImageWithURL(uriText);
          return false;
        }
        if (
          text &&
          externalDragAction === "image-import" &&
          hyperlinkIsImage(text)
        ) {
          void this.view.addImageSaveToVault(text);
          return false;
        }
        if (
          uriText &&
          externalDragAction === "image-import" &&
          hyperlinkIsImage(uriText)
        ) {
          void this.view.addImageSaveToVault(uriText);
          return false;
        }
        if (
          this.plugin.settings.oEmbedAllowed &&
          text.match(/^https:\/\/\S*$/)
        ) {
          void this.view.addTextWithOEmbed(text);
          return false;
        }
        //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/599
        if (text.startsWith("obsidian://open?vault=")) {
          const html = event.dataTransfer.getData("text/html");
          if (html) {
            const path = html.match(/href="app:\/\/obsidian\.md\/(.*?)"/);
            if (path.length === 2) {
              const link = decodeURIComponent(path[1]).split("#");
              const f = this.app.vault.getAbstractFileByPath(link[0]);
              if (f && f instanceof TFile) {
                const path = this.app.metadataCache.fileToLinktext(
                  f,
                  this.file.path,
                );
                void this.view.addText(
                  `[[${
                    path + (link.length > 1 ? `#${link[1]}|${path}` : "")
                  }]]`,
                );
                return;
              }
              void this.view.addText(`[[${decodeURIComponent(path[1])}]]`);
              return false;
            }
          }
          const path = text.split("file=");
          if (path.length === 2) {
            void this.view.addText(`[[${decodeURIComponent(path[1])}]]`);
            return false;
          }
        }
        void this.view.addText(text.replace(/(!\[\[.*#[^\]]*\]\])/g, "$1{40}"));
      }
      return false;
    }
    if (onDropHook("unknown", null, null)) {
      return false;
    }
    return true;
  }

  public onDragOver(e: DragEvent) {
    const action = this.dropAction(e.dataTransfer);
    if (action) {
      if (!this.draginfoDiv) {
        this.draginfoDiv = createDiv({ cls: "excalidraw-draginfo" });
        this.ownerDocument.body.appendChild(this.draginfoDiv);
      }
      let msg: string = "";
      if (this.app.dragManager.draggable) {
        //drag from Obsidian file manager
        msg =
          modifierKeyTooltipMessages().InternalDragAction[
            internalDragModifierType(e)
          ];
      } else if (
        e.dataTransfer.types.length === 1 &&
        e.dataTransfer.types.includes("Files")
      ) {
        //drag from OS file manager
        msg =
          modifierKeyTooltipMessages().LocalFileDragAction[
            localFileDragModifierType(e)
          ];
        if (DEVICE.isMacOS && isWinCTRLorMacCMD(e)) {
          msg =
            "CMD is reserved by MacOS for file system drag actions.\nCan't use it in Obsidian.\nUse a combination of SHIFT, CTRL, OPT instead.";
        }
      } else {
        //drag from Internet
        msg =
          modifierKeyTooltipMessages().WebBrowserDragAction[
            webbrowserDragModifierType(e)
          ];
      }
      if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
        msg +=
          DEVICE.isMacOS || DEVICE.isIOS
            ? "\nTry SHIFT, OPT, CTRL combinations for other drop actions"
            : "\nTry SHIFT, CTRL, ALT, Meta combinations for other drop actions";
      }
      if (this.draginfoDiv.innerText !== msg) {
        this.draginfoDiv.innerText = msg;
      }
      const top = `${e.clientY - parseFloat(getComputedStyle(this.draginfoDiv).fontSize) * 8}px`;
      const left = `${e.clientX - this.draginfoDiv.clientWidth / 2}px`;
      if (this.draginfoDiv.style.top !== top) {
        setStyle(this.draginfoDiv, { top });
      }
      if (this.draginfoDiv.style.left !== left) {
        setStyle(this.draginfoDiv, { left });
      }
      e.dataTransfer.dropEffect = action;
      e.preventDefault();
      return false;
    }
  }

  public onDragLeave() {
    if (this.draginfoDiv) {
      this.ownerDocument.body.removeChild(this.draginfoDiv);
      delete this.draginfoDiv;
    }
  }

  private dropAction(transfer: DataTransfer) {
    // Return a 'copy' or 'link' action according to the content types, or undefined if no recognized type
    const files = this.app.dragManager.draggable?.files;
    if (files) {
      if (files[0] == this.file) {
        files.shift();
        if (this.app.dragManager.draggable) {
          this.app.dragManager.draggable.title = `${files.length} files`;
        }
      }
    }
    if (["file", "files"].includes(this.app.dragManager.draggable?.type)) {
      return "link";
    }
    if (
      transfer.types?.includes("text/html") ||
      transfer.types?.includes("text/plain") ||
      transfer.types?.includes("Files")
    ) {
      return "copy";
    }
  }
}
