/** 
 ** About the various text fields of textElements
 ** rawText vs. text vs. original text
    text: The displyed text. This will have linebreaks if wrapped & will be the parsed text or the original-markup depending on Obsidian view mode
    originalText: this is the text without added linebreaks for wrapping. This will be parsed or markup depending on view mode
    rawText: text with original markdown markup and without the added linebreaks for wrapping
 */
import { App, Notice, TFile } from "obsidian";
import {
  nanoid,
  fileid,
  DEVICE,
  EMBEDDABLE_THEME_FRONTMATTER_VALUES,
  getLineHeight,
  ERROR_IFRAME_CONVERSION_CANCELED,
  JSON_parse,
  FRONTMATTER_KEYS,
  refreshTextDimensions,
  getContainerElement,
  loadSceneFonts,
} from "../constants/constants";
import ExcalidrawPlugin from "../core/main";
import { TextMode } from "./TextMode";
import type ExcalidrawView from "../view/ExcalidrawView";
import { addAppendUpdateCustomData } from "../utils/elementCustomDataUtils";
import {
  compress,
  decompress,
  getBinaryFileFromDataURL,
  getExportTheme,
  getLinkParts,
  hasExportTheme,
  isVersionNewerThanOther,
  LinkParts,
  updateFrontmatterInString,
  wrapTextAtCharLength,
  arrayToMap,
  compressAsync,
} from "../utils/sceneDataUtils";
import { isObsidianThemeDark } from "../utils/obsidianUtils";
import { cleanBlockRef, cleanSectionHeading } from "../utils/pathUtils";
import {
  ElementsMap,
  ExcalidrawElement,
  ExcalidrawImageElement,
  ExcalidrawTextElement,
  FileId,
} from "@zsviczian/excalidraw/types/element/src/types";
import {
  BinaryFiles,
  DataURL,
  SceneData,
} from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFile } from "./EmbeddedFileLoader";
import { MimeType } from "src/types/embeddedFileLoaderTypes";
import { MultiOptionConfirmationPrompt } from "./Dialogs/Prompt";
import {
  getMermaidImageElements,
  getMermaidText,
  shouldRenderMermaid,
} from "../utils/mermaidUtils";
import { Mutable } from "@zsviczian/excalidraw/types/common/src/utility-types";
import { updateElementIdsInScene } from "../utils/excalidrawSceneUtils";
import { importFileToVault } from "../utils/fileUtils";
import { t } from "../lang/helpers";
import { displayFontMessage } from "../utils/excalidrawViewUtils";
import { getPDFRect } from "../utils/PDFUtils";
import { URLs } from "src/constants/safeUrls";
import { errorlog } from "src/utils/coreUtils";

type SceneDataWithFiles = SceneData & { files: BinaryFiles };

type LegacyGridColor = NonNullable<
  NonNullable<SceneData["appState"]>["gridColor"]
> & {
  MajorGridFrequency?: number;
};

type ExcalidrawDataScene = SceneDataWithFiles & {
  type?: string;
  version?: number;
  source?: string;
  theme?: NonNullable<SceneData["appState"]>["theme"];
  prevTextMode?: TextMode;
  gridStep?: number;
  elements: Mutable<ExcalidrawElement>[];
  appState: Mutable<NonNullable<SceneData["appState"]>> & {
    previousGridSize?: number | null;
    gridColor?: LegacyGridColor;
  };
};

type RegExpMatchIteratorResult = IteratorResult<RegExpMatchArray, undefined>;

type MarkdownBlockNode = {
  type: string;
  id?: string;
  depth?: number;
  level?: number;
  value?: string;
  title?: string;
  data?: {
    hProperties?: {
      dataHeading?: string;
    };
  };
  children?: MarkdownBlockNode[];
  position: {
    start: { offset: number; line: number };
    end: { offset: number };
  };
};

type MarkdownBlockCacheEntry = {
  node: MarkdownBlockNode;
  display: string;
};

export enum AutoexportPreference {
  none,
  both,
  png,
  svg,
  inherit,
}

