import {
  App,
  MarkdownPostProcessorContext,
  MetadataCache,
  PaneType,
  TFile,
  Vault,
} from "obsidian";
import { DEVICE, RERENDER_EVENT } from "../../constants/constants";
import { EmbeddedFilesLoader } from "../../shared/EmbeddedFileLoader";
import { createPNG, createSVG } from "../../utils/excalidrawAutomateUtils";
import ExcalidrawPlugin from "../main";
import { getIMGFilename } from "../../utils/fileUtils";
import {
  getEmbeddedFilenameParts,
  getExportTheme,
  getQuickImagePreview,
  getExportPadding,
  getWithBackground,
  hasExportTheme,
  convertSVGStringToElement,
  isMaskFile,
} from "../../utils/utils";
import {
  getParentOfClass,
  isObsidianThemeDark,
  getFileCSSClasses,
  getSafeFrontmatter,
} from "../../utils/obsidianUtils";
import { linkClickModifierType } from "../../utils/modifierkeyHelper";
import { ImageKey, getImageCache } from "../../shared/ImageCache";
import { FILENAMEPARTS, PreviewImageType } from "../../types/utilTypes";
import { CustomMutationObserver, DEBUGGING } from "../../utils/debugHelper";
import { getExcalidrawFileForwardLinks } from "../../utils/excalidrawViewUtils";
import { linkPrompt } from "../../shared/Dialogs/Prompt";
import {
  isInstanceOfHTMLElement,
  isInstanceOfHTMLImageElement,
  isInstanceOfSVGElement,
} from "../../utils/typechecks";
import { ExportSettings } from "src/types/exportUtilTypes";
import { setElementDisplay } from "src/utils/htmlUtils";
import { setStyle } from "src/utils/styleUtils";
import { getGlobalTaskQueue } from "src/shared/GlobalTaskQueue";

interface imgElementAttributes {
  file?: TFile;
  fname: string; //Excalidraw filename
  fwidth: string; //Display width of image
  fheight: string; //Display height of image
  imgstyle: string[]; //css style to apply to IMG element
}

declare const mainDocument: Document;

let plugin: ExcalidrawPlugin;
let app: App;
let vault: Vault;
let metadataCache: MetadataCache;

const getDefaultWidth = (plugin: ExcalidrawPlugin): string => {
  const width = parseInt(plugin.settings.width);
  if (isNaN(width) || width === 0 || width === null) {
    if (getDefaultHeight(plugin) !== "") {
      return "";
    }
    return "400";
  }
  return plugin.settings.width;
};

const getDefaultHeight = (plugin: ExcalidrawPlugin): string => {
  const height = parseInt(plugin.settings.height);
  if (isNaN(height) || height === 0 || height === null) {
    return "";
  }
  return plugin.settings.height;
};

/**
 * Utility to aggressively halt background image decoding and free Blob memory
 * when an element is discarded without being attached to the DOM.
 */
const discardImage = (el: Element | null) => {
  if (!el) {
    return;
  }
  const images = isInstanceOfHTMLImageElement(el)
    ? [el]
    : Array.from(el.querySelectorAll("img"));
  images.forEach((img) => {
    if (img.src.startsWith("blob:")) {
      getImageCache().releaseObsidianURL(img.src);
    }
    // Instantly aborts the browser's background V8 image decoder task
    img.src = "";
  });
};

export const initializeMarkdownPostProcessor = (p: ExcalidrawPlugin) => {
  plugin = p;
  app = plugin.app;
  vault = app.vault;
  metadataCache = app.metadataCache;
};

const _getPNG = async ({
  imgAttributes,
  filenameParts,
  theme,
  cacheReady,
  img,
  file,
  exportSettings,
  loader,
}: {
  imgAttributes: imgElementAttributes;
  filenameParts: FILENAMEPARTS;
  theme: string;
  cacheReady: boolean;
  img: HTMLImageElement;
  file: TFile;
  exportSettings: ExportSettings;
  loader: EmbeddedFilesLoader;
}): Promise<HTMLImageElement> => {
  const width = parseInt(imgAttributes.fwidth);
  const scale =
    width >= 2400
      ? 5
      : width >= 1800
        ? 4
        : width >= 1200
          ? 3
          : width >= 600
            ? 2
            : 1;

  const cacheKey = {
    ...filenameParts,
    isDark: theme === "dark",
    previewImageType: PreviewImageType.PNG,
    scale,
    isTransparent: !exportSettings.withBackground,
    inlineFonts: true, //though for PNG this makes no difference, but the key requires it
  };

  if (cacheReady) {
    const src = await getImageCache().getImageFromCache(cacheKey);
    //In case of PNG I cannot change the viewBox to select the area of the element
    //being referenced. For PNG only the group reference works
    if (src && typeof src === "string") {
      img.src = src;
      return img;
    }
  }

  const quickPNG = !(filenameParts.hasGroupref || filenameParts.hasFrameref)
    ? await getQuickImagePreview(plugin, file.path, "png")
    : undefined;

  const png =
    quickPNG ??
    (await createPNG(
      filenameParts.hasGroupref ||
        filenameParts.hasFrameref ||
        filenameParts.hasClippedFrameref
        ? filenameParts.filepath + filenameParts.linkpartReference
        : file.path,
      scale,
      filenameParts.hasClippedFrameref
        ? {
            ...exportSettings,
            frameRendering: {
              enabled: true,
              name: false,
              outline: false,
              clip: true,
            },
          }
        : exportSettings,
      loader,
      theme,
      null,
      null,
      [],
      plugin,
      0,
    ));
  if (!png) {
    return null;
  }
  const blobUrl = URL.createObjectURL(png);
  img.src = blobUrl;

  // Revoke the ObjectURL after the image loads or fails to prevent memory leaks
  if (!cacheReady) {
    const cleanup = () => URL.revokeObjectURL(blobUrl);
    img.addEventListener("load", cleanup, { once: true });
    img.addEventListener("error", cleanup, { once: true });
  }

  if (cacheReady) {
    getImageCache().addImageToCache(cacheKey, blobUrl, png);
  }

  return img;
};

const setImgStyle = ({
  element,
  imgAttributes,
  onCanvas,
  isNativeSVG,
}: {
  element: HTMLElement;
  imgAttributes: imgElementAttributes;
  onCanvas: boolean;
  isNativeSVG: boolean;
}) => {
  if (!onCanvas) {
    const styles: Partial<CSSStyleDeclaration> = {};

    // 1. Resolve Width & Max-Width
    if (imgAttributes.fwidth) {
      const unit = imgAttributes.fwidth.match(/\d$/) ? "px" : "";
      const finalWidth = `${imgAttributes.fwidth}${unit}`;

      if (isNativeSVG) {
        styles.maxWidth = finalWidth;
      } else {
        styles.maxWidth = "100%";
        styles.width = finalWidth;
      }
    } else {
      styles.width = "fit-content";
    }

    // 2. Resolve Height (Min vs Max)
    if (imgAttributes.fheight) {
      const targetHeightProperty = imgAttributes.fwidth
        ? "minHeight"
        : "maxHeight";
      styles[targetHeightProperty] = `${imgAttributes.fheight}px`;
    }

    // Apply styles uniformly using the generic function
    setStyle(element, styles);
  }
  element.classList.add(...Array.from(imgAttributes.imgstyle));
  if (!element.hasClass("excalidraw-embedded-img")) {
    element.addClass("excalidraw-embedded-img");
  }
  if (
    window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed &&
    !element.hasClass("excalidraw-canvas-immersive")
  ) {
    element.addClass("excalidraw-canvas-immersive");
  }
};

const _getSVGIMG = async ({
  filenameParts,
  theme,
  cacheReady,
  img,
  file,
  exportSettings,
  loader,
}: {
  filenameParts: FILENAMEPARTS;
  theme: string;
  cacheReady: boolean;
  img: HTMLImageElement;
  file: TFile;
  exportSettings: ExportSettings;
  loader: EmbeddedFilesLoader;
}): Promise<HTMLImageElement> => {
  exportSettings.skipInliningFonts = false;
  const cacheKey = {
    ...filenameParts,
    isDark: theme === "dark",
    previewImageType: PreviewImageType.SVGIMG,
    scale: 1,
    isTransparent: !exportSettings.withBackground,
    inlineFonts: !exportSettings.skipInliningFonts,
  };

  if (cacheReady) {
    const src = await getImageCache().getImageFromCache(cacheKey);
    if (src && typeof src === "string") {
      img.setAttribute("src", src);
      return img;
    }
  }

  if (!(filenameParts.hasBlockref || filenameParts.hasSectionref)) {
    const quickSVG = await getQuickImagePreview(plugin, file.path, "svg");
    if (quickSVG) {
      const svg = convertSVGStringToElement(quickSVG);
      if (svg) {
        return addSVGToImgSrc(img, svg, cacheReady, cacheKey);
      }
    }
  }

  const svg = convertSVGStringToElement(
    (
      await createSVG(
        filenameParts.hasGroupref ||
          filenameParts.hasBlockref ||
          filenameParts.hasSectionref ||
          filenameParts.hasFrameref ||
          filenameParts.hasClippedFrameref
          ? filenameParts.filepath + filenameParts.linkpartReference
          : file.path,
        true,
        filenameParts?.hasClippedFrameref
          ? {
              ...exportSettings,
              frameRendering: {
                enabled: true,
                name: false,
                outline: false,
                clip: true,
              },
            }
          : exportSettings,
        loader,
        theme,
        null,
        null,
        [],
        plugin,
        0,
        getExportPadding(plugin, file),
      )
    ).outerHTML,
  );

  if (!svg) {
    return null;
  }

  //need to remove width and height attributes to support area= embeds
  //svg.removeAttribute("width");
  svg.setAttribute("width", "6000");
  svg.removeAttribute("height");
  return addSVGToImgSrc(img, svg, cacheReady, cacheKey);
};

const _getSVGNative = async ({
  filenameParts,
  theme,
  cacheReady,
  containerElement,
  file,
  exportSettings,
  loader,
  width,
}: {
  filenameParts: FILENAMEPARTS;
  theme: string;
  cacheReady: boolean;
  containerElement: HTMLDivElement;
  file: TFile;
  exportSettings: ExportSettings;
  loader: EmbeddedFilesLoader;
  width?: number;
}): Promise<HTMLDivElement> => {
  exportSettings.skipInliningFonts = false;
  const cacheKey = {
    ...filenameParts,
    isDark: theme === "dark",
    previewImageType: PreviewImageType.SVG,
    scale: 1,
    isTransparent: !exportSettings.withBackground,
    inlineFonts: !exportSettings.skipInliningFonts,
  };
  let maybeSVG;
  if (cacheReady) {
    maybeSVG = await getImageCache().getImageFromCache(cacheKey);
  }

  const svg =
    maybeSVG && maybeSVG instanceof SVGSVGElement
      ? maybeSVG
      : convertSVGStringToElement(
          (
            await createSVG(
              filenameParts.hasGroupref ||
                filenameParts.hasBlockref ||
                filenameParts.hasSectionref ||
                filenameParts.hasFrameref ||
                filenameParts.hasClippedFrameref
                ? filenameParts.filepath + filenameParts.linkpartReference
                : file.path,
              false,
              filenameParts.hasClippedFrameref
                ? {
                    ...exportSettings,
                    frameRendering: {
                      enabled: true,
                      name: false,
                      outline: false,
                      clip: true,
                    },
                  }
                : exportSettings,
              loader,
              theme,
              null,
              null,
              [],
              plugin,
              0,
              getExportPadding(plugin, file),
              undefined,
              true,
            )
          ).outerHTML,
        );

  if (!svg) {
    return null;
  }

  //cache SVG should have the width and height parameters and not the embedded font
  if (!maybeSVG && cacheReady) {
    getImageCache().addImageToCache(cacheKey, "", svg);
  }

  if (width && !isNaN(width)) {
    svg.setAttribute("width", width.toString());
  } else {
    svg.removeAttribute("width");
  }
  svg.removeAttribute("height");
  containerElement.append(svg);
  return containerElement;
};