export const REGEX_TAGS = {
  // #[\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+
  //   1
  EXPR: /(#[\p{Letter}\p{Emoji_Presentation}\p{Number}/_-]+)/gu,
  getResList: (text: string): RegExpMatchIteratorResult[] => {
    const res = text.matchAll(REGEX_TAGS.EXPR);
    let parts: RegExpMatchIteratorResult;
    const resultList = [];
    while (!(parts = res.next()).done) {
      resultList.push(parts);
    }
    return resultList;
  },
  getTag: (parts: RegExpMatchIteratorResult): string => {
    return parts.value[1];
  },
  isTag: (parts: RegExpMatchIteratorResult): boolean => {
    return parts.value[1]?.startsWith("#");
  },
};

export const REGEX_LINK = {
  //![[link|alias]] [alias](link){num}
  //      1   2    3           4             5         67         8  9
  //EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(([^)]*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
  //      1   2    3           4             5         67                             8  9
  EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(((?:[^()]|\([^()]*\))*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1963

  getResList: (text: string): RegExpMatchIteratorResult[] => {
    const res = text.matchAll(REGEX_LINK.EXPR);
    let parts: RegExpMatchIteratorResult;
    const resultList = [];
    while (!(parts = res.next()).done) {
      resultList.push(parts);
    }
    return resultList;
  },
  getRes: (text: string): IterableIterator<RegExpMatchArray> => {
    return text.matchAll(REGEX_LINK.EXPR);
  },
  isTransclusion: (parts: RegExpMatchIteratorResult): boolean => {
    return !!parts.value[1];
  },
  getLink: (parts: RegExpMatchIteratorResult): string => {
    return parts.value[3] ? parts.value[3] : parts.value[6];
  },
  isWikiLink: (parts: RegExpMatchIteratorResult): boolean => {
    return !!parts.value[3];
  },
  getAliasOrLink: (parts: RegExpMatchIteratorResult): string => {
    return REGEX_LINK.isWikiLink(parts)
      ? parts.value[4]
        ? parts.value[4]
        : parts.value[3]
      : parts.value[5]
        ? parts.value[5]
        : parts.value[6];
  },
  getWrapLength: (
    parts: RegExpMatchIteratorResult,
    defaultWrap: number,
  ): number => {
    const len = parseInt(parts.value[8]);
    if (isNaN(len)) {
      return defaultWrap > 0 ? defaultWrap : null;
    }
    return len;
  },
};

//added \n at and of DRAWING_REG: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/357
const DRAWING_REG = /\n##? Drawing\n[^`]*(```json\n)([\s\S]*?)```\n/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_REG_FALLBACK = /\n##? Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
export const DRAWING_COMPRESSED_REG =
  /(\n##? Drawing\n[^`]*(?:```compressed-json\n))([\s\S]*?)(```\n)/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_COMPRESSED_REG_FALLBACK =
  /(\n##? Drawing\n(?:```compressed-json\n)?)(.*)((```)?(%%)?)/gm;
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;

const isCompressedMD = (data: string): boolean => {
  return data.match(/```compressed-json\n/gm) !== null;
};

const getDecompressedScene = (
  data: string,
): [string | null, RegExpMatchIteratorResult] => {
  let res = data.matchAll(DRAWING_COMPRESSED_REG);

  //In case the user adds a text element with the contents "# Drawing\n"
  let parts;
  parts = res.next();
  if (parts.done) {
    //did not find a match
    res = data.matchAll(DRAWING_COMPRESSED_REG_FALLBACK);
    parts = res.next();
  }
  if (parts.value && parts.value.length > 1) {
    return [decompress(parts.value[2]), parts];
  }
  return [null, parts];
};

export const changeThemeOfExcalidrawMD = (data: string): string => {
  const compressed = isCompressedMD(data);
  let scene = compressed ? getDecompressedScene(data)[0] : data;
  if (!scene) {
    return data;
  }
  if (isObsidianThemeDark) {
    if ((scene.match(/"theme"\s*:\s*"light"\s*,/g) || []).length === 1) {
      scene = scene.replace(/"theme"\s*:\s*"light"\s*,/, `"theme": "dark",`);
    }
  } else if ((scene.match(/"theme"\s*:\s*"dark"\s*,/g) || []).length === 1) {
    scene = scene.replace(/"theme"\s*:\s*"dark"\s*,/, `"theme": "light",`);
  }
  if (compressed) {
    return data.replace(DRAWING_COMPRESSED_REG, `$1${compress(scene)}$3`);
  }
  return scene;
};

export function getJSON(data: string): { scene: string; pos: number } {
  let res;
  if (isCompressedMD(data)) {
    const [result, parts] = getDecompressedScene(data);
    if (result) {
      return {
        scene: result.substring(0, result.lastIndexOf("}") + 1),
        pos: parts.value.index,
      }; //this is a workaround in case sync merges two files together and one version is still an old version without the ```codeblock
    }
    return { scene: data, pos: parts.value ? parts.value.index : 0 };
  }
  res = data.matchAll(DRAWING_REG);

  //In case the user adds a text element with the contents "# Drawing\n"
  let parts;
  parts = res.next();
  if (parts.done) {
    //did not find a match
    res = data.matchAll(DRAWING_REG_FALLBACK);
    parts = res.next();
  }
  if (parts.value && parts.value.length > 1) {
    const result = parts.value[2];
    return {
      scene: result.substring(0, result.lastIndexOf("}") + 1),
      pos: parts.value.index,
    }; //this is a workaround in case sync merges two files together and one version is still an old version without the ```codeblock
  }
  return { scene: data, pos: parts.value ? parts.value.index : 0 };
}

export async function getMarkdownDrawingSectionAsync(
  jsonString: string,
  compressed: boolean,
) {
  const result = compressed
    ? `## Drawing\n\x60\x60\x60compressed-json\n${await compressAsync(
        jsonString,
      )}\n\x60\x60\x60\n%%`
    : `## Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
  return result;
}

export function getMarkdownDrawingSection(
  jsonString: string,
  compressed: boolean,
): string {
  const result = compressed
    ? `## Drawing\n\x60\x60\x60compressed-json\n${compress(
        jsonString,
      )}\n\x60\x60\x60\n%%`
    : `## Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
  return result;
}

//WITHSECTION refers to back of the card note (see this.inputEl.onkeyup in SelectCard.ts)
const RE_EXCALIDRAWDATA_WITHSECTION_OK =
  /^(#\n+)%%\n+# Excalidraw Data(?:\n|$)/m;
const RE_EXCALIDRAWDATA_WITHSECTION_NOTOK =
  /#\n+%%\n+# Excalidraw Data(?:\n|$)/m;
const RE_EXCALIDRAWDATA_NOSECTION_OK = /^(%%\n+)?# Excalidraw Data(?:\n|$)/m;

//WITHSECTION refers to back of the card note (see this.inputEl.onkeyup in SelectCard.ts)
const RE_TEXTELEMENTS_WITHSECTION_OK = /^#\n+%%\n+##? Text Elements(?:\n|$)/m;
const RE_TEXTELEMENTS_WITHSECTION_NOTOK = /#\n+%%\n+##? Text Elements(?:\n|$)/m;
const RE_TEXTELEMENTS_NOSECTION_OK = /^(%%\n+)?##? Text Elements(?:\n|$)/m;

//The issue is that when editing in markdown embeds the user can delete the last enter causing two sections
//to collide. This is particularly problematic when the user is editing the last section before # Text Elements
const RE_EXCALIDRAWDATA_FALLBACK_1 = /(.*)%%\n+# Excalidraw Data(?:\n|$)/m;
const RE_EXCALIDRAWDATA_FALLBACK_2 = /(.*)# Excalidraw Data(?:\n|$)/m;

const RE_TEXTELEMENTS_FALLBACK_1 = /(.*)%%\n+##? Text Elements(?:\n|$)/m;
const RE_TEXTELEMENTS_FALLBACK_2 = /(.*)##? Text Elements(?:\n|$)/m;

const RE_DRAWING = /^(%%\n+)?##? Drawing\n/m;

export function getExcalidrawMarkdownHeader(data: string): {
  header: string;
  shouldFixTrailingHashtag: boolean;
  processingOk: boolean;
} {
  //The base case scenario is at the top, continued with fallbacks in order of likelihood and file structure
  //change history for sake of backward compatibility

  /* Expected markdown structure:
  bla bla bla
  #
  %%
  # Excalidraw Data
  */

  //trimming the json because in legacy excalidraw files the JSON was a single string resulting in very slow regexp parsing
  const drawingTrimLocation = data.search(RE_DRAWING);
  if (drawingTrimLocation > 0) {
    data = data.substring(0, drawingTrimLocation);
  }

  const m1 = data.match(RE_EXCALIDRAWDATA_WITHSECTION_OK);
  let trimLocation = m1?.index ?? -1; //data.search(RE_EXCALIDRAWDATA_WITHSECTION_OK);
  let shouldFixTrailingHashtag = false;
  if (trimLocation > 0) {
    trimLocation += m1[1].length; //accounts for the "(#\n\s*)" which I want to leave there untouched
  }

  /* Expected markdown structure (this happens when the user deletes the last empty line of the last back-of-the-card note):
  bla bla bla#
  %%
  # Excalidraw Data
  */
  if (trimLocation === -1) {
    trimLocation = data.search(RE_EXCALIDRAWDATA_WITHSECTION_NOTOK);
    if (trimLocation > 0) {
      shouldFixTrailingHashtag = true;
    }
  }
  /* Expected markdown structure
  a)
    bla bla bla
    %%
    # Excalidraw Data
  b)
    bla bla bla
    # Excalidraw Data
  */
  if (trimLocation === -1) {
    trimLocation = data.search(RE_EXCALIDRAWDATA_NOSECTION_OK);
  }
  /* Expected markdown structure:
  bla bla bla%%
  # Excalidraw Data
  */
  if (trimLocation === -1) {
    const res = data.match(RE_EXCALIDRAWDATA_FALLBACK_1);
    if (res && Boolean(res[1])) {
      trimLocation = res.index + res[1].length;
    }
  }
  /* Expected markdown structure:
  bla bla bla# Excalidraw Data
  */
  if (trimLocation === -1) {
    const res = data.match(RE_EXCALIDRAWDATA_FALLBACK_2);
    if (res && Boolean(res[1])) {
      trimLocation = res.index + res[1].length;
    }
  }
  /* Expected markdown structure:
  bla bla bla
  #
  %%
  # Text Elements
  */
  if (trimLocation === -1) {
    trimLocation = data.search(RE_TEXTELEMENTS_WITHSECTION_OK);
    if (trimLocation > 0) {
      trimLocation += 2; //accounts for the "#\n" which I want to leave there untouched
    }
  }
  /* Expected markdown structure:
  bla bla bla#
  %%
  # Text Elements
  */
  if (trimLocation === -1) {
    trimLocation = data.search(RE_TEXTELEMENTS_WITHSECTION_NOTOK);
    if (trimLocation > 0) {
      shouldFixTrailingHashtag = true;
    }
  }
  /* Expected markdown structure
  a)
    bla bla bla
    %%
    # Text Elements
  b)
    bla bla bla
    # Text Elements
  */
  if (trimLocation === -1) {
    trimLocation = data.search(RE_TEXTELEMENTS_NOSECTION_OK);
  }
  /* Expected markdown structure:
  bla bla bla%%
  # Text Elements
  */
  if (trimLocation === -1) {
    const res = data.match(RE_TEXTELEMENTS_FALLBACK_1);
    if (res && Boolean(res[1])) {
      trimLocation = res.index + res[1].length;
    }
  }
  /* Expected markdown structure:
  bla bla bla# Text Elements
  */
  if (trimLocation === -1) {
    const res = data.match(RE_TEXTELEMENTS_FALLBACK_2);
    if (res && Boolean(res[1])) {
      trimLocation = res.index + res[1].length;
    }
  }
  /* Expected markdown structure:
  a)
    bla bla bla
    # Drawing
  b)
    bla bla bla
    %%
    # Drawing
  */
  if (trimLocation === -1) {
    if (drawingTrimLocation > 0) {
      trimLocation = drawingTrimLocation;
    }
  }
  if (trimLocation === -1) {
    return {
      header: data.endsWith("\n") ? data : `${data}\n`,
      shouldFixTrailingHashtag,
      processingOk: false,
    };
  }

  return {
    header: data.substring(0, trimLocation),
    shouldFixTrailingHashtag,
    processingOk: true,
  };
}

export const getExcalidrawMarkdownHeaderSection = (
  data: string,
  keys?: [string, string][],
): string => {
  const { header, shouldFixTrailingHashtag, processingOk } =
    getExcalidrawMarkdownHeader(data);
  if (!processingOk) {
    return header;
  }

  const updatedHeader = updateFrontmatterInString(header, keys);
  //this should be removed at a later time. Left it here to remediate 1.4.9 mistake
  /*const REG_IMG = /(^---[\w\W]*?---\n)(!\[\[.*?]]\n(%%\n)?)/m; //(%%\n)? because of 1.4.8-beta... to be backward compatible with anyone who installed that version
  if (header.match(REG_IMG)) {
    header = header.replace(REG_IMG, "$1");
  }*/
  //end of remove
  return shouldFixTrailingHashtag
    ? `${updatedHeader}\n#\n`
    : updatedHeader.endsWith("\n")
      ? header
      : `${header}\n`;
};

export type EquationItem = { latex: string; isLoaded: boolean };
export type MermaidItem = { mermaid: string; isLoaded: boolean };

export class ExcalidrawData {
  public textElements: Map<
    string,
    { raw: string; parsed: string; hasTextLink: boolean }
  > = null;
  public scene: ExcalidrawDataScene = null;
  public deletedElements: ExcalidrawElement[] = [];
  public file: TFile = null;
  private app: App;
  private showLinkBrackets: boolean;
  private linkPrefix: string;
  public embeddableTheme: "light" | "dark" | "auto" | "default" = "auto";
  private urlPrefix: string;
  public autoexportPreference: AutoexportPreference =
    AutoexportPreference.inherit;
  private textMode: TextMode = TextMode.raw;
  public loaded: boolean = false;
  public elementLinks: Map<string, string> = null;
  public files: Map<FileId, EmbeddedFile> = null; //fileId, path
  private equations: Map<FileId, EquationItem> = null; //fileId, path
  private mermaids: Map<FileId, MermaidItem> = null; //fileId, path
  private compatibilityMode: boolean = false;
  private textElementCommentedOut: boolean = false;
  selectedElementIds: { [key: string]: boolean } = {}; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609

  constructor(
    private plugin: ExcalidrawPlugin,
    private view?: ExcalidrawView,
  ) {
    this.app = this.plugin.app;
    this.files = new Map<FileId, EmbeddedFile>();
    this.equations = new Map<FileId, EquationItem>();
    this.mermaids = new Map<FileId, MermaidItem>();
  }

  public destroy() {
    this.textElements = null;
    this.scene = null;
    this.deletedElements = [];
    this.file = null;
    this.app = null;
    this.showLinkBrackets = null;
    this.linkPrefix = null;
    this.embeddableTheme = null;
    this.urlPrefix = null;
    this.autoexportPreference = null;
    this.textMode = null;
    this.loaded = false;
    this.elementLinks = null;
    this.files = null;
    this.equations = null;
    this.mermaids = null;
    this.compatibilityMode = null;
    this.textElementCommentedOut = null;
    this.selectedElementIds = null;
  }

  /**
   * 1.5.4: for backward compatibility following the release of container bound text elements and the depreciation boundElementIds field
   */
  private initializeNonInitializedFields() {
    if (!this.scene || !this.scene.elements) {
      return;
    }

    const saveVersion =
      this.scene.source?.split(
        `${URLs.GITHUB_COM_ZSVICZIAN_OBSIDIAN_EXCALIDRAW_PLUGIN_RELEASES_TAG}/`,
      )[1] ?? "1.8.16";

    const elements = this.scene.elements as Mutable<
      ExcalidrawElement & { boundElementIds?: string[] }
    >[];
    for (const el of elements) {
      if (el.type === "iframe" && !el.customData) {
        //@ts-ignore -- retyping in ExcalidrawData is possible, this is not the live inmutable object
        el.type = "embeddable";
      }

      if (el.boundElements) {
        const map = new Map<string, "text" | "arrow">();
        let alreadyHasText: boolean = false;
        el.boundElements.forEach((item: { id: string; type: string }) => {
          if (item.type !== "text" && item.type !== "arrow") {
            return;
          }
          if (item.type === "text") {
            if (!alreadyHasText) {
              map.set(item.id, item.type);
              alreadyHasText = true;
            } else {
              const elementToClean = elements.find(
                (el: ExcalidrawElement) => el.id === item.id,
              );
              if (elementToClean) {
                //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1600
                if (elementToClean.type === "text") {
                  elementToClean.containerId = null;
                }
              }
            }
          } else {
            map.set(item.id, item.type);
          }
        });
        const boundElements = Array.from(map, ([id, type]) => ({ id, type }));
        if (boundElements.length !== el.boundElements.length) {
          el.boundElements = boundElements;
        }
      }

      //convert .boundElementIds to boundElements
      if (el.boundElementIds) {
        if (!el.boundElements) {
          el.boundElements = [];
        }
        el.boundElements = el.boundElements.concat(
          el.boundElementIds.map((id: string) => ({
            type: "arrow",
            id,
          })),
        );
        delete el.boundElementIds;
      }

      //add containerId to TextElements if missing
      if (el.type === "text" && !el.containerId) {
        el.containerId = null;
      }

      if (el.type === "text" && !el.originalText) {
        el.originalText = el.rawText ?? el.text ?? "";
      }

      //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/494
      if (el.x === null) {
        el.x = 0;
      }
      if (el.y === null) {
        el.y = 0;
      }
      /* FixedPointBinding type no longer has focus property. restoreElemenets should take care of this
      if (el.startBinding?.focus === null) {
        el.startBinding.focus = 0;
      }
      if (el.endBinding?.focus === null) {
        el.endBinding.focus = 0;
      }
      */

      //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/497
      if (el.type === "text" && el.fontSize === null) {
        el.fontSize = 20;
      }

      if (el.type === "text" && !Object.hasOwn(el ?? {}, "autoResize")) {
        el.autoResize = true;
      }

      if (el.type === "text" && !Object.hasOwn(el ?? {}, "lineHeight")) {
        el.lineHeight = getLineHeight(el.fontFamily) as number & {
          _brand: "unitlessLineHeight";
        };
      }

      if (el.type === "image" && !Object.hasOwn(el ?? {}, "roundness")) {
        el.roundness = null;
      }
    }

    //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/569
    try {
      //Fix text elements that point to a container, but the container does not point back
      const textElWithOneWayLinkToContainer = elements.filter(
        (
          textEl: Mutable<ExcalidrawElement>,
        ): textEl is Mutable<ExcalidrawTextElement> =>
          textEl.type === "text" &&
          textEl.containerId &&
          elements.some(
            (container: Mutable<ExcalidrawElement>) =>
              container.id === textEl.containerId &&
              container.boundElements.length > 0 &&
              container.boundElements.some(
                (boundEl: { id: string; type: string }) =>
                  boundEl.type === "text" &&
                  boundEl.id !== textEl.id &&
                  boundEl.id.length > 8,
              ),
          ),
      );
      //if(textElWithOneWayLinkToContainer.length>0) log({message: "cleanup", textElWithOneWayLinkToContainer});
      textElWithOneWayLinkToContainer.forEach(
        (textEl: Mutable<ExcalidrawTextElement>) => {
          try {
            const container = elements.filter(
              (container: Mutable<ExcalidrawElement>) =>
                container.id === textEl.containerId,
            )[0];
            const boundEl = container.boundElements.filter(
              (boundEl: { id: string; type: "text" | "arrow" }) =>
                !(
                  boundEl.type === "text" &&
                  !elements.some(
                    (el: Mutable<ExcalidrawElement>) => el.id === boundEl.id,
                  )
                ),
            );
            container.boundElements = [
              { id: textEl.id, type: "text" as "text" | "arrow" },
            ].concat(boundEl);
          } catch (e: unknown) {
            errorlog({
              message: "unexpected error in initializeNonInitializedFields",
              context: this.initializeNonInitializedFields.bind(
                this,
              ) as unknown,
              error: e,
            });
          }
        },
      );

      const ellipseAndRhombusContainerWrapping = !isVersionNewerThanOther(
        saveVersion,
        "1.8.16",
      );

      //Remove from bound elements references that do not exist in the scene
      const containers = elements.filter(
        (container: Mutable<ExcalidrawElement>) =>
          container.boundElements && container.boundElements.length > 0,
      );
      containers.forEach((container: Mutable<ExcalidrawElement>) => {
        if (
          ellipseAndRhombusContainerWrapping &&
          !container.customData?.legacyTextWrap
        ) {
          addAppendUpdateCustomData(container, { legacyTextWrap: true });
        }
        const filteredBoundElements = container.boundElements.filter(
          (boundEl: { id: string; type: string }) =>
            elements.some(
              (el: Mutable<ExcalidrawElement>) => el.id === boundEl.id,
            ),
        );
        if (filteredBoundElements.length !== container.boundElements.length) {
          //log({message: "cleanup",oldBound: container.boundElements, newBound: filteredBoundElements});
          container.boundElements = filteredBoundElements;
        }
      });

      //Clear the containerId for textElements if the referenced container does not exist in the scene
      elements
        .filter(
          (
            textEl: Mutable<ExcalidrawElement>,
          ): textEl is Mutable<ExcalidrawTextElement> =>
            textEl.type === "text" &&
            textEl.containerId &&
            !elements.some(
              (container: Mutable<ExcalidrawElement>) =>
                container.id === textEl.containerId,
            ),
        )
        .forEach((textEl: Mutable<ExcalidrawTextElement>) => {
          textEl.containerId = null;
        }); // log({message:"cleanup",textEl})});
    } catch (error: unknown) {
      errorlog({
        message: "unexpected error in loadData",
        error,
      });
    }
  }

  /**
   * Loads a new drawing
   * @param {TFile} file - the MD file containing the Excalidraw drawing
   * @returns {boolean} - true if file was loaded, false if there was an error
   */
  public async loadData(
    data: string,
    file: TFile,
    textMode: TextMode,
  ): Promise<boolean> {
    if (!file) {
      return false;
    }
    this.loaded = false;
    this.selectedElementIds = {};
    this.textElements = new Map<
      string,
      { raw: string; parsed: string; hasTextLink: boolean }
    >();
    this.elementLinks = new Map<string, string>();
    if (this.file !== file) {
      //this is a reload - files, equations and mermaids will take care of reloading when needed
      this.files.clear();
      this.equations.clear();
      this.mermaids.clear();
    }
    this.file = file;
    this.compatibilityMode = false;

    //I am storing these because if the settings change while a drawing is open parsing will run into errors during save
    //The drawing will use these values until next drawing is loaded or this drawing is re-loaded
    this.setShowLinkBrackets();
    this.setLinkPrefix();
    this.setUrlPrefix();
    this.setAutoexportPreferences();
    this.setembeddableThemePreference();

    this.scene = null;

    //In compatibility mode if the .excalidraw file was more recently updated than the .md file, then the .excalidraw file
    //should be loaded as the scene.
    //This feature is mostly likely only relevant to people who use Obsidian and Logseq on the same vault and edit .excalidraw
    //drawings in Logseq.
    if (this.plugin.settings.syncExcalidraw) {
      const excalfile = `${file.path.substring(
        0,
        file.path.lastIndexOf(".md"),
      )}.excalidraw`;
      const f = this.app.vault.getAbstractFileByPath(excalfile);
      if (f && f instanceof TFile && f.stat.mtime > file.stat.mtime) {
        //the .excalidraw file is newer then the .md file
        const d = await this.app.vault.read(f);
        this.scene = JSON.parse(d) as ExcalidrawDataScene;
      }
    }

    // https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/396
    let sceneJSONandPOS = null;
    const loadJSON = (): { scene: string; pos: number } => {
      //Load scene: Read the JSON string after "# Drawing"
      const sceneJSONandPOS = getJSON(data);
      if (sceneJSONandPOS.pos === -1) {
        throw new Error("Excalidraw JSON not found in the file");
      }
      if (!this.scene) {
        this.scene = JSON_parse(sceneJSONandPOS.scene); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
      }
      return sceneJSONandPOS;
    };
    sceneJSONandPOS = loadJSON();

    this.deletedElements = this.scene.elements.filter(
      (el: ExcalidrawElement) => el.isDeleted,
    );
    this.scene.elements = this.scene.elements.filter(
      (el: ExcalidrawElement) => !el.isDeleted,
    );

    //once off migration of legacy scenes
    if (
      this.scene?.elements?.some(
        (el: ExcalidrawElement) => el.type === "iframe" && !el.customData,
      )
    ) {
      const prompt = new MultiOptionConfirmationPrompt(
        this.plugin,
        "This file contains embedded frames " +
          "which will be migrated to a newer version for compatibility with " +
          `excalidraw.com.<br>🔄 If you're using Obsidian on ` +
          "multiple devices, you may proceed now, but please, before opening this " +
          "file on your other devices, update Excalidraw on those as well.<br>🔍 More info is available " +
          `<a href=${URLs.GITHUB_COM_ZSVICZIAN_OBSIDIAN_EXCALIDRAW_PLUGIN_RELEASES_TAG}/1.9.9>here</a>.<br>🌐 ` +
          `<a href=${URLs.TRANSLATE_GOOGLE_COM}>` +
          "Translate</a>.",
      );
      prompt.contentEl.focus();
      const confirmation = await prompt.waitForClose;
      if (!confirmation) {
        throw new Error(ERROR_IFRAME_CONVERSION_CANCELED);
      }
    }
    this.initializeNonInitializedFields();

    const timer = window.setTimeout(() => {
      const notice = new Notice(t("FONT_LOAD_SLOW"), 15000);
      notice.messageEl.oncontextmenu = () => {
        displayFontMessage(this.app);
      };
    }, 5000);
    await loadSceneFonts(this.scene.elements);
    window.clearTimeout(timer);

    if (!this.scene.files) {
      this.scene.files = {}; //loading legacy scenes that do not yet have the files attribute.
    }

    if (hasExportTheme(this.plugin, this.file)) {
      this.scene.appState.theme = getExportTheme(
        this.plugin,
        this.file,
        "light",
      );
    } else if (this.plugin.settings.matchThemeAlways) {
      this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
    }

    //girdSize, gridStep, previousGridSize, gridModeEnabled migration
    if (Object.hasOwn(this.scene.appState ?? {}, "previousGridSize")) {
      //if previousGridSize was present this is legacy data
      if (this.scene.appState.gridSize === null) {
        this.scene.appState.gridSize = this.scene.appState.previousGridSize;
        this.scene.appState.gridModeEnabled = false;
      } else {
        this.scene.appState.gridModeEnabled = true;
      }
      delete this.scene.appState.previousGridSize;
    }

    if (
      Object.hasOwn(this.scene.appState?.gridColor ?? {}, "MajorGridFrequency")
    ) {
      //if this is present, this is legacy data
      if (this.scene.appState.gridColor.MajorGridFrequency > 1) {
        this.scene.gridStep = this.scene.appState.gridColor.MajorGridFrequency;
      }
      delete this.scene.appState.gridColor.MajorGridFrequency;
    }

    data = data.substring(0, sceneJSONandPOS.pos);

    //The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the
    //link was updated due to filename changes
    //The .excalidraw JSON is modified to reflect the MD in case of difference
    //Read the text elements into the textElements Map
    let position = data.search(RE_EXCALIDRAWDATA_NOSECTION_OK);
    if (position === -1) {
      //resillience in case back of the note was saved right on top of text elements
      // # back of note section
      // ....# Excalidraw Data
      // ....
      // --------------
      // instead of
      // --------------
      // # back of note section
      // ....
      // # Excalidraw Data
      position = data.search(RE_EXCALIDRAWDATA_FALLBACK_2);
    }

    if (position === -1) {
      // # back of note section
      // ....
      // # Text Elements
      position = data.search(RE_TEXTELEMENTS_NOSECTION_OK);
    }

    if (position === -1) {
      //resillience in case back of the note was saved right on top of text elements
      // # back of note section
      // ....# Text Elements
      // ....
      // --------------
      // instead of
      // --------------
      // # back of note section
      // ....
      // # Text Elements
      position = data.search(RE_TEXTELEMENTS_FALLBACK_2);
    }
    if (position === -1) {
      await this.setTextMode(textMode);
      this.loaded = true;
      return true; //Text Elements header does not exist
    }
    data = data.slice(position);
    const normalMatch =
      data.match(
        /^((%%\n*)?# Excalidraw Data\n\n?## Text Elements(?:\n|$))/m,
      ) ?? data.match(/^((%%\n*)?##? Text Elements(?:\n|$))/m);

    const textElementsMatch = normalMatch
      ? normalMatch[0]
      : data.match(/(.*##? Text Elements(?:\n|$))/m)[0];

    data = data.slice(textElementsMatch.length);
    this.textElementCommentedOut = textElementsMatch.startsWith("%%\n");
    position = 0;
    let parts;

    //load element links
    const elementLinkMap = new Map<string, string>();
    const indexOfNewElementLinks = data.indexOf("## Element Links\n");
    const lengthOfNewElementLinks = 17; //`## Element Links\n`.length
    const indexOfOldElementLinks = data.indexOf("# Element Links\n");
    const lengthOfOldElementLinks = 16; //`# Element Links\n`.length
    const elementLinksData =
      indexOfNewElementLinks > -1
        ? data.substring(indexOfNewElementLinks + lengthOfNewElementLinks)
        : data.substring(indexOfOldElementLinks + lengthOfOldElementLinks);
    //Load Embedded files
    const RE_ELEMENT_LINKS = /^(.{8}):\s*(.*)$/gm;
    const linksRes = elementLinksData.matchAll(RE_ELEMENT_LINKS);
    while (!(parts = linksRes.next()).done) {
      elementLinkMap.set(parts.value[1], parts.value[2]);
    }

    //iterating through all the text elements in .md
    //Text elements always contain the raw value
    const BLOCKREF_LEN: number = 12; // " ^12345678\n\n".length;
    const RE_TEXT_ELEMENT_LINK =
      /^%%\*\*\*>>>text element-link:(\[\[[^<*\]]*]])<<<\*\*\*%%/gm;
    let res = data.matchAll(/\s\^(.{8})[\n]+/g);
    while (!(parts = res.next()).done) {
      let text = data.substring(position, parts.value.index);
      const id: string = parts.value[1];
      const textEl = this.scene.elements.filter(
        (el: ExcalidrawElement) => el.id === id,
      )[0];
      if (textEl) {
        if (textEl.type !== "text") {
          //markdown link attached to elements
          //legacy fileformat support as of 2.0.26
          if (textEl.link !== text) {
            textEl.link = text;
            textEl.version++;
            textEl.versionNonce++;
          }
          this.elementLinks.set(id, text);
        } else {
          //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
          const elementLinkRes = text.matchAll(RE_TEXT_ELEMENT_LINK);
          const elementLink = elementLinkRes.next();
          if (!elementLink.done) {
            text = text.replace(RE_TEXT_ELEMENT_LINK, "");
            textEl.link = elementLink.value[1];
          }
          if (elementLinkMap.has(id)) {
            textEl.link = elementLinkMap.get(id);
            elementLinkMap.delete(id);
          }
          const parseRes = await this.parse(text);
          textEl.rawText = text;
          this.textElements.set(id, {
            raw: text,
            parsed: parseRes.parsed,
            hasTextLink: !!parseRes.link,
          });
          if (parseRes.link && this.plugin.settings.syncElementLinkWithText) {
            textEl.link = parseRes.link;
          }
          textEl.hasTextLink = !!parseRes.link;
          //this will set the rawText field of text elements imported from files before 1.3.14, and from other instances of Excalidraw
          if (textEl && (!textEl.rawText || textEl.rawText === "")) {
            textEl.rawText = text;
          }
        }
      }
      position = parts.value.index + BLOCKREF_LEN;
    }

    //In theory only non-text elements should be left in the elementLinkMap
    //new file format from 2.0.26
    for (const [id, link] of elementLinkMap) {
      const textEl = this.scene.elements.filter(
        (el: ExcalidrawElement) => el.id === id,
      )[0];
      if (textEl) {
        textEl.link = link;
        textEl.version++;
        textEl.versionNonce++;
        this.elementLinks.set(id, link);
      }
    }

    const indexOfNewEmbeddedFiles = data.indexOf("## Embedded Files\n");
    const embeddedFilesNewLength = 18; //"## Embedded Files\n".length
    const indexOfOldEmbeddedFiles = data.indexOf("# Embedded files\n");
    const embeddedFilesOldLength = 17; //"# Embedded files\n".length

    if (indexOfNewEmbeddedFiles > -1 || indexOfOldEmbeddedFiles > -1) {
      data =
        indexOfNewEmbeddedFiles > -1
          ? data.substring(indexOfNewEmbeddedFiles + embeddedFilesNewLength)
          : data.substring(indexOfOldEmbeddedFiles + embeddedFilesOldLength);
      //Load Embedded files
      const REG_FILEID_FILEPATH =
        /([\w\d]*):\s*!?\[\[([^\]]*)]]\s*(\{[^}]*})?\n/gm;
      res = data.matchAll(REG_FILEID_FILEPATH);
      while (!(parts = res.next()).done) {
        const embeddedFile = new EmbeddedFile(
          this.plugin,
          this.file.path,
          parts.value[2],
          parts.value[3],
        );
        this.setFile(parts.value[1] as FileId, embeddedFile);
      }

      //Load links
      const REG_LINKID_FILEPATH =
        /([\w\d]*):\s*((?:https?|file|ftps?):\/\/[^\s]*)\n/gm;
      res = data.matchAll(REG_LINKID_FILEPATH);
      while (!(parts = res.next()).done) {
        const embeddedFile = new EmbeddedFile(
          this.plugin,
          null,
          parts.value[2],
        );
        this.setFile(parts.value[1] as FileId, embeddedFile);
      }

      //Load Equations
      const REG_FILEID_EQUATION = /([\w\d]*):\s*\$\$([\s\S]*?)(\$\$\s*\n)/gm;
      res = data.matchAll(REG_FILEID_EQUATION);
      while (!(parts = res.next()).done) {
        this.setEquation(parts.value[1] as FileId, {
          latex: parts.value[2],
          isLoaded: false,
        });
      }

      //Load Mermaids
      const mermaidElements = getMermaidImageElements(this.scene.elements);
      if (mermaidElements.length > 0 && !shouldRenderMermaid()) {
        new Notice(
          "Mermaid images are only supported in Obsidian 1.4.14 and above. Please update Obsidian to see the mermaid images in this drawing. Obsidian mobile 1.4.14 currently only avaiable to Obsidian insiders",
          5000,
        );
      } else {
        mermaidElements.forEach((el) =>
          this.setMermaid(el.fileId, {
            mermaid: getMermaidText(el),
            isLoaded: false,
          }),
        );
      }
    }
    //Check to see if there are text elements in the JSON that were missed from the # Text Elements section
    //e.g. if the entire text elements section was deleted.
    this.findNewTextElementsInScene();
    this.findNewElementLinksInScene(); //non-text element links
    await this.setTextMode(textMode);
    this.loaded = true;
    return true;
  }

  public async loadLegacyData(data: string, file: TFile): Promise<boolean> {
    if (!file) {
      return false;
    }
    this.loaded = false;
    this.selectedElementIds = {};
    this.compatibilityMode = true;
    this.file = file;
    this.textElements = new Map<
      string,
      { raw: string; parsed: string; hasTextLink: boolean }
    >();
    this.elementLinks = new Map<string, string>();
    this.setShowLinkBrackets();
    this.setLinkPrefix();
    this.setUrlPrefix();
    this.setembeddableThemePreference();
    this.scene = JSON.parse(data);
    if (!this.scene.files) {
      this.scene.files = {}; //loading legacy scenes without the files element
    }
    this.initializeNonInitializedFields();
    if (this.plugin.settings.matchThemeAlways) {
      this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
    }
    this.files.clear();
    this.equations.clear();
    this.mermaids.clear();
    this.findNewTextElementsInScene();
    this.findNewElementLinksInScene();
    await this.setTextMode(TextMode.raw); //legacy files are always displayed in raw mode.
    this.loaded = true;
    return true;
  }

  public async setTextMode(textMode: TextMode) {
    if (!this.scene) {
      return;
    }
    this.textMode = textMode;
    await this.updateSceneTextElements();
  }

  /**
   * Updates the TextElements in the Excalidraw scene based on textElements MAP in ExcalidrawData
   * Depending on textMode, TextElements will receive their raw or parsed values
   * @param forceupdate : will update text elements even if text contents has not changed, this will
   * correct sizing issues
   */
  private async updateSceneTextElements() {
    //update text in scene based on textElements Map
    //first get scene text elements
    const elementsMap = arrayToMap(this.scene.elements) as ElementsMap;
    const texts = this.scene.elements?.filter(
      (el: ExcalidrawElement) => el.type === "text" && !el.isDeleted,
    ) as Mutable<ExcalidrawTextElement>[];
    for (const te of texts) {
      const container = getContainerElement(te, elementsMap);
      const originalText =
        (await this.getText(te.id)) ?? te.originalText ?? te.text;
      const { text, x, y, width, height } = refreshTextDimensions(
        te,
        container,
        elementsMap,
        originalText,
      );
      try {
        //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1062
        te.originalText = originalText;
        te.text = text;
        te.x = x;
        te.y = y;
        te.width = width;
        te.height = height;
      } catch (error) {
        console.log(
          "error updating text element dimensions, likely due to element being deleted meanwhile",
          "updateSceneTextElements",
          error,
        );
      }
    }
  }

  private async getText(id: string): Promise<string> {
    const text = this.textElements.get(id);
    if (!text) {
      return null;
    }
    if (this.textMode === TextMode.parsed) {
      if (!text.parsed) {
        const parseRes = await this.parse(text.raw);
        this.textElements.set(id, {
          raw: text.raw,
          parsed: parseRes.parsed,
          hasTextLink: !!parseRes.link,
        });
      }
      //console.log("parsed",this.textElements.get(id).parsed);
      return text.parsed;
    }
    //console.log("raw",this.textElements.get(id).raw);
    return text.raw;
  }

  private findNewElementLinksInScene(): boolean {
    let result = false;
    const elements = this.scene.elements?.filter((el: ExcalidrawElement) => {
      return el.type !== "text" && el.link && !this.elementLinks.has(el.id);
    });
    if (elements.length === 0) {
      return result;
    }

    let id: string; //will be used to hold the new 8 char long ID for textelements that don't yet appear under # Text Elements

    for (const el of elements) {
      id = el.id;
      //replacing Excalidraw element IDs with my own nanoid, because default IDs may contain
      //characters not recognized by Obsidian block references
      //also Excalidraw IDs are inconveniently long
      if (el.id.length > 8) {
        result = true;
        id = nanoid();
        updateElementIdsInScene(this.scene, el, id);
      }
      this.elementLinks.set(id, el.link);
    }
    return result;
  }

  /**
   * check for textElements in Scene missing from textElements Map
   * @returns {boolean} - true if there were changes
   */
  private findNewTextElementsInScene(
    selectedElementIds: { [key: string]: boolean } = {},
  ): boolean {
    //console.log("Excalidraw.Data.findNewTextElementsInScene()");
    //get scene text elements
    this.selectedElementIds = selectedElementIds;
    const texts = this.scene.elements?.filter(
      (el: ExcalidrawElement) => el.type === "text",
    );

    let dirty: boolean = false; //to keep track if the json has changed
    let id: string; //will be used to hold the new 8 char long ID for textelements that don't yet appear under # Text Elements
    for (const te of texts) {
      id = te.id;
      //replacing Excalidraw text IDs with my own nanoid, because default IDs may contain
      //characters not recognized by Obsidian block references
      //also Excalidraw IDs are inconveniently long
      if (te.id.length > 8) {
        dirty = true;
        id = nanoid();
        if (this.selectedElementIds[te.id]) {
          //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609
          delete this.selectedElementIds[te.id];
          this.selectedElementIds[id] = true;
        }
        updateElementIdsInScene(this.scene, te, id);
        if (this.textElements.has(te.id)) {
          //element was created with onBeforeTextSubmit
          const text = this.textElements.get(te.id);
          this.textElements.set(id, {
            raw: text.raw,
            parsed: text.parsed,
            hasTextLink: text.hasTextLink,
          });
          this.textElements.delete(te.id); //delete the old ID from the Map
        }
        if (!this.textElements.has(id)) {
          const raw = te.rawText && te.rawText !== "" ? te.rawText : te.text; //this is for compatibility with drawings created before the rawText change on ExcalidrawTextElement
          this.textElements.set(id, { raw, parsed: null, hasTextLink: false });
          void this.parseasync(id, raw);
        }
      } else if (!this.textElements.has(te.id)) {
        const raw = te.rawText && te.rawText !== "" ? te.rawText : te.text; //this is for compatibility with drawings created before the rawText change on ExcalidrawTextElement
        this.textElements.set(id, { raw, parsed: null, hasTextLink: false });
        void this.parseasync(id, raw);
      }
    }
    return dirty;
  }

  private updateElementLinksFromScene() {
    for (const key of this.elementLinks.keys()) {
      //find element in the scene
      const el = this.scene.elements?.filter(
        (el: ExcalidrawElement) =>
          el.type !== "text" && el.id === key && el.link, //&&
      );
      if (el.length === 0) {
        this.elementLinks.delete(key); //if no longer in the scene, delete the text element
      } else {
        this.elementLinks.set(key, el[0].link);
      }
    }
  }

  /**
   * update text element map by deleting entries that are no long in the scene
   * and updating the textElement map based on the text updated in the scene
   */
  private async updateTextElementsFromScene() {
    for (const key of this.textElements.keys()) {
      //find text element in the scene
      const el = this.scene.elements?.filter(
        (el: ExcalidrawElement) => el.type === "text" && el.id === key,
      );
      if (el.length === 0) {
        this.textElements.delete(key); //if no longer in the scene, delete the text element
      } else {
        const text = await this.getText(key);
        const raw =
          this.scene.prevTextMode === TextMode.parsed
            ? el[0].rawText
            : (el[0].originalText ?? el[0].text);
        if (text !== (el[0].originalText ?? el[0].text)) {
          const parseRes = await this.parse(text);
          this.textElements.set(key, {
            raw,
            parsed: parseRes.parsed,
            hasTextLink: !!parseRes.link,
          });
        }
      }
    }
  }

  private async parseasync(key: string, raw: string) {
    const parseRes = await this.parse(raw);
    this.textElements.set(key, {
      raw,
      parsed: parseRes.parsed,
      hasTextLink: !!parseRes.link,
    });
  }

  private parseLinks(
    text: string,
    position: number,
    parts: RegExpMatchIteratorResult,
  ): string {
    return (
      text.substring(position, parts.value.index) +
      (this.showLinkBrackets ? "[[" : "") +
      REGEX_LINK.getAliasOrLink(parts) +
      (this.showLinkBrackets ? "]]" : "")
    );
  }

  /**
   *
   * @param text
   * @returns [string,number] - the transcluded text, and the line number for the location of the text
   */
  public async getTransclusion(
    link: string,
  ): Promise<{ contents: string; lineNum: number }> {
    const linkParts = getLinkParts(link, this.file);
    const file = this.app.metadataCache.getFirstLinkpathDest(
      linkParts.path,
      this.file.path,
    );
    return await getTransclusion(
      linkParts,
      this.app,
      file,
      this.plugin.settings.pageTransclusionCharLimit,
    );
  }

  /**
   * Process aliases and block embeds
   * @param text
   * @returns
   */
  private async parse(text: string): Promise<{ parsed: string; link: string }> {
    text = this.parseCheckbox(text);
    let outString = "";
    let link = null;
    let position = 0;
    const res = REGEX_LINK.getRes(text);
    let linkIcon = false;
    let urlIcon = false;
    let parts;
    if (text.match(REG_LINKINDEX_HYPERLINK)) {
      link = text;
      urlIcon = true;
    }
    while (!(parts = res.next()).done) {
      if (!link) {
        const l = REGEX_LINK.getLink(parts);
        if (l.match(REG_LINKINDEX_HYPERLINK)) {
          link = l;
        } else {
          link = `[[${l}]]`;
        }
      }
      if (REGEX_LINK.isTransclusion(parts)) {
        //transclusion //parts.value[1] || parts.value[4]
        let contents = this.parseCheckbox(
          (await this.getTransclusion(REGEX_LINK.getLink(parts))).contents,
        ).replaceAll(/%%[^%]*%%/gm, ""); //remove comments, consequence of https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
        if (this.plugin.settings.removeTransclusionQuoteSigns) {
          //remove leading > signs from transcluded quotations; the first > sign is not explicitlyl removed becuse
          //Obsidian app.metadataCache.blockCache returns the block position already discarding the first '> '
          contents = contents.replaceAll(/\n\s*>\s?/gm, "\n");
        }
        outString +=
          text.substring(position, parts.value.index) +
          wrapTextAtCharLength(
            contents,
            REGEX_LINK.getWrapLength(
              parts,
              this.plugin.settings.wordWrappingDefault,
            ),
            this.plugin.settings.forceWrap,
          );
      } else {
        const parsedLink = this.parseLinks(text, position, parts);
        if (parsedLink) {
          outString += parsedLink;
          if (!(urlIcon || linkIcon)) {
            const linkText = REGEX_LINK.getLink(parts);
            if (linkText.match(REG_LINKINDEX_HYPERLINK)) {
              urlIcon = !linkText.startsWith("cmd://"); //don't display the url icon for cmd:// links
            } else {
              linkIcon = true;
            }
          }
        }
      }
      position = parts.value.index + parts.value[0].length;
    }
    outString += text.substring(position, text.length);
    outString = this.unescapeSquareBrackets(outString);
    if (linkIcon) {
      outString = this.linkPrefix + outString;
    }
    if (urlIcon) {
      outString = this.urlPrefix + outString;
    }

    return { parsed: outString, link };
  }

  /**
   * Public wrapper around the internal text parser.
   *
   * This reuses existing ExcalidrawData parsing behavior, including
   * transclusions, link formatting, and prefix handling.
   * @param text Raw text to parse.
   * @returns Parsed text string.
   */
  public async parseText(text: string): Promise<string> {
    if (!text) {
      return;
    }
    const res = await this.parse(text);
    return res.parsed;
  }

  private parseCheckbox(text: string): string {
    return this.plugin.settings.parseTODO
      ? text
          .replaceAll(/^- \[\s] /g, `${this.plugin.settings.todo} `)
          .replaceAll(/\n- \[\s] /g, `\n${this.plugin.settings.todo} `)
          .replaceAll(/^- \[[^\s]] /g, `${this.plugin.settings.done} `)
          .replaceAll(/\n- \[[^\s]] /g, `\n${this.plugin.settings.done} `)
      : text;
  }

  private unescapeSquareBrackets(text: string): string {
    return text?.replaceAll(/\\\[/g, "[") ?? text;
  }

  /**
   * Does a quick parse of the raw text. Returns the parsed string if raw text does not include a transclusion.
   * Return null if raw text includes a transclusion.
   * This is implemented in a separate function, because by nature resolving a transclusion is an asynchronious
   * activity. Quick parse gets the job done synchronously if possible.
   * @param text
   */
  private quickParse(text: string): [string, string] {
    const hasTransclusion = (text: string): boolean => {
      const res = REGEX_LINK.getRes(text);
      let parts;
      while (!(parts = res.next()).done) {
        if (REGEX_LINK.isTransclusion(parts)) {
          return true;
        }
      }
      return false;
    };
    if (hasTransclusion(text)) {
      return [null, null];
    }
    text = this.parseCheckbox(text);
    let outString = "";
    let link = null;
    let position = 0;
    const res = REGEX_LINK.getRes(text);
    let linkIcon = false;
    let urlIcon = false;
    let parts;
    if (text.match(REG_LINKINDEX_HYPERLINK)) {
      link = text;
      urlIcon = true;
    }
    while (!(parts = res.next()).done) {
      if (!link) {
        const l = REGEX_LINK.getLink(parts);
        if (l.match(REG_LINKINDEX_HYPERLINK)) {
          link = l;
        } else {
          link = `[[${l}]]`;
        }
      }
      const parsedLink = this.parseLinks(text, position, parts);
      if (parsedLink) {
        outString += parsedLink;
        if (!(urlIcon || linkIcon)) {
          const linkText = REGEX_LINK.getLink(parts);
          if (linkText.match(REG_LINKINDEX_HYPERLINK)) {
            urlIcon = !linkText.startsWith("cmd://"); //don't display the url icon for cmd:// links
          } else {
            linkIcon = true;
          }
        }
      }
      position = parts.value.index + parts.value[0].length;
    }
    outString += text.substring(position, text.length);
    outString = this.unescapeSquareBrackets(outString);
    if (linkIcon) {
      outString = this.linkPrefix + outString;
    }
    if (urlIcon) {
      outString = this.urlPrefix + outString;
    }
    return [outString, link];
  }

  /**
   * Generate markdown file representation of excalidraw drawing
   * @returns markdown string
   */
  disableCompression: boolean = false;
  generateMDBase(deletedElements: ExcalidrawElement[] = []) {
    const syncTextLinks = this.plugin.settings.syncElementLinkWithText;
    let outString = this.textElementCommentedOut ? "%%\n" : "";
    outString += `# Excalidraw Data\n\n## Text Elements\n`;
    if (this.plugin.settings.addDummyTextElement) {
      outString += `\n^_dummy!_\n\n`;
    }
    const textElementLinks = new Map<string, string>();
    for (const key of this.textElements.keys()) {
      //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
      const element = this.scene.elements.filter(
        (el: ExcalidrawElement) => el.id === key,
      );
      const elementString = this.textElements.get(key).raw;
      if (
        element &&
        element.length === 1 &&
        element[0].link &&
        (!syncTextLinks || element[0].rawText === element[0].originalText)
      ) {
        //if(element[0].link.match(/^\[\[[^\]]*]]$/g)) { //apply this only to markdown links
        textElementLinks.set(key, element[0].link);
        //elementString = `%%***>>>text element-link:${element[0].link}<<<***%%` + elementString;
        //}
      }
      outString += `${elementString} ^${key}\n\n`;
    }

    if (this.elementLinks.size > 0 || textElementLinks.size > 0) {
      outString += `## Element Links\n`;
      for (const key of this.elementLinks.keys()) {
        outString += `${key}: ${this.elementLinks.get(key)}\n\n`;
      }
      for (const key of textElementLinks.keys()) {
        outString += `${key}: ${textElementLinks.get(key)}\n\n`;
      }
    }

    // deliberately not adding mermaids to here. It is enough to have the mermaidText in the image element's customData
    outString +=
      this.equations.size > 0 || this.files.size > 0
        ? "## Embedded Files\n"
        : "";
    if (this.equations.size > 0) {
      for (const key of this.equations.keys()) {
        outString += `${key}: $$${this.equations.get(key).latex.trim()}$$\n\n`;
      }
    }
    if (this.files.size > 0) {
      for (const key of this.files.keys()) {
        const PATHREG = /(^[^#|]*)/;
        const ef = this.files.get(key);
        if (ef.isHyperLink || ef.isLocalLink) {
          outString += `${key}: ${ef.hyperlink}\n\n`;
        } else {
          //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/829
          const path = ef.file
            ? ef.linkParts.original.replace(
                PATHREG,
                this.app.metadataCache.fileToLinktext(ef.file, this.file.path),
              )
            : ef.linkParts.original;
          const colorMap = ef.colorMap ? ` ${JSON.stringify(ef.colorMap)}` : "";
          outString += `${key}: [[${path}]]${colorMap}\n\n`;
        }
      }
    }
    //outString += this.equations.size > 0 || this.files.size > 0 ? "\n" : "";

    const sceneJSONstring = JSON.stringify(
      {
        type: this.scene.type,
        version: this.scene.version,
        source: this.scene.source,
        elements: this.scene.elements.concat(deletedElements),
        appState: this.scene.appState,
        files: this.scene.files,
      },
      null,
      "\t",
    );
    return { outString, sceneJSONstring };
  }

  async generateMDAsync(
    deletedElements: ExcalidrawElement[] = [],
  ): Promise<string> {
    const { outString, sceneJSONstring } = this.generateMDBase(deletedElements);
    const result =
      outString +
      (this.textElementCommentedOut ? "" : "%%\n") +
      (await getMarkdownDrawingSectionAsync(
        sceneJSONstring,
        this.disableCompression ? false : this.plugin.settings.compress,
      ));
    return result;
  }

  generateMDSync(deletedElements: ExcalidrawElement[] = []): string {
    const { outString, sceneJSONstring } = this.generateMDBase(deletedElements);
    const result =
      outString +
      (this.textElementCommentedOut ? "" : "%%\n") +
      getMarkdownDrawingSection(
        sceneJSONstring,
        this.disableCompression ? false : this.plugin.settings.compress,
      );
    return result;
  }

  public async saveDataURLtoVault(
    dataURL: DataURL,
    mimeType: MimeType,
    key: FileId,
    name?: string,
  ) {
    const scene = this.scene;
    let fname = name;

    if (!fname) {
      fname = `Pasted Image ${window.moment().format("YYYYMMDDHHmmss_SSS")}`;

      switch (mimeType) {
        case "image/png":
          fname += ".png";
          break;
        case "image/jpeg":
          fname += ".jpg";
          break;
        case "image/svg+xml":
          fname += ".svg";
          break;
        case "image/gif":
          fname += ".gif";
          break;
        default:
          fname += ".png";
      }
    }

    const arrayBuffer = await getBinaryFileFromDataURL(dataURL);
    if (!arrayBuffer) {
      return null;
    }

    const file = await importFileToVault(
      this.app,
      fname,
      arrayBuffer,
      this.file,
      this.view,
    );

    const embeddedFile = new EmbeddedFile(
      this.plugin,
      this.file.path,
      file.path,
    );

    embeddedFile.setImage({
      imgBase64: dataURL,
      mimeType,
      size: { height: 0, width: 0 },
      isDark: scene.appState?.theme === "dark",
      isSVGwithBitmap: mimeType === "image/svg+xml", //this treat all SVGs as if they had embedded images REF:addIMAGE
    });
    this.setFile(key, embeddedFile);
    return file;
  }

  private syncCroppedPDFs() {
    const scene = this.scene;
    const pdfScale = this.plugin.settings.pdfScale;
    scene.elements
      .filter((el) => el.type === "image" && el.crop && !el.isDeleted)
      .forEach((el: Mutable<ExcalidrawImageElement>) => {
        const ef = this.getFile(el.fileId);
        if (!ef.file) {
          return;
        }
        if (ef.file.extension !== "pdf") {
          return;
        }
        const pageRef = ef.linkParts.original.split("#")?.[1];
        if (
          !pageRef ||
          !pageRef.startsWith("page=") ||
          pageRef.includes("rect")
        ) {
          return;
        }
        const restOfLink = el.link
          ? el.link.match(/&rect=\d*,\d*,\d*,\d*(.*)/)?.[1]
          : "";
        const link =
          ef.linkParts.original +
          getPDFRect({
            elCrop: el.crop,
            scale: pdfScale,
            customData: el.customData,
          }) +
          (restOfLink ? restOfLink : "]]");
        el.link = `[[${link}`;
        this.elementLinks.set(el.id, el.link);
      });
  }

  /**
   * deletes fileIds from Excalidraw data for files no longer in the scene
   * @returns
   */
  private async syncFiles(): Promise<boolean> {
    let dirty = false;
    const scene = this.scene;

    //remove files and equations that no longer have a corresponding image element
    const images = scene.elements.filter(
      (e) => e.type === "image",
    ) as ExcalidrawImageElement[];
    const fileIds = images.map((e) => e.fileId);
    this.files.forEach((value, key) => {
      if (!fileIds.contains(key)) {
        this.files.delete(key);
        dirty = true;
      }
    });

    this.equations.forEach((value, key) => {
      if (!fileIds.contains(key)) {
        this.equations.delete(key);
        dirty = true;
      }
    });

    this.mermaids.forEach((value, key) => {
      if (!fileIds.contains(key)) {
        this.mermaids.delete(key);
        dirty = true;
      }
    });

    //check if there are any images that need to be processed in the new scene
    if (!scene.files || Object.keys(scene.files).length === 0) {
      return false;
    }

    //assing new fileId to duplicate equation and markdown files
    //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/601
    //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/593
    //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/297
    const processedIds = new Set<string>();
    fileIds.forEach((fileId, idx) => {
      if (processedIds.has(fileId)) {
        const embeddedFile = this.getFile(fileId);
        const equation = this.getEquation(fileId);
        const mermaid = this.getMermaid(fileId);

        //images should have a single reference, but equations, and markdown embeds should have as many as instances of the file in the scene
        if (
          embeddedFile &&
          (embeddedFile.isHyperLink ||
            embeddedFile.isLocalLink ||
            (embeddedFile.file &&
              (embeddedFile.file.extension !== "md" ||
                this.plugin.isExcalidrawFile(embeddedFile.file))))
        ) {
          return;
        }
        if (mermaid) {
          return;
        }

        if (getMermaidText(images[idx])) {
          this.setMermaid(fileId, {
            mermaid: getMermaidText(images[idx]),
            isLoaded: true,
          });
          return;
        }

        if (!embeddedFile && !equation && !mermaid) {
          //processing freshly pasted images from likely anotehr instance of excalidraw (e.g. Excalidraw.com, or another Obsidian instance)
          return;
        }

        const newId = fileid();
        (
          scene.elements
            .filter((el: ExcalidrawImageElement) => el.fileId === fileId)
            .sort((a, b) =>
              a.updated < b.updated ? 1 : -1,
            )[0] as Mutable<ExcalidrawImageElement>
        ).fileId = newId as FileId;
        dirty = true;
        processedIds.add(newId);
        if (embeddedFile) {
          this.setFile(
            newId as FileId,
            new EmbeddedFile(
              this.plugin,
              this.file.path,
              embeddedFile.linkParts.original,
            ),
          );
        }
        if (equation) {
          this.setEquation(newId as FileId, {
            latex: equation.latex,
            isLoaded: false,
          });
        }
      }
      processedIds.add(fileId);
    });

    for (const key of Object.keys(scene.files)) {
      const fileData = scene.files[key] as (typeof scene.files)[string] & {
        name?: string;
      };
      const mermaidElements = getMermaidImageElements(
        scene.elements.filter(
          (el: ExcalidrawImageElement) => el.fileId === key,
        ),
      );
      if (
        !(
          this.hasFile(key as FileId) ||
          this.hasEquation(key as FileId) ||
          this.hasMermaid(key as FileId) ||
          mermaidElements.length > 0
        )
      ) {
        dirty = true;
        await this.saveDataURLtoVault(
          fileData.dataURL,
          fileData.mimeType,
          key as FileId,
          fileData.name,
        );
      }
    }

    return dirty;
  }

  public async syncElements(
    newScene: SceneDataWithFiles,
    selectedElementIds?: { [key: string]: boolean },
  ): Promise<boolean> {
    this.scene = newScene as ExcalidrawDataScene;
    let result = false;
    if (!this.compatibilityMode) {
      result = await this.syncFiles();
      this.scene.files = {}; //files contains the dataURLs of files. Once synced these are all saved to disk
    }
    this.updateElementLinksFromScene();
    result =
      result ||
      this.syncCroppedPDFs() ||
      this.setLinkPrefix() ||
      this.setUrlPrefix() ||
      this.setShowLinkBrackets() ||
      this.findNewElementLinksInScene();
    await this.updateTextElementsFromScene();
    return result || this.findNewTextElementsInScene(selectedElementIds);
  }

  public async updateScene(newScene: string) {
    //console.log("Excalidraw.Data.updateScene()");
    this.scene = JSON_parse(newScene);
    this.updateElementLinksFromScene();
    const result =
      this.setLinkPrefix() ||
      this.setUrlPrefix() ||
      this.setShowLinkBrackets() ||
      this.setembeddableThemePreference() ||
      this.findNewElementLinksInScene();
    await this.updateTextElementsFromScene();
    if (result || this.findNewTextElementsInScene()) {
      await this.updateSceneTextElements();
      return true;
    }
    return false;
  }

  public getRawText(id: string) {
    return this.textElements.get(id)?.raw;
  }

  /**
   * returns parsed text with the correct line length
   * @param id
   * @returns
   */
  public getParsedText(id: string): string {
    const t = this.textElements.get(id);
    if (!t) {
      return null;
    }
    return t.parsed;
  }

  public getParsedResult(id: string) {
    const t = this.textElements.get(id);
    if (!t) {
      return null;
    }
    return t;
  }

  /**
   * Attempts to quickparse (sycnhronously) the raw text.
   *
   * If successful:
   *   - it will set the textElements cache with the parsed result, and
   *   - return the parsed result as an array of 3 values: [parsedTextWrapped, parsedText, link]
   *
   * If the text contains a transclusion:
   *   - it will initiate the async parse, and
   *   - it will return [null,null,null].
   * @param elementID
   * @param rawText
   * @param rawOriginalText
   * @param updateSceneCallback
   * @returns [parseResultOriginal: string, link: string]
   */
  public setTextElement(
    elementID: string,
    rawOriginalText: string,
    updateSceneCallback: (parsedText: string) => void,
  ): [parseResultOriginal: string, link: string] {
    //const maxLineLen = estimateMaxLineLen(rawText, rawOriginalText);
    const [parseResult, link] = this.quickParse(rawOriginalText); //will return the parsed result if raw text does not include transclusion
    if (parseResult) {
      //No transclusion
      this.textElements.set(elementID, {
        raw: rawOriginalText,
        parsed: parseResult,
        hasTextLink: !!link,
      });
      return [parseResult, link];
    }
    //transclusion needs to be resolved asynchornously
    void this.parse(rawOriginalText).then((parseRes) => {
      const parsedText = parseRes.parsed;
      this.textElements.set(elementID, {
        raw: rawOriginalText,
        parsed: parsedText,
        hasTextLink: !!parseRes.link,
      });
      if (parsedText) {
        updateSceneCallback(parsedText);
      }
    });
    return [null, null];
  }

  public async addTextElement(
    elementID: string,
    rawText: string,
    rawOriginalText: string,
  ): Promise<{ parseResult: string; link: string }> {
    const parseResult = await this.parse(rawOriginalText);
    this.textElements.set(elementID, {
      raw: rawOriginalText,
      parsed: parseResult.parsed,
      hasTextLink: !!parseResult.link,
    });
    return {
      parseResult: parseResult.parsed,
      link: parseResult.link,
    };
  }

  public deleteTextElement(id: string) {
    this.textElements.delete(id);
  }

  public getOpenMode(): { viewModeEnabled: boolean; zenModeEnabled: boolean } {
    const fileCache = this.app.metadataCache.getFileCache(this.file);
    if (this.plugin.forceExcalidrawViewMode) {
      return { viewModeEnabled: true, zenModeEnabled: false };
    }
    let mode =
      this.plugin.settings.defaultMode === "view-mobile"
        ? DEVICE.isPhone
          ? "view"
          : "normal"
        : this.plugin.settings.defaultMode;
    if (
      fileCache?.frontmatter &&
      fileCache.frontmatter[FRONTMATTER_KEYS["default-mode"].name] !== null &&
      typeof fileCache.frontmatter[FRONTMATTER_KEYS["default-mode"].name] !==
        "undefined"
    ) {
      mode = fileCache.frontmatter[FRONTMATTER_KEYS["default-mode"].name];
    }

    switch (mode) {
      case "zen":
        return { viewModeEnabled: false, zenModeEnabled: true };
      case "view":
        return { viewModeEnabled: true, zenModeEnabled: false };
      default:
        return { viewModeEnabled: false, zenModeEnabled: false };
    }
  }

  public getLinkOpacity(): number {
    const fileCache = this.app.metadataCache.getFileCache(this.file);
    let opacity = this.plugin.settings.linkOpacity;
    if (
      fileCache?.frontmatter &&
      fileCache.frontmatter[FRONTMATTER_KEYS["linkbutton-opacity"].name] !==
        null &&
      typeof fileCache.frontmatter[
        FRONTMATTER_KEYS["linkbutton-opacity"].name
      ] !== "undefined"
    ) {
      opacity =
        fileCache.frontmatter[FRONTMATTER_KEYS["linkbutton-opacity"].name];
    }
    return opacity;
  }

  public getOnLoadScript(): string {
    const fileCache = this.app.metadataCache.getFileCache(this.file);
    if (
      fileCache?.frontmatter &&
      fileCache.frontmatter[FRONTMATTER_KEYS["onload-script"].name] !== null &&
      typeof fileCache.frontmatter[FRONTMATTER_KEYS["onload-script"].name] !==
        "undefined"
    ) {
      return fileCache.frontmatter[FRONTMATTER_KEYS["onload-script"].name];
    }
    return null;
  }

  private setLinkPrefix(): boolean {
    const linkPrefix = this.linkPrefix;
    const fileCache = this.app.metadataCache.getFileCache(this.file);
    if (
      fileCache?.frontmatter &&
      typeof fileCache.frontmatter[FRONTMATTER_KEYS["link-prefix"].name] !==
        "undefined"
    ) {
      this.linkPrefix =
        fileCache.frontmatter[FRONTMATTER_KEYS["link-prefix"].name] ?? "";
    } else {
      this.linkPrefix = this.plugin.settings.linkPrefix;
    }
    return linkPrefix !== this.linkPrefix;
  }

  private setUrlPrefix(): boolean {
    const urlPrefix = this.urlPrefix;
    const fileCache = this.app.metadataCache.getFileCache(this.file);
    if (
      fileCache?.frontmatter &&
      typeof fileCache.frontmatter[FRONTMATTER_KEYS["url-prefix"].name] !==
        "undefined"
    ) {
      this.urlPrefix =
        fileCache.frontmatter[FRONTMATTER_KEYS["url-prefix"].name] ?? "";
    } else {
      this.urlPrefix = this.plugin.settings.urlPrefix;
    }
    return urlPrefix !== this.urlPrefix;
  }

  private setAutoexportPreferences() {
    const fileCache = this.app.metadataCache.getFileCache(this.file);
    if (
      fileCache?.frontmatter &&
      fileCache.frontmatter[FRONTMATTER_KEYS.autoexport.name] !== null &&
      typeof fileCache.frontmatter[FRONTMATTER_KEYS.autoexport.name] !==
        "undefined"
    ) {
      switch (
        fileCache.frontmatter[FRONTMATTER_KEYS.autoexport.name].toLowerCase()
      ) {
        case "none":
          this.autoexportPreference = AutoexportPreference.none;
          break;
        case "both":
          this.autoexportPreference = AutoexportPreference.both;
          break;
        case "png":
          this.autoexportPreference = AutoexportPreference.png;
          break;
        case "svg":
          this.autoexportPreference = AutoexportPreference.svg;
          break;
        default:
          this.autoexportPreference = AutoexportPreference.inherit;
      }
    } else {
      this.autoexportPreference = AutoexportPreference.inherit;
    }
  }

  private setembeddableThemePreference(): boolean {
    const embeddableTheme = this.embeddableTheme;
    const fileCache = this.app.metadataCache.getFileCache(this.file);
    if (
      fileCache?.frontmatter &&
      fileCache.frontmatter[FRONTMATTER_KEYS["embeddable-theme"].name] !==
        null &&
      typeof fileCache.frontmatter[
        FRONTMATTER_KEYS["embeddable-theme"].name
      ] !== "undefined"
    ) {
      this.embeddableTheme =
        fileCache.frontmatter[
          FRONTMATTER_KEYS["embeddable-theme"].name
        ].toLowerCase();
      if (!EMBEDDABLE_THEME_FRONTMATTER_VALUES.includes(this.embeddableTheme)) {
        this.embeddableTheme = "default";
      }
    } else if (
      //backwards compatibility
      fileCache?.frontmatter &&
      fileCache.frontmatter[FRONTMATTER_KEYS["iframe-theme"].name] !== null &&
      typeof fileCache.frontmatter[FRONTMATTER_KEYS["iframe-theme"].name] !==
        "undefined"
    ) {
      this.embeddableTheme =
        fileCache.frontmatter[
          FRONTMATTER_KEYS["iframe-theme"].name
        ].toLowerCase();
      if (!EMBEDDABLE_THEME_FRONTMATTER_VALUES.includes(this.embeddableTheme)) {
        this.embeddableTheme = "default";
      }
    } else {
      this.embeddableTheme = this.plugin.settings.iframeMatchExcalidrawTheme
        ? "auto"
        : "default";
    }
    return embeddableTheme !== this.embeddableTheme;
  }

  private setShowLinkBrackets(): boolean {
    const showLinkBrackets = this.showLinkBrackets;
    const fileCache = this.app.metadataCache.getFileCache(this.file);
    if (
      fileCache?.frontmatter &&
      fileCache.frontmatter[FRONTMATTER_KEYS["link-brackets"].name] !== null &&
      typeof fileCache.frontmatter[FRONTMATTER_KEYS["link-brackets"].name] !==
        "undefined"
    ) {
      this.showLinkBrackets =
        fileCache.frontmatter[FRONTMATTER_KEYS["link-brackets"].name] !== false;
    } else {
      this.showLinkBrackets = this.plugin.settings.showLinkBrackets;
    }
    return showLinkBrackets !== this.showLinkBrackets;
  }

  /** 
   Files, equations and mermaid copy/paste support
   This is not a complete solution, it assumes the source document is opened first
   at that time the fileId is stored in the master files/equations map
   when pasted the map is checked if the file already exists
   This will not work if pasting from one vault to another, but for the most common usecase 
   of copying an image or equation from one drawing to another within the same vault
   this is going to do the job
  */
  public setFile(fileId: FileId, data: EmbeddedFile) {
    //always store absolute path because in case of paste, relative path may not resolve ok
    if (!data) {
      return;
    }
    this.files.set(fileId, data);

    if (data.isHyperLink || data.isLocalLink) {
      this.plugin.filesMaster.set(fileId, {
        isHyperLink: data.isHyperLink,
        isLocalLink: data.isLocalLink,
        path: data.hyperlink,
        blockrefData: null,
        hasSVGwithBitmap: data.isSVGwithBitmap,
      });
      return;
    }

    if (!data.file) {
      return;
    }

    const parts = data.linkParts.original.split("#");
    this.plugin.filesMaster.set(fileId, {
      isHyperLink: false,
      isLocalLink: false,
      path: data.file.path + (data.shouldScale() ? "" : "|100%"),
      blockrefData: parts.length === 1 ? null : parts[1],
      hasSVGwithBitmap: data.isSVGwithBitmap,
      colorMapJSON: data.colorMap ? JSON.stringify(data.colorMap) : null,
    });
  }

  public getFiles(): EmbeddedFile[] {
    return Object.values(this.files);
  }

  public getFile(fileId: FileId): EmbeddedFile {
    let embeddedFile = this.files.get(fileId);
    if (embeddedFile) {
      return embeddedFile;
    }
    const masterFile = this.plugin.filesMaster.get(fileId);
    if (!masterFile) {
      return embeddedFile;
    }
    embeddedFile = new EmbeddedFile(
      this.plugin,
      this.file.path,
      masterFile.blockrefData
        ? `${masterFile.path}#${masterFile.blockrefData}`
        : masterFile.path,
      masterFile.colorMapJSON,
    );
    this.files.set(fileId, embeddedFile);
    return embeddedFile;
  }

  public getFileEntries() {
    return this.files.entries();
  }

  public deleteFile(fileId: FileId) {
    this.files.delete(fileId);
    //deliberately not deleting from plugin.filesMaster
    //could be present in other drawings as well
  }

  //Image copy/paste support
  public hasFile(fileId: FileId): boolean {
    if (this.files.has(fileId)) {
      return true;
    }
    if (this.plugin.filesMaster.has(fileId)) {
      const masterFile = this.plugin.filesMaster.get(fileId);
      if (masterFile.isHyperLink || masterFile.isLocalLink) {
        this.files.set(
          fileId,
          new EmbeddedFile(this.plugin, this.file.path, masterFile.path),
        );
        return true;
      }
      const path = masterFile.path.split("|")[0].split("#")[0];
      if (!this.app.vault.getAbstractFileByPath(path)) {
        this.plugin.filesMaster.delete(fileId);
        return true;
      } // the file no longer exists
      const fixScale = masterFile.path.endsWith("100%");
      const embeddedFile = new EmbeddedFile(
        this.plugin,
        this.file.path,
        (masterFile.blockrefData
          ? `${path}#${masterFile.blockrefData}`
          : path) + (fixScale ? "|100%" : ""),
        masterFile.colorMapJSON,
      );
      this.files.set(fileId, embeddedFile);
      return true;
    }
    return false;
  }

  //--------------
  //Equations
  //--------------
  public setEquation(
    fileId: FileId,
    data: { latex: string; isLoaded: boolean },
  ) {
    this.equations.set(fileId, { latex: data.latex, isLoaded: data.isLoaded });
    this.plugin.equationsMaster.set(fileId, data.latex);
  }

  public getEquation(fileId: FileId): { latex: string; isLoaded: boolean } {
    const result = this.equations.get(fileId);
    if (result) {
      return result;
    }
    const latex = this.plugin.equationsMaster.get(fileId);
    if (!latex) {
      return result;
    }
    this.equations.set(fileId, { latex, isLoaded: false });
    return { latex, isLoaded: false };
  }

  public getEquationEntries() {
    return this.equations?.entries();
  }

  public deleteEquation(fileId: FileId) {
    this.equations.delete(fileId);
    //deliberately not deleting from plugin.equationsMaster
    //could be present in other drawings as well
  }

  //Image copy/paste support
  public hasEquation(fileId: FileId): boolean {
    if (this.equations.has(fileId)) {
      return true;
    }
    if (this.plugin.equationsMaster.has(fileId)) {
      this.equations.set(fileId, {
        latex: this.plugin.equationsMaster.get(fileId),
        isLoaded: false,
      });
      return true;
    }
    return false;
  }

  //--------------
  //Mermaids
  //--------------
  public setMermaid(
    fileId: FileId,
    data: { mermaid: string; isLoaded: boolean },
  ) {
    this.mermaids.set(fileId, {
      mermaid: data.mermaid,
      isLoaded: data.isLoaded,
    });
    this.plugin.mermaidsMaster.set(fileId, data.mermaid);
  }

  public getMermaid(fileId: FileId): { mermaid: string; isLoaded: boolean } {
    const result = this.mermaids.get(fileId);
    if (result) {
      return result;
    }
    const mermaid = this.plugin.mermaidsMaster.get(fileId);
    if (!mermaid) {
      return result;
    }
    this.mermaids.set(fileId, { mermaid, isLoaded: false });
    return { mermaid, isLoaded: false };
  }

  public getMermaidEntries() {
    return this.mermaids.entries();
  }

  public deleteMermaid(fileId: FileId) {
    this.mermaids.delete(fileId);
    //deliberately not deleting from plugin.mermaidsMaster
    //could be present in other drawings as well
  }

  //Image copy/paste support
  public hasMermaid(fileId: FileId): boolean {
    if (this.mermaids.has(fileId)) {
      return true;
    }
    if (this.plugin.mermaidsMaster.has(fileId)) {
      this.mermaids.set(fileId, {
        mermaid: this.plugin.mermaidsMaster.get(fileId),
        isLoaded: false,
      });
      return true;
    }
    return false;
  }
}

export const getTransclusion = async (
  linkParts: LinkParts,
  app: App,
  file: TFile,
  charCountLimit?: number,
): Promise<{ contents: string; lineNum: number; leadingHashes?: string }> => {
  //file-name#^blockref
  //1         2 3

  if (!linkParts.path) {
    return { contents: linkParts.original.trim(), lineNum: 0 };
  } //filename not found

  if (!file || !(file instanceof TFile)) {
    return { contents: linkParts.original.trim(), lineNum: 0 };
  }

  const contents = await app.vault.read(file);

  if (!linkParts.ref) {
    //no blockreference
    return charCountLimit
      ? { contents: contents.substring(0, charCountLimit).trim(), lineNum: 0 }
      : { contents: contents.trim(), lineNum: 0 };
  }

  const blocks = (
    await app.metadataCache.blockCache.getForFile(
      { isCancelled: () => false },
      file,
    )
  ).blocks.filter(
    (block: MarkdownBlockCacheEntry) => block.node.type !== "comment",
  );
  if (!blocks) {
    return { contents: linkParts.original.trim(), lineNum: 0 };
  }

  if (linkParts.isBlockRef) {
    let para = blocks.filter(
      (block: MarkdownBlockCacheEntry) => block.node.id === linkParts.ref,
    )[0]?.node;
    if (!para) {
      return { contents: linkParts.original.trim(), lineNum: 0 };
    }
    if (["blockquote"].includes(para.type)) {
      para = para.children[0];
    } //blockquotes are special, they have one child, which has the paragraph
    const startPos = para.position.start.offset;
    const lineNum = para.position.start.line;
    const endPos = para.position.end.offset; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/853
    //para.children[para.children.length - 1]?.position.start.offset - 1; //!not clear what the side effect of the #853 change is
    return {
      contents: contents
        .substring(startPos, endPos)
        .replaceAll(/ \^\S*$|^\^\S*$/gm, "")
        .trim(), //remove the block reference from the end of the line, or from the beginning of a new line
      lineNum,
    };
  }

  const headings = blocks.filter(
    (block: MarkdownBlockCacheEntry) => block.display.search(/^#+\s/) === 0,
  ); // startsWith("#"));
  let startPos: number = null;
  let lineNum: number = 0;
  let endPos: number = null;
  let depth: number = 1;
  for (let i = 0; i < headings.length; i++) {
    if (startPos && !endPos) {
      let j = i;
      while (j < headings.length && headings[j].node.depth > depth) {
        j++;
      }
      if (j === headings.length && headings[j - 1].node.depth > depth) {
        return {
          leadingHashes: `${"#".repeat(depth)} `,
          contents: contents.substring(startPos).trim(),
          lineNum,
        };
      }
      endPos = headings[j].node.position.start.offset - 1;
      return {
        leadingHashes: `${"#".repeat(depth)} `,
        contents: contents.substring(startPos, endPos).trim(),
        lineNum,
      };
    }
    const c = headings[i].node.children[0];
    const dataHeading = headings[i].node.data?.hProperties?.dataHeading;
    const cc = c?.children;
    //const refNoSpace = linkParts.ref.replaceAll(" ","");
    if (
      !startPos &&
      (cleanBlockRef(c?.value) === linkParts.ref ||
        cleanBlockRef(c?.title) === linkParts.ref ||
        cleanBlockRef(dataHeading) === linkParts.ref ||
        (cc ? cleanBlockRef(cc[0]?.value) === linkParts.ref : false) ||
        cleanSectionHeading(c?.value) === linkParts.ref ||
        cleanSectionHeading(c?.title) === linkParts.ref ||
        cleanSectionHeading(dataHeading) === linkParts.ref ||
        (cc ? cleanSectionHeading(cc[0]?.value) === linkParts.ref : false))
    ) {
      startPos = headings[i].node.children[0]?.position.start.offset; //
      depth = headings[i].node.depth;
      lineNum = headings[i].node.children[0]?.position.start.line; //
    }
  }
  if (startPos) {
    return {
      leadingHashes: `${"#".repeat(depth)} `,
      contents: contents.substring(startPos).trim(),
      lineNum,
    };
  }
  return { contents: linkParts.original.trim(), lineNum: 0 };
};