/**
 * Generates an IMG or DIV element
 * - The IMG element will have the drawing encoded as a base64 SVG or a PNG (depending on settings)
 * - The DIV element will have the drawing as an SVG element
 * @param parts {imgElementAttributes} - display properties of the image
 * @returns {Promise<HTMLElement>} - the IMG HTML element containing the image
 */
const getIMG = async (
  imgAttributes: imgElementAttributes,
  onCanvas: boolean = false,
  embedEl?: Node,
): Promise<HTMLImageElement | HTMLDivElement> => {
  let file = imgAttributes.file;
  if (!imgAttributes.file) {
    const f = vault.getAbstractFileByPath(imgAttributes.fname?.split("#")[0]);
    if (!(f && f instanceof TFile)) {
      return null;
    }
    file = f;
  }

  const filenameParts = getEmbeddedFilenameParts(imgAttributes.fname);

  // https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/387
  imgAttributes.imgstyle = imgAttributes.imgstyle.map((s) =>
    s.replaceAll(" ", "-"),
  );

  const forceTheme = hasExportTheme(plugin, file)
    ? getExportTheme(plugin, file, "light")
    : undefined;

  const exportSettings: ExportSettings = {
    withBackground: getWithBackground(plugin, file),
    withTheme: forceTheme ? true : plugin.settings.exportWithTheme,
    isMask: isMaskFile(plugin, file),
  };

  const theme =
    forceTheme ??
    (plugin.settings.previewMatchObsidianTheme
      ? isObsidianThemeDark()
        ? "dark"
        : "light"
      : !plugin.settings.exportWithTheme
        ? "light"
        : undefined);
  if (theme) {
    exportSettings.withTheme = true;
  }
  const loader = new EmbeddedFilesLoader(
    plugin,
    theme ? theme === "dark" : undefined,
  );

  let terminationTimer: number;
  if (embedEl) {
    terminationTimer = window.setInterval(() => {
      if (!embedEl.isConnected) {
        loader.terminate = true;
        window.clearInterval(terminationTimer);
      }
    }, 500);
  }

  const cacheReady = getImageCache().isReady();

  let resultElement: HTMLImageElement | HTMLDivElement = null;
  try {
    await plugin.awaitInit();
    switch (plugin.settings.previewImageType) {
      case PreviewImageType.PNG: {
        const img = createEl("img");
        setImgStyle({
          element: img,
          imgAttributes,
          onCanvas,
          isNativeSVG: false,
        });
        resultElement = await _getPNG({
          imgAttributes,
          filenameParts,
          theme,
          cacheReady,
          img,
          file,
          exportSettings,
          loader,
        });
        break;
      }
      case PreviewImageType.SVGIMG: {
        const img = createEl("img");
        setImgStyle({
          element: img,
          imgAttributes,
          onCanvas,
          isNativeSVG: false,
        });
        resultElement = await _getSVGIMG({
          filenameParts,
          theme,
          cacheReady,
          img,
          file,
          exportSettings,
          loader,
        });
        break;
      }
      case PreviewImageType.SVG: {
        const img = createDiv();
        setImgStyle({
          element: img,
          imgAttributes,
          onCanvas,
          isNativeSVG: true,
        });
        resultElement = await _getSVGNative({
          filenameParts,
          theme,
          cacheReady,
          containerElement: img,
          file,
          exportSettings,
          loader,
          width: imgAttributes.fwidth
            ? !imgAttributes.fwidth.endsWith("%")
              ? parseInt(imgAttributes.fwidth)
              : 6000
            : undefined,
        });
        break;
      }
    }
  } finally {
    if (terminationTimer) {
      window.clearInterval(terminationTimer);
    }
  }
  return resultElement;
};

const addSVGToImgSrc = (
  img: HTMLImageElement,
  svg: SVGSVGElement,
  cacheReady: boolean,
  cacheKey: ImageKey,
): HTMLImageElement => {
  //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026
  //const svgString = new XMLSerializer().serializeToString(svg);
  const svgString = svg.outerHTML;
  const blob = new Blob([svgString], { type: "image/svg+xml" });
  const blobUrl = URL.createObjectURL(blob);
  img.setAttribute("src", blobUrl);

  // Revoke the ObjectURL after the image loads or fails to prevent memory leaks
  if (!cacheReady) {
    const cleanup = () => URL.revokeObjectURL(blobUrl);
    img.addEventListener("load", cleanup, { once: true });
    img.addEventListener("error", cleanup, { once: true });
  }

  if (cacheReady) {
    getImageCache().addImageToCache(cacheKey, blobUrl, blob);
  }
  return img;
};

const createImgElement = async (
  attr: imgElementAttributes,
  onCanvas: boolean = false,
  embedEl?: Element,
): Promise<HTMLElement> => {
  const imgOrDiv = await getIMG(attr, onCanvas, embedEl);
  if (!imgOrDiv) {
    return null;
  }
  imgOrDiv.setAttribute("fileSource", attr.fname);
  if (attr.fwidth) {
    imgOrDiv.setAttribute("w", attr.fwidth);
  }
  if (attr.fheight) {
    imgOrDiv.setAttribute("h", attr.fheight);
  }
  imgOrDiv.setAttribute("draggable", "false");
  imgOrDiv.setAttribute("onCanvas", onCanvas ? "true" : "false");

  let timer: number;
  const clickEvent = (ev: PointerEvent) => {
    if (
      !(isInstanceOfHTMLElement(ev.target) || isInstanceOfSVGElement(ev.target))
    ) {
      return;
    }
    const targetElement = ev.target;
    const containerElement = targetElement.hasClass("excalidraw-embedded-img")
      ? ev.target
      : getParentOfClass(targetElement, "excalidraw-embedded-img");
    if (!containerElement) {
      return;
    }
    const src = imgOrDiv.getAttribute("fileSource");
    if (src) {
      const srcParts = src.match(/([^#]*)(.*)/);
      if (!srcParts) {
        return;
      }
      const f = vault.getFileByPath(srcParts[1]);
      const linkModifier = linkClickModifierType(ev);
      if (plugin.isExcalidrawFile(f) && isMaskFile(plugin, f)) {
        void (async () => {
          const linkString = `[[${f.path}${srcParts[2] ? `#${srcParts[2]}` : ""}]] ${getExcalidrawFileForwardLinks(plugin.app, f, new Set<string>())}`;
          const result = await linkPrompt(linkString, plugin.app);
          if (!result) {
            return;
          }
          const [file, linkText, subpath] = result;
          if (plugin.isExcalidrawFile(file)) {
            plugin.openDrawing(file, linkModifier, true, subpath);
            return;
          }
          let paneType: boolean | PaneType = false;
          switch (linkModifier) {
            case "active-pane":
              paneType = false;
              break;
            case "new-pane":
              paneType = "split";
              break;
            case "popout-window":
              paneType = "window";
              break;
            case "new-tab":
              paneType = "tab";
              break;
            case "md-properties":
              paneType = "tab";
              break;
          }
          await plugin.app.workspace.openLinkText(
            linkText,
            "",
            paneType,
            subpath ? { eState: { subpath } } : {},
          );
        })();
        return;
      }
      plugin.openDrawing(f, linkModifier, true, srcParts[2]);
    } //.ctrlKey||ev.metaKey);
  };
  //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1003
  let pointerDownEvent: PointerEvent | null = null;
  const eventElement = imgOrDiv as HTMLElement;

  /*plugin.settings.previewImageType === PreviewImageType.SVG
    ? imgOrDiv.firstElementChild as HTMLElement
    : imgOrDiv;*/

  eventElement.addEventListener("pointermove", (ev) => {
    if (!timer) {
      return;
    }
    if (
      Math.abs(ev.screenX - pointerDownEvent.screenX) > 10 ||
      Math.abs(ev.screenY - pointerDownEvent.screenY) > 10
    ) {
      window.clearTimeout(timer);
      timer = null;
    }
  });
  eventElement.addEventListener("pointerdown", (ev) => {
    if (imgOrDiv?.parentElement?.hasClass("canvas-node-content")) {
      return;
    }
    const PLUGIN = app.plugins.plugins[
      "obsidian-excalidraw-plugin"
    ] as ExcalidrawPlugin;
    const timeoutValue = DEVICE.isDesktop
      ? PLUGIN.settings.longPressDesktop
      : PLUGIN.settings.longPressMobile;
    timer = window.setTimeout(() => clickEvent(ev), timeoutValue);
    pointerDownEvent = ev;
  });
  eventElement.addEventListener("pointerup", () => {
    if (timer) {
      window.clearTimeout(timer);
    }
    timer = null;
  });
  eventElement.addEventListener("dblclick", clickEvent);
  eventElement.addEventListener(RERENDER_EVENT, (e) => {
    e.stopPropagation();
    void getGlobalTaskQueue(plugin).addTask(async () => {
      const parent = imgOrDiv.parentElement;
      if (!parent || !parent.isConnected) {
        return;
      }
      const imgMaxWidth = imgOrDiv.style.maxWidth;
      const imgMaxHeigth = imgOrDiv.style.maxHeight;
      const fileSource = imgOrDiv.getAttribute("fileSource");
      const onCanvas = imgOrDiv.getAttribute("onCanvas") === "true";
      const newImg = await createImgElement(
        {
          fname: fileSource,
          fwidth: imgOrDiv.getAttribute("w"),
          fheight: imgOrDiv.getAttribute("h"),
          imgstyle: [...Array.from(imgOrDiv.classList)],
        },
        onCanvas,
        parent,
      );
      if (!newImg) {
        return;
      }

      if (!parent.isConnected) {
        discardImage(newImg);
        return;
      }

      parent.empty();
      if (!onCanvas) {
        setStyle(newImg, {
          maxHeight: imgMaxHeigth,
          maxWidth: imgMaxWidth,
        });
      }
      newImg.setAttribute("fileSource", fileSource);
      parent.append(newImg);
    });
  });
  const cssClasses = getFileCSSClasses(attr.file);
  cssClasses.forEach((cssClass) => {
    if (imgOrDiv.hasClass(cssClass)) {
      return;
    }
    imgOrDiv.addClass(cssClass);
  });
  if (window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed) {
    if (!imgOrDiv.hasClass("excalidraw-canvas-immersive")) {
      imgOrDiv.addClass("excalidraw-canvas-immersive");
    }
  } else if (imgOrDiv.hasClass("excalidraw-canvas-immersive")) {
    imgOrDiv.removeClass("excalidraw-canvas-immersive");
  }
  return imgOrDiv;
};

const createImageDiv = async (
  attr: imgElementAttributes,
  onCanvas: boolean = false,
  embedEl?: Element,
): Promise<HTMLDivElement> => {
  const img = await createImgElement(attr, onCanvas, embedEl);
  if (!img) {
    return null;
  }
  return createDiv(attr.imgstyle.join(" "), (el) => el.append(img));
};

const processReadingMode = async (
  embeddedItems: NodeListOf<Element> | [HTMLElement],
  ctx: MarkdownPostProcessorContext,
) => {
  //We are processing a non-excalidraw file in reading mode
  //Embedded files will be displayed in an .internal-embed container

  //Iterating all the containers in the file to check which one is an excalidraw drawing
  //This is a for loop instead of embeddedItems.forEach() because processInternalEmbed at the end
  //is awaited, otherwise excalidraw images would not display in the Kanban plugin
  const promises: Promise<void>[] = [];

  for (const maybeDrawing of embeddedItems) {
    // If the node is detached from the DOM, the user has closed the note or navigated away.
    if (!maybeDrawing.isConnected) {
      break;
    }

    //check to see if the file in the src attribute exists
    const fname = maybeDrawing.getAttribute("src")?.split("#")[0];
    if (!fname) {
      continue;
    }

    const file = metadataCache.getFirstLinkpathDest(fname, ctx.sourcePath);

    //if the embeddedFile exits and it is an Excalidraw file
    //then lets replace the .internal-embed with the generated PNG or SVG image
    if (file && file instanceof TFile && plugin.isExcalidrawFile(file)) {
      if (isTextOnlyEmbed(maybeDrawing)) {
        //legacy reference to a block or section as text
        //should be embedded as legacy text
        continue;
      }

      promises.push(
        getGlobalTaskQueue(plugin).addTask(async () => {
          if (!maybeDrawing.isConnected) {
            return;
          }

          const imgDiv = await processInternalEmbed(maybeDrawing, file);
          if (!imgDiv) {
            return;
          }

          // Ensure we don't attempt to append onto a detached DOM tree
          if (!maybeDrawing.isConnected) {
            discardImage(imgDiv);
            return;
          }

          maybeDrawing.parentElement.replaceChild(imgDiv, maybeDrawing);
        }),
      );
    }
  }

  // Await the completion of all generated jobs allowing concurrent processing via the Global Task Queue
  await Promise.all(promises);
};

const processInternalEmbed = async (
  internalEmbedEl: Element,
  file: TFile,
): Promise<HTMLDivElement> => {
  const attr: imgElementAttributes = {
    fname: "",
    fheight: "",
    fwidth: "",
    imgstyle: [],
  };

  const src = internalEmbedEl.getAttribute("src");
  if (!src) {
    return null;
  }

  //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1059
  internalEmbedEl.removeClass("markdown-embed");
  internalEmbedEl.removeClass("inline-embed");
  internalEmbedEl.addClass("media-embed");
  internalEmbedEl.addClass("image-embed");

  attr.fwidth = internalEmbedEl.getAttribute("width")
    ? internalEmbedEl.getAttribute("width")
    : getDefaultWidth(plugin);
  attr.fheight = internalEmbedEl.getAttribute("height")
    ? internalEmbedEl.getAttribute("height")
    : getDefaultHeight(plugin);
  const alt = internalEmbedEl.getAttribute("alt");
  attr.imgstyle = ["excalidraw-svg"];
  processAltText(src.split("#")[0], alt, attr);
  const fnameParts = getEmbeddedFilenameParts(src);
  attr.fname =
    file?.path +
    (fnameParts.hasBlockref || fnameParts.hasSectionref
      ? fnameParts.linkpartReference
      : "");
  attr.file = file;
  return await createImageDiv(attr, false, internalEmbedEl);
};

function getDimensionsFromAliasString(data: string) {
  const dimensionRegex = /^(?<width>\d+%|\d+)(x(?<height>\d+%|\d+))?$/;
  const heightOnlyRegex = /^x(?<height>\d+%|\d+)$/;

  const match = data.match(dimensionRegex) || data.match(heightOnlyRegex);
  if (match) {
    const { width, height } = match.groups;

    // Ensure width and height do not start with '0'
    if (
      (width && width.startsWith("0") && width !== "0") ||
      (height && height.startsWith("0") && height !== "0")
    ) {
      return null;
    }

    return {
      width: width || undefined,
      height: height || undefined,
    };
  }

  // If the input starts with a 0 or is a decimal, return null
  if (/^0\d|^\d+\.\d+/.test(data)) {
    return null;
  }
  return null;
}

type AliasParts = {
  alias?: string;
  width?: string;
  height?: string;
  style?: string;
};
function parseAlias(input: string): AliasParts {
  const result: AliasParts = {};
  const parts = input.split("|").map((part) => part.trim());

  switch (parts.length) {
    case 1: {
      const singleMatch = getDimensionsFromAliasString(parts[0]);
      if (singleMatch) {
        return singleMatch; // Return dimensions if valid
      }
      result.style = parts[0]; // Otherwise, return as style
      break;
    }

    case 2: {
      const firstDim = getDimensionsFromAliasString(parts[0]);
      const secondDim = getDimensionsFromAliasString(parts[1]);

      if (secondDim) {
        result.alias = parts[0];
        result.width = secondDim.width;
        result.height = secondDim.height;
      } else if (firstDim) {
        result.width = firstDim.width;
        result.height = firstDim.height;
        result.style = parts[1]; // Second part is style
      } else {
        result.alias = parts[0];
        result.style = parts[1]; // Assuming second part is style
      }
      break;
    }

    case 3: {
      const middleMatch = getDimensionsFromAliasString(parts[1]);
      if (middleMatch) {
        result.alias = parts[0];
        result.width = middleMatch.width;
        result.height = middleMatch.height;
        result.style = parts[2];
      } else {
        result.alias = parts[0];
        result.style = parts[2]; // Last part is style
      }
      break;
    }

    default: {
      const secondValue = getDimensionsFromAliasString(parts[1]);
      if (secondValue) {
        result.alias = parts[0];
        result.width = secondValue.width;
        result.height = secondValue.height;
        result.style = parts[parts.length - 1]; // Last part is style
      } else {
        result.alias = parts[0];
        result.style = parts[parts.length - 1]; // Last part is style
      }
      break;
    }
  }

  // Clean up the result to remove undefined properties
  Object.keys(result).forEach((key: keyof AliasParts) => {
    if (result[key] === undefined) {
      delete result[key];
    }
  });

  return result;
}

const processAltText = (
  fname: string,
  alt: string,
  attr: imgElementAttributes,
) => {
  if (alt && !alt.startsWith(fname)) {
    const aliasParts = parseAlias(alt);
    attr.fwidth = aliasParts.width ?? attr.fwidth;
    attr.fheight = aliasParts.height ?? attr.fheight;
    if (aliasParts.style && !aliasParts.style.startsWith(fname)) {
      attr.imgstyle = [`excalidraw-svg${`-${aliasParts.style}`}`];
    }
  }
};

const isTextOnlyEmbed = (internalEmbedEl: Element): boolean => {
  const src = internalEmbedEl.getAttribute("src");
  if (!src) {
    return true;
  } //technically this does not mean this is a text only embed, but still should abort further processing
  const fnameParts = getEmbeddedFilenameParts(src);
  return (
    !(
      fnameParts.hasArearef ||
      fnameParts.hasGroupref ||
      fnameParts.hasFrameref ||
      fnameParts.hasClippedFrameref
    ) &&
    (fnameParts.hasBlockref || fnameParts.hasSectionref) &&
    fnameParts.blockref !== "as-image"
  );
};

const tmpObsidianWYSIWYG = async (
  el: HTMLElement,
  ctx: MarkdownPostProcessorContext,
  isPrinting: boolean,
  isMarkdownReadingMode: boolean,
  isHoverPopover: boolean,
) => {
  const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
  if (!(file instanceof TFile)) {
    return;
  }
  if (!plugin.isExcalidrawFile(file)) {
    return;
  }
  const safeFrontmatter = getSafeFrontmatter(ctx.frontmatter);
  if (safeFrontmatter["excalidraw-open-md"]) {
    return;
  }
  if (ctx.remainingNestLevel < 4) {
    return;
  }

  //internal-embed: Excalidraw is embedded into a markdown document
  //markdown-reading-view: we are processing the markdown reading view of an actual Excalidraw file
  //markdown-embed: we are processing the hover preview of a markdown file
  //alt, width, and height attributes of .internal-embed to size and style the image
  const containerEl = ctx.containerEl;

  if (
    !plugin.settings.renderImageInMarkdownReadingMode &&
    isMarkdownReadingMode  &&
    // https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2835
    !getParentOfClass(containerEl, "internal-embed") &&
    !getParentOfClass(containerEl, "markdown-embed")
  ) {
    return;
  }

  if (!plugin.settings.renderImageInMarkdownToPDF && isPrinting) {
    //containerEl.parentElement?.hasClass("print")) {
    return;
  }

  let internalEmbedDiv: HTMLElement = containerEl;
  while (
    !internalEmbedDiv.hasClass("print") &&
    !internalEmbedDiv.hasClass("dataview") &&
    !internalEmbedDiv.hasClass("cm-preview-code-block") &&
    !internalEmbedDiv.hasClass("cm-embed-block") &&
    !internalEmbedDiv.hasClass("internal-embed") &&
    !internalEmbedDiv.hasClass("markdown-reading-view") &&
    !internalEmbedDiv.hasClass("markdown-embed") &&
    internalEmbedDiv.parentElement
  ) {
    internalEmbedDiv = internalEmbedDiv.parentElement;
  }

  if (
    internalEmbedDiv.hasClass("dataview") ||
    internalEmbedDiv.hasClass("cm-preview-code-block") ||
    internalEmbedDiv.hasClass("cm-embed-block")
  ) {
    return; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/835
  }

  if (!plugin.settings.renderImageInHoverPreviewForMDNotes) {
    //const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover");
    const safeFrontmatter = getSafeFrontmatter(ctx.frontmatter);
    const shouldOpenMD = Boolean(safeFrontmatter["excalidraw-open-md"]);
    if (isHoverPopover && shouldOpenMD) {
      return;
    }
  }

  //const isPrinting = Boolean(internalEmbedDiv.hasClass("print"));

  const attr: imgElementAttributes = {
    fname: ctx.sourcePath,
    fheight: isPrinting ? "100%" : getDefaultHeight(plugin),
    fwidth: isPrinting ? "100%" : getDefaultWidth(plugin),
    imgstyle: ["excalidraw-svg"],
  };

  attr.file = file;

  const markdownEmbed = internalEmbedDiv.hasClass("markdown-embed");
  const markdownReadingView = isPrinting || isMarkdownReadingMode; //internalEmbedDiv.hasClass("markdown-reading-view")
  if (
    !internalEmbedDiv.hasClass("internal-embed") &&
    (markdownEmbed || markdownReadingView)
  ) {
    if (isPrinting) {
      internalEmbedDiv = containerEl;
    }
    //We are processing the markdown preview of an actual Excalidraw file
    //the excalidraw file in markdown preview mode
    const isFrontmatterDiv = Boolean(el.querySelector(".frontmatter"));
    let areaPreview = false;
    if (ctx.frontmatter) {
      el.empty();
    } else {
      //Obsidian changed this at some point from h3 to h5 and also the text...
      let warningEl = el.querySelector("div>*[data-heading^='Unable to find ");
      if (!warningEl) {
        //changed in Obsidian 1.8.9
        warningEl = el.querySelector("div > *[data-heading]");
      }
      if (warningEl) {
        const dataHeading = warningEl.getAttr("data-heading");
        const ref =
          dataHeading.match(
            /.+(\^(?:group=|area=|frame=|clippedframe=)[A-Za-z0-9_-]{8,21})/,
          )?.[1] ??
          // I am unsure if this works with all Obsidian translations,
          // thus I kept the first with the fix ID length and have the frame name as fallback
          dataHeading.match(
            /.+(\^(?:group=|area=|frame=|clippedframe=)[\p{L}\p{N}_ -]+)/u,
          )?.[1];
        if (ref) {
          attr.fname = `${file.path}#${ref}`;
          areaPreview = true;
        }
      }
    }
    if (!isFrontmatterDiv && !areaPreview) {
      if (el.parentElement === containerEl) {
        containerEl.removeChild(el);
      }
      return;
    }
    internalEmbedDiv.empty();
    const onCanvas = internalEmbedDiv.hasClass("canvas-node-content");

    await getGlobalTaskQueue(plugin).addTask(async () => {
      if (!internalEmbedDiv.isConnected) {
        return;
      }

      const imgDiv = await createImageDiv(attr, onCanvas, internalEmbedDiv);
      if (!imgDiv) {
        return;
      }

      // Check if the user navigated away while the image was generating
      if (!internalEmbedDiv.isConnected) {
        discardImage(imgDiv);
        return;
      }

      if (markdownEmbed) {
        //display image on canvas without markdown frame
        internalEmbedDiv.removeClass("markdown-embed");
        internalEmbedDiv.removeClass("inline-embed");
        internalEmbedDiv.addClass("media-embed");
        internalEmbedDiv.addClass("image-embed");
        if (
          !onCanvas &&
          (isInstanceOfHTMLElement(imgDiv.firstChild) ||
            isInstanceOfSVGElement(imgDiv.firstChild))
        ) {
          setStyle(imgDiv.firstChild, {
            maxHeight: "100%",
            maxWidth: null,
          });
        }
        // Resolve the cyclic size dependency by applying a CSS width and/or height
        if (
          !onCanvas &&
          isHoverPopover &&
          isInstanceOfHTMLImageElement(imgDiv.firstChild)
        ) {
          internalEmbedDiv.style.setProperty(
            "--popover-width",
            `${attr.fwidth}px`,
          );
          internalEmbedDiv.style.setProperty(
            "--popover-height",
            `${attr.fheight}px`,
          );
          setStyle(internalEmbedDiv, {
            width: "var(--popover-width)",
            height: "var(--popover-height)",
          });
        }
        internalEmbedDiv.appendChild(imgDiv.firstChild);
        return;
      }
      internalEmbedDiv.appendChild(imgDiv);
    });
    return;
  }

  if (isTextOnlyEmbed(internalEmbedDiv)) {
    //legacy reference to a block or section as text
    //should be embedded as legacy text
    return;
  }

  el.empty();

  if (internalEmbedDiv.hasAttribute("ready")) {
    return;
  }
  internalEmbedDiv.setAttribute("ready", "");

  internalEmbedDiv.empty();

  await getGlobalTaskQueue(plugin).addTask(async () => {
    if (!internalEmbedDiv.isConnected) {
      return;
    }

    const imgDiv = await processInternalEmbed(internalEmbedDiv, file);
    if (!imgDiv) {
      return;
    }

    // Abort insertion if the container is no longer connected
    if (!internalEmbedDiv.isConnected) {
      discardImage(imgDiv);
      return;
    }

    internalEmbedDiv.appendChild(imgDiv);
  });

  //timer to avoid the image flickering when the user is typing
  let timer: number = null;
  const markdownObserverFn: MutationCallback = (m) => {
    if (!["alt", "width", "height"].contains(m[0]?.attributeName)) {
      return;
    }
    if (timer) {
      window.clearTimeout(timer);
    }
    timer = window.setTimeout(() => {
      timer = null;
      internalEmbedDiv.empty();
      void getGlobalTaskQueue(plugin).addTask(async () => {
        if (!internalEmbedDiv.isConnected) {
          observer.disconnect();
          return;
        }

        const imgDiv = await processInternalEmbed(internalEmbedDiv, file);
        if (!imgDiv) {
          return;
        }

        if (internalEmbedDiv.isConnected) {
          internalEmbedDiv.appendChild(imgDiv);
        } else {
          discardImage(imgDiv);
        }
      });
    }, 500);
  };
  const observer = DEBUGGING
    ? new CustomMutationObserver(
        markdownObserverFn,
        "markdowPostProcessorObserverFn",
      )
    : new MutationObserver(markdownObserverFn);
  observer.observe(internalEmbedDiv, {
    attributes: true, //configure it to listen to attribute changes
  });
};

const docIDs = new Set<string>();
/**
 *
 * @param el
 * @param ctx
 */
export const markdownPostProcessor = async (
  el: HTMLElement,
  ctx: MarkdownPostProcessorContext,
) => {
  await plugin.awaitSettings();
  const isPrinting = Boolean(
    mainDocument.body.querySelectorAll("body > .print").length > 0,
  );
  //firstElementChild: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1956
  const isFrontmatter =
    el.hasClass("mod-frontmatter") ||
    el.firstElementChild?.hasClass("frontmatter") ||
    el.firstElementChild?.hasClass("block-language-yaml");
  if (isPrinting && isFrontmatter) {
    //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2184
    const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
    const isDrawing =
      file && file instanceof TFile && plugin.isExcalidrawFile(file);
    if (isDrawing) {
      return;
    }
  }
  const containerEl = ctx.containerEl;

  //check to see if we are rendering in editing mode or live preview
  //if yes, then there should be no .internal-embed containers
  const isMarkdownReadingMode = Boolean(
    containerEl && getParentOfClass(containerEl, "markdown-reading-view"),
  );
  const isHoverPopover = Boolean(
    containerEl && getParentOfClass(containerEl, "hover-popover"),
  );
  const safeFrontmatter = getSafeFrontmatter(ctx.frontmatter);
  const isPreview =
    isHoverPopover &&
    Boolean(safeFrontmatter["excalidraw-open-md"]) &&
    !plugin.settings.renderImageInHoverPreviewForMDNotes;
  const embeddedItems = el.querySelectorAll(".internal-embed");

  if (isPrinting && plugin.settings.renderImageInMarkdownToPDF) {
    await tmpObsidianWYSIWYG(
      el,
      ctx,
      isPrinting,
      isMarkdownReadingMode,
      isHoverPopover,
    );
    return;
  }

  if (!isPreview && embeddedItems.length === 0) {
    if (isFrontmatter) {
      docIDs.add(ctx.docId);
    } else {
      if (docIDs.has(ctx.docId) && !el.hasChildNodes()) {
        docIDs.delete(ctx.docId);
      }
      const isAreaGroupFrameRef =
        el.querySelectorAll("div > *[data-heading]").length === 1;
      if (!isAreaGroupFrameRef) {
        return;
      }
    }
    await tmpObsidianWYSIWYG(
      el,
      ctx,
      isPrinting,
      isMarkdownReadingMode,
      isHoverPopover,
    );
    return;
  }

  //If the file being processed is an excalidraw file,
  //then I want to hide all embedded items as these will be
  //transcluded text element or some other transcluded content inside the Excalidraw file
  //in reading mode these elements should be hidden
  const excalidrawFile = Boolean(safeFrontmatter["excalidraw-plugin"]);
  if (!(isPreview || isMarkdownReadingMode || isPrinting) && excalidrawFile) {
    setElementDisplay(el, "none");
    return;
  }

  await processReadingMode(embeddedItems, ctx);
};

/**
 * internal-link quick preview
 * @param e
 * @returns
 */
export const hoverEvent = (
  e: MouseEvent & { linktext?: string; sourcePath?: string },
) => {
  if (!e.linktext) {
    plugin.hover.linkText = null;
    return;
  }
  plugin.hover.linkText = e.linktext;
  plugin.hover.sourcePath = e.sourcePath;
};

//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
const legacyExcalidrawPopoverObserverFn: MutationCallback = (m) => {
  if (m.length === 0) {
    return;
  }
  if (!plugin.hover.linkText) {
    return;
  }
  if (!plugin.hover.linkText.endsWith("excalidraw")) {
    return;
  }
  const file = metadataCache.getFirstLinkpathDest(
    plugin.hover.linkText,
    plugin.hover.sourcePath ? plugin.hover.sourcePath : "",
  );
  if (!file) {
    return;
  }
  if (!(file instanceof TFile)) {
    return;
  }
  if (file.extension !== "excalidraw") {
    return;
  }

  const svgFileName = getIMGFilename(file.path, "svg");
  const svgFile = vault.getAbstractFileByPath(svgFileName);
  if (svgFile && svgFile instanceof TFile) {
    return;
  } //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview

  const pngFileName = getIMGFilename(file.path, "png");
  const pngFile = vault.getAbstractFileByPath(pngFileName);
  if (pngFile && pngFile instanceof TFile) {
    return;
  } //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview

  if (!plugin.hover.linkText) {
    return;
  }
  if (m.length !== 1) {
    return;
  }
  if (m[0].addedNodes.length !== 1) {
    return;
  }
  if (
    (m[0].addedNodes[0] as HTMLElement).className !== "popover hover-popover"
  ) {
    return;
  }
  const node = m[0].addedNodes[0];
  node.empty();

  //this div will be on top of original DIV. By stopping the propagation of the click
  //I prevent the default Obsidian feature of opening the link in the native app
  void getGlobalTaskQueue(plugin).addTask(async () => {
    if (!node.isConnected) {
      return;
    }

    const img = await getIMG(
      {
        file,
        fname: file.path,
        fwidth: "300",
        fheight: null,
        imgstyle: ["excalidraw-svg"],
      },
      false,
      node,
    );

    if (!img) {
      return;
    }

    if (!node.isConnected) {
      discardImage(img);
      return;
    }

    const div = createDiv("", (el) => {
      el.appendChild(img);
      el.setAttribute("src", file.path);
      el.onClickEvent((ev) => {
        ev.stopImmediatePropagation();
        const src = el.getAttribute("src");
        if (src) {
          plugin.openDrawing(
            vault.getFileByPath(src),
            linkClickModifierType(ev),
          );
        } //.ctrlKey||ev.metaKey);
      });
    });
    node.appendChild(div);
  });
};

export const legacyExcalidrawPopoverObserver = DEBUGGING
  ? new CustomMutationObserver(
      legacyExcalidrawPopoverObserverFn,
      "legacyExcalidrawPopoverObserverFn",
    )
  : new MutationObserver(legacyExcalidrawPopoverObserverFn);
