import ExcalidrawPlugin from "src/core/main";
import {
  FillStyle,
  StrokeStyle,
  ExcalidrawElement,
  ExcalidrawBindableElement,
  FileId,
  NonDeletedExcalidrawElement,
  ExcalidrawImageElement,
  ExcalidrawTextElement,
  StrokeRoundness,
  RoundnessType,
  ExcalidrawFrameElement,
  ExcalidrawTextContainer,
} from "@zsviczian/excalidraw/types/element/src/types";
import { ColorMap, MimeType } from "./EmbeddedFileLoader";
import { Editor,  Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian";
import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings, TextMode } from "src/view/ExcalidrawView";
import { ExcalidrawData, getMarkdownDrawingSection } from "./ExcalidrawData";
import {
  FRONTMATTER,
  nanoid,
  MAX_IMAGE_SIZE,
  COLOR_NAMES,
  fileid,
  GITHUB_RELEASES,
  determineFocusDistance,
  getCommonBoundingBox,
  getLineHeight,
  getMaximumGroups,
  intersectElementWithLine,
  DEVICE,
  mermaidToExcalidraw,
  refreshTextDimensions,
} from "src/constants/constants";
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, splitFolderAndFilename } from "src/utils/fileUtils";
import {
  //debug,
  getImageSize,
  isMaskFile,
  wrapTextAtCharLength,
  arrayToMap,
  addAppendUpdateCustomData,
  getSVG,
} from "src/utils/utils";
import { getAttachmentsFolderAndFilePath, getExcalidrawViews, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "src/utils/obsidianUtils";
import { AppState, BinaryFileData,  DataURL,  ExcalidrawImperativeAPI, SceneData } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFile, EmbeddedFilesLoader } from "./EmbeddedFileLoader";
import { tex2dataURL } from "./LaTeX";
import { NewFileActions } from "src/shared/Dialogs/Prompt";
import { ConnectionPoint, DeviceType, Point  } from "src/types/types";
import CM, { ColorMaster, extendPlugins } from "@zsviczian/colormaster";
import HarmonyPlugin from "@zsviczian/colormaster/plugins/harmony";
import MixPlugin from "@zsviczian/colormaster/plugins/mix"
import A11yPlugin from "@zsviczian/colormaster/plugins/accessibility"
import NamePlugin from "@zsviczian/colormaster/plugins/name"
import LCHPlugin from "@zsviczian/colormaster/plugins/lch";
import LUVPlugin from "@zsviczian/colormaster/plugins/luv";
import LABPlugin from "@zsviczian/colormaster/plugins/lab";
import UVWPlugin from "@zsviczian/colormaster/plugins/uvw";
import XYZPlugin from "@zsviczian/colormaster/plugins/xyz";
import HWBPlugin from "@zsviczian/colormaster/plugins/hwb";
import HSVPlugin from "@zsviczian/colormaster/plugins/hsv";
import RYBPlugin from "@zsviczian/colormaster/plugins/ryb";
import CMYKPlugin from "@zsviczian/colormaster/plugins/cmyk";
import { TInput } from "@zsviczian/colormaster/types";
import {ConversionResult, svgToExcalidraw} from "src/shared/svgToExcalidraw/parser"
import { ROUNDNESS } from "src/constants/constants";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
import { emulateKeysForLinkClick, PaneTarget } from "src/utils/modifierkeyHelper";
import { Mutable } from "@zsviczian/excalidraw/types/common/src/utility-types";
import PolyBool from "polybooljs";
import { EmbeddableMDCustomProps } from "./Dialogs/EmbeddableSettings";
import {
  AIRequest,
  postOpenAI as _postOpenAI,
  extractCodeBlocks as _extractCodeBlocks,
} from "../utils/AIUtils";
import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO } from "./Dialogs/SuggesterInfo";
import { addBackOfTheNoteCard } from "../utils/excalidrawViewUtils";
import { log } from "../utils/debugHelper";
import { ExcalidrawLib } from "../types/excalidrawLib";
import { GlobalPoint } from "@zsviczian/excalidraw/types/math/src/types";
import { AddImageOptions, ImageInfo, SVGColorInfo } from "src/types/excalidrawAutomateTypes";
import { _measureText, cloneElement, createPNG, createSVG, errorMessage, filterColorMap, getEmbeddedFileForImageElment, getFontFamily, getLineBox, getTemplate, isColorStringTransparent, isSVGColorInfo, mergeColorMapIntoSVGColorInfo, normalizeLinePoints, repositionElementsToCursor, svgColorInfoToColorMap, updateOrAddSVGColorInfo, verifyMinimumPluginVersion } from "src/utils/excalidrawAutomateUtils";
import { exportToPDF, getMarginValue, getPageDimensions, PageDimensions, PageOrientation, PageSize, PDFExportScale, PDFPageProperties } from "src/utils/exportUtils";
import { FrameRenderingOptions } from "src/types/utilTypes";
import { CaptureUpdateAction } from "src/constants/constants";
import { AutoexportConfig } from "src/types/excalidrawViewTypes";
import { FloatingModal } from "./Dialogs/FloatingModal";
import { patchMobileView } from "src/utils/customEmbeddableUtils";
import { ObsidianCanvasNode } from "src/view/managers/CanvasNodeFactory";

extendPlugins([
  HarmonyPlugin,
  MixPlugin,
  A11yPlugin,
  NamePlugin,
  LCHPlugin,
  LUVPlugin,
  LABPlugin,
  UVWPlugin,
  XYZPlugin,
  HWBPlugin,
  HSVPlugin,
  RYBPlugin,
  CMYKPlugin
]);

declare const PLUGIN_VERSION:string;
declare var LZString: any;
declare const excalidrawLib: typeof ExcalidrawLib;

const GAP = 4;

/**
 * ExcalidrawAutomate is a utility class that provides a simplified API to interact with Excalidraw elements and the Excalidraw canvas.
 * Elements in the Excalidraw Scene are immutable. You should never directly change element properties in the scene object.
 * ExcalidrawAutomate provides a "workbench" where you can create, modify, and delete elements before committing them to the Excalidraw Scene.
 * The basic workflow is to create elements in ExcalidrawAutomate and once ready commit them to the Excalidraw Scene using addElementsToView().
 * To modify elements in the scene, you should first copy them over to EA using copyViewElementsToEAforEditing, make the necessary modifications,
 * then commit them back to the scene using addElementsToView().
 * To delete an element from the view set element.isDeleted = true and commit the changes to the scene using addElementsToView().
 * 
 * At a very high level, EA has 3 type of functions:
 * - functions that modify elements in the EA workbench
 * - functions that access elements and properties of the Scene
 *   - these only work if targetView is set using setView()
 *   - Scripts executed by the Excalidraw ScritpEngine will have the targetView set automatically
 *   - These functions include the word view in their name e.g. getViewSelectedElements()
 * - utility functions that do not modify eleeemnts in the EA workbench or access the scene e.g.
 *   - ea.obsidian is a utility function that returns the Obsidian Module object.
 *   - eg.getCM() returns the ColorMaster object for manipulationg colors,
 *   - ea.help() provides information about functions and properties in the ExcalidrawAutomate class intended for use in Developer Console
 *   - checkAndCreateFolder (thought this has been superceeded by app.vault.createFolder in the Obsidian API)
 *   - etc.
 * 
 * Note that some actions are asynchronous and require await to complete. e.g.: 
 *   - addImage()
 *   - convertStringToDataURL()
 *   - etc.
 * 
 * About the Excalidraw Automate Script Engine:
 * --------------------------------------------
 * Excalidraw Scripts utilize ExcalidrawAutomate. When the script is invoked Excalidraw passes an ExcalidrawAutomate instance to the script.
 * you may access this object via the variable `ea`. e.g. ea.addImage(); This ea object is already set to the targetView.
 * Through ea.obsidian all of the Obsidian API is available to the script. Thus you can create modal views, open files, etc.
 * You can access Obsidian type definitions here: https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts
 * In addition to the ea object, the script also receives the `utils` object. utils includes to utility functions: suggester and inputPrompt
 *   - inputPrompt(inputPrompt: (
 *       header: string,
 *       placeholder?: string,
 *       value?: string,
 *       buttons?: ButtonDefinition[],
 *       lines?: number,
 *       displayEditorButtons?: boolean,
 *       customComponents?: (container: HTMLElement) => void,
 *       blockPointerInputOutsideModal?: boolean,
 *     ) => Promise<string>;
 *   -  displayItems: string[],
 *       items: any[],
 *       hint?: string,
 *       instructions?: Instruction[],
 *     ) => Promise<any>;
 */
export class ExcalidrawAutomate {
  /**
   * Utility function that returns the Obsidian Module object.
   * @returns {typeof obsidian_module} The Obsidian module object.
   */
  get obsidian() {
    return obsidian_module;
  };

  /**
   * This is a modified version of the Obsidian.Modal class
   * that allows the modal to be dragged around the screen
   * and that does not dim the background.
   */
  get FloatingModal() {
    return FloatingModal;
  }

  /**
   * Retrieves the laser pointer settings from the plugin.
   * @returns {Object} The laser pointer settings.
   */
  get LASERPOINTER() {
    return this.plugin.settings.laserSettings;
  }

  /**
   * Retrieves the device type information.
   * @returns {DeviceType} The device type.
   */
  get DEVICE():DeviceType {
    return DEVICE;
  }

  /**
   * Prints a detailed breakdown of the startup time.
   */
  public printStartupBreakdown() {
    this.plugin.printStarupBreakdown();
  }

  /**
   * Add or modify keys in an element's customData while preserving existing keys.
   * Creates customData={} if it does not exist.
   * @param {string} id - The element ID in elementsDict to modify.
   * @param {Partial<Record<string, unknown>>} newData - Object containing key-value pairs to add/update. Set value to undefined to delete a key.
   * @returns {Mutable<ExcalidrawElement> | undefined} The modified element, or undefined if element does not exist.
   */
  public addAppendUpdateCustomData(id:string, newData: Partial<Record<string, unknown>>) {
    const el = this.elementsDict[id];
    if (!el) {
      return;
    }
    return addAppendUpdateCustomData(el,newData);
  }

  /**
   * Displays help information for EA functions and properties intended to be used in Obsidian developer console.
   * @param {Function | string} target - Function reference or property name as string.
   * Usage examples:
   * - ea.help(ea.functionName) 
   * - ea.help('propertyName')
   * - ea.help('utils.functionName')
   */
  public help(target: Function | string) {
    if (!target) {
      log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
      return;
    }
  
    let funcInfo;
  
    if (typeof target === 'function') {
      funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === target.name);
    } else if (typeof target === 'string') {
      let stringTarget:string = target;
      stringTarget = stringTarget.startsWith("utils.") ? stringTarget.substring(6) : stringTarget;
      stringTarget = stringTarget.startsWith("ea.") ? stringTarget.substring(3) : stringTarget;
      funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === stringTarget);
      if(!funcInfo) {
        funcInfo = EXCALIDRAW_SCRIPTENGINE_INFO.find((info) => info.field === stringTarget);
      }
    }
  
    if(!funcInfo) {
      log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
      return;
    }

    let isMissing = true;
    if (funcInfo.code) {
      isMissing = false;
      log(`Declaration: ${funcInfo.code}`);
    }
    if (funcInfo.desc) {
      isMissing = false;
      const formattedDesc = funcInfo.desc
        .replaceAll("<br>", "\n")
        .replace(/<code>(.*?)<\/code>/g, '%c\u200b$1%c') // Zero-width space
        .replace(/<b>(.*?)<\/b>/g, '%c\u200b$1%c')      // Zero-width space
        .replace(/<a onclick='window\.open\("(.*?)"\)'>(.*?)<\/a>/g, (_, href, text) => `%c\u200b${text}%c\u200b (link: ${href})`); // Zero-width non-joiner
  
      const styles = Array.from({ length: (formattedDesc.match(/%c/g) || []).length }, (_, i) => i % 2 === 0 ? 'color: #007bff;' : '');
      log(`Description: ${formattedDesc}`, ...styles);
    } 
    if (isMissing) {
      log("Description not available for this function.");
    }
  }

  /**
   * Posts an AI request to the OpenAI API and returns the response.
   * @param {AIRequest} request - The AI request configuration.
   * @returns {Promise<RequestUrlResponse>} Promise resolving to the API response.
   */
  public async postOpenAI(request: AIRequest): Promise<RequestUrlResponse> {
    return await _postOpenAI(request);
  } 

  /**
   * Extracts code blocks from markdown text.
   * @param {string} markdown - The markdown string to parse.
   * @returns {Array<{ data: string, type: string }>} Array of objects containing code block contents and types.
   */
  public extractCodeBlocks(markdown: string): { data: string, type: string }[] {
    return _extractCodeBlocks(markdown);
  }

  /**
   * Converts a string to a data URL with specified MIME type.
   * @param {string} data - The string to convert.
   * @param {string} [type="text/html"] - MIME type (default: "text/html").
   * @returns {Promise<string>} Promise resolving to the data URL string.
   */
  public async convertStringToDataURL(data:string, type: string = "text/html"): Promise<string> {
    // Create a blob from the HTML string
    const blob = new Blob([data], { type });
  
    // Read the blob as Data URL
    const base64String = await new Promise((resolve) => {
      const reader = new FileReader();
      reader.onload = () => {
        if(typeof reader.result === "string") {
          const base64String = reader.result.split(',')[1];
          resolve(base64String);
        } else {
          resolve(null);
        }
      };
      reader.readAsDataURL(blob);
    });
    if(base64String) {
      return `data:${type};base64,${base64String}`;
    }
    return "about:blank";
  }

  /**
   * Creates a folder if it doesn't exist.
   * @param {string} folderpath - Path of folder to create.
   * @returns {Promise<TFolder>} Promise resolving to the created/existing TFolder.
   */
  public async checkAndCreateFolder(folderpath: string): Promise<TFolder> {
    return await checkAndCreateFolder(folderpath);
  }

  /**
   * @param filepath - The file path to split into folder and filename.
   * @returns object containing folderpath, filename, basename, and extension.
   */
  public splitFolderAndFilename(filepath: string) : {
    folderpath: string;
    filename: string;
    basename: string;
    extension: string;
  } {
    return splitFolderAndFilename(filepath);
  }

  /**
   * Generates a unique filepath by appending a number if file already exists.
   * @param {string} filename - Base filename.
   * @param {string} folderpath - Target folder path.
   * @returns {string} Unique filepath string.
   */
  public getNewUniqueFilepath(filename: string, folderpath: string): string {
    return getNewUniqueFilepath(this.plugin.app.vault, filename, folderpath);
  }

  /**
   * Gets list of available Excalidraw template files.
   * @returns {TFile[] | null} Array of template TFiles or null if none found.
   */
  public getListOfTemplateFiles(): TFile[] | null {
    return getListOfTemplateFiles(this.plugin);
  }

  /**
   * Gets all embedded images in a drawing recursively.
   * @param {TFile} [excalidrawFile] - Optional file to check, defaults to ea.targetView.file.
   * @returns {TFile[]} Array of embedded image TFiles.
   */
  public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[] {
    if(!excalidrawFile && this.targetView && this.targetView.file) {
      excalidrawFile = this.targetView.file;
    }
    if(!excalidrawFile) {
      return [];
    }
    return getExcalidrawEmbeddedFilesFiletree(excalidrawFile, this.plugin);
  }

  /**
   * Returns a new unique attachment filepath for the filename provided based on Obsidian settings.
   * @param {string} filename - The filename for the attachment.
   * @returns {Promise<string>} Promise resolving to the unique attachment filepath.
   */
  public async getAttachmentFilepath(filename: string): Promise<string> {
    if (!this.targetView || !this.targetView?.file) {
      errorMessage("targetView not set", "getAttachmentFolderAndFilePath()");
      return null;
    }
    const folderAndPath = await getAttachmentsFolderAndFilePath(this.plugin.app,this.targetView.file.path, filename);
    return getNewUniqueFilepath(this.plugin.app.vault, filename, folderAndPath.folder);
  }

  /**
   * Compresses a string to base64 using LZString.
   * @param {string} str - The string to compress.
   * @returns {string} The compressed base64 string.
   */
  public compressToBase64(str:string): string {
    return LZString.compressToBase64(str);
  }

  /**
   * Decompresses a string from base64 using LZString.
   * @param {string} data - The base64 string to decompress.
   * @returns {string} The decompressed string.
   */
  public decompressFromBase64(data:string): string {
    if (!data) throw new Error("No input string provided for decompression.");
    let cleanedData = '';
    const length = data.length;
    for (let i = 0; i < length; i++) {
      const char = data[i];
      if (char !== '\\n' && char !== '\\r') {
        cleanedData += char;
      }
    }
    return LZString.decompressFromBase64(cleanedData);
  }

  /**
   * Prompts the user with a dialog to select new file action.
   * - create markdown file
   * - create excalidraw file
   * - cancel action
   * The new file will be relative to this.targetView.file.path, unless parentFile is provided.
   * If shouldOpenNewFile is true, the new file will be opened in a workspace leaf. 
   * targetPane control which leaf will be used for the new file.
   * Returns the TFile for the new file or null if the user cancelled the action.
   * @param {string} newFileNameOrPath - The new file name or path.
   * @param {boolean} shouldOpenNewFile - Whether to open the new file.
   * @param {PaneTarget} [targetPane] - The target pane for the new file.
   * @param {TFile} [parentFile] - The parent file for the new file.
   * @returns {Promise<TFile | null>} Promise resolving to the new TFile or null if cancelled.
   */
  public async newFilePrompt(
    newFileNameOrPath: string,
    shouldOpenNewFile: boolean,
    targetPane?: PaneTarget,
    parentFile?: TFile,
  ): Promise<TFile | null> {
    if (!this.targetView || !this.targetView?.file) {
      errorMessage("targetView not set", "newFileActions()");
      return null;
    }
    const modifierKeys = emulateKeysForLinkClick(targetPane);
    const newFilePrompt = new NewFileActions({
      plugin: this.plugin,
      path: newFileNameOrPath,
      keys: modifierKeys,
      view: this.targetView,
      openNewFile: shouldOpenNewFile,
      parentFile: parentFile
    })
    newFilePrompt.open();
    return await newFilePrompt.waitForClose;
  }

  /**
   * Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if available, etc.
   * @param {WorkspaceLeaf} origo - The currently active leaf, the origin of the new leaf.
   * @param {PaneTarget} [targetPane] - The target pane for the new leaf.
   * @returns {WorkspaceLeaf} The new or adjacent workspace leaf.
   */
  public getLeaf (
    origo: WorkspaceLeaf,
    targetPane?: PaneTarget,
  ): WorkspaceLeaf {
    const modifierKeys = emulateKeysForLinkClick(targetPane??"new-tab");
    return getLeaf(this.plugin,origo,modifierKeys);
  }

  /**
   * Returns the editor or leaf.view of the currently active embedded obsidian file.
   * If view is not provided, ea.targetView is used.
   * If the embedded file is a markdown document the function will return
   * {file:TFile, editor:Editor} otherwise it will return {view:any}. You can check view type with view.getViewType();
   * @param {ExcalidrawView} [view] - The view to check.
   * @returns {{view:any}|{file:TFile, editor:Editor}|null} The active embeddable view or editor.
   */
  public getActiveEmbeddableViewOrEditor (view?:ExcalidrawView): {view:any}|{file:TFile, editor:Editor}|{node: ObsidianCanvasNode}|null {
    if (!this.targetView && !view) {
      return null;
    }
    view = view ?? this.targetView;
    const leafOrNode = view.getActiveEmbeddable();
    if(leafOrNode) {
      if(leafOrNode.node && leafOrNode.node.isEditing) {
        return {file: leafOrNode.node.file, editor: leafOrNode.node.child.editor};
      }
      if(leafOrNode.node) {
        return {node: leafOrNode.node};
      }
      if(leafOrNode.leaf && leafOrNode.leaf.view) {
        return {view: leafOrNode.leaf.view};
      }
    }
    return null;
  }

  /**
   * Checks if the Excalidraw File is a mask file.
   * @param {TFile} [file] - The file to check.
   * @returns {boolean} True if the file is a mask file, false otherwise.
   */
  public isExcalidrawMaskFile(file?:TFile): boolean {
    if(file) {
      return this.isExcalidrawFile(file) && isMaskFile(this.plugin, file);
    }
    if (!this.targetView || !this.targetView?.file) {
      errorMessage("targetView not set", "isMaskFile()");
      return null;
    }
    return isMaskFile(this.plugin, this.targetView.file);
  }

  plugin: ExcalidrawPlugin;
  elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id
  imagesDict: {[key: FileId]: ImageInfo}; //the images files including DataURL, indexed by fileId
  mostRecentMarkdownSVG:SVGSVGElement = null; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes
  style: {
    strokeColor: string; //https://www.w3schools.com/colors/default.asp
    backgroundColor: string;
    angle: number; //radian
    fillStyle: FillStyle; //type FillStyle = "hachure" | "cross-hatch" | "solid"
    strokeWidth: number;
    strokeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted"
    roughness: number;
    opacity: number;
    strokeSharpness?: StrokeRoundness; //defaults to undefined, use strokeRoundess and roundess instead. Only kept for legacy script compatibility type StrokeRoundness = "round" | "sharp"
    roundness: null | { type: RoundnessType; value?: number };
    fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:Local Font
    fontSize: number;
    textAlign: string; //"left"|"right"|"center"
    verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently
    startArrowHead: string; //"arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null
    endArrowHead: string;
  };
  canvas: {
    theme: string; //"dark"|"light"
    viewBackgroundColor: string;
    gridSize: number;
  };
  colorPalette: {};

  constructor(plugin: ExcalidrawPlugin, view?: ExcalidrawView) {
    this.plugin = plugin;
    this.reset();
    this.targetView = view;
  }

  /**
   * Returns the last recorded pointer position on the Excalidraw canvas.
   * @returns {{x:number, y:number}} The last recorded pointer position.
   */
  public getViewLastPointerPosition(): {x:number, y:number} {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "getExcalidrawAPI()");
      return null;
    }
    return this.targetView.currentPosition;
  }

  /**
   * Returns the center position of the current view in Excalidraw coordinates.
   * @returns {{x:number, y:number}} The center position of the view.
   */
  public getViewCenterPosition(): {x:number, y:number} {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "getExcalidrawAPI()");
      return null;
    }
    const st = (this.getExcalidrawAPI() as ExcalidrawImperativeAPI).getAppState();
    if (!st) return null;

    const zoom = st.zoom?.value ?? 1;
    const x = -st.scrollX + (st.width / 2) / zoom;
    const y = -st.scrollY + (st.height / 2) / zoom;

    return { x, y };
  }

  /**
   * Returns the Excalidraw API for the current view or the view provided.
   * @param {ExcalidrawView} [view] - The view to get the API for.
   * @returns {ExcalidrawAutomate} The Excalidraw API.
   */
  public getAPI(view?:ExcalidrawView):ExcalidrawAutomate {
    const ea = new ExcalidrawAutomate(this.plugin, view);
    this.plugin.eaInstances.push(ea);
    return ea;
  }

  /**
   * Sets the fill style for new elements.
   * @param {number} val - The fill style value (0: "hachure", 1: "cross-hatch", 2: "solid").
   * @returns {"hachure"|"cross-hatch"|"solid"} The fill style string.
   */
  setFillStyle(val: number):"hachure"|"cross-hatch"|"solid" {
    switch (val) {
      case 0:
        this.style.fillStyle = "hachure";
        return "hachure";
      case 1:
        this.style.fillStyle = "cross-hatch";
        return "cross-hatch";
      default:
        this.style.fillStyle = "solid";
        return "solid";
    }
  };

  /**
   * Sets the stroke style for new elements.
   * @param {number} val - The stroke style value (0: "solid", 1: "dashed", 2: "dotted").
   * @returns {"solid"|"dashed"|"dotted"} The stroke style string.
   */
  setStrokeStyle(val: number):"solid"|"dashed"|"dotted" {
    switch (val) {
      case 0:
        this.style.strokeStyle = "solid";
        return "solid";
      case 1:
        this.style.strokeStyle = "dashed";
        return "dashed";
      default:
        this.style.strokeStyle = "dotted";
        return "dotted";
    }
  };

  /**
   * Sets the stroke sharpness for new elements.
   * @param {number} val - The stroke sharpness value (0: "round", 1: "sharp").
   * @returns {"round"|"sharp"} The stroke sharpness string.
   */
  setStrokeSharpness(val: number):"round"|"sharp" {
    switch (val) {
      case 0:
        this.style.roundness = {
          type: ROUNDNESS.LEGACY
        }
        return "round";
      default:
        this.style.roundness = null; //sharp
        return "sharp";
    }
  };

  /**
   * Sets the font family for new text elements.
   * @param {number} val - The font family value (1: Virgil, 2: Helvetica, 3: Cascadia).
   * @returns {string} The font family string.
   */
  setFontFamily(val: number):string {
    switch (val) {
      case 1:
        this.style.fontFamily = 4;
        return getFontFamily(4);
      case 2:
        this.style.fontFamily = 2;
        return getFontFamily(2);
      case 3:
        this.style.fontFamily = 3;
        return getFontFamily(3);
      default:
        this.style.fontFamily = 1;
        return getFontFamily(1);
    }
  };

  /**
   * Sets the theme for the canvas.
   * @param {number} val - The theme value (0: "light", 1: "dark").
   * @returns {"light"|"dark"} The theme string.
   */
  setTheme(val: number):"light"|"dark" {
    switch (val) {
      case 0:
        this.canvas.theme = "light";
        return "light";
      default:
        this.canvas.theme = "dark";
        return "dark";
    }
  };

  /**
   * Generates a groupID and adds the groupId to all the elements in the objectIds array. Essentially grouping the elements in the view.
   * @param {string[]} objectIds - Array of element IDs to group.
   * @returns {string} The generated group ID.
   */
  addToGroup(objectIds: string[]): string {
    const id = nanoid();
    objectIds.forEach((objectId) => {
      this.elementsDict[objectId]?.groupIds?.push(id);
    });
    return id;
  };

  /**
   * Copies elements from ExcalidrawAutomate to the clipboard as a valid Excalidraw JSON string.
   * @param {string} [templatePath] - Optional template path to include in the clipboard data.
   */
  async toClipboard(templatePath?: string) {
    const template = templatePath
      ? await getTemplate(
          this.plugin,
          templatePath,
          false,
          new EmbeddedFilesLoader(this.plugin),
          0
        )
      : null;
    let elements = template ? template.elements : [];
    elements = elements.concat(this.getElements());
    navigator.clipboard.writeText(
      JSON.stringify({
        type: "excalidraw/clipboard",
        elements,
      }),
    );
  };

  /**
   * Extracts the Excalidraw Scene from an Excalidraw File.
   * @param {TFile} file - The Excalidraw file to extract the scene from.
   * @returns {Promise<{elements: ExcalidrawElement[]; appState: AppState;}>} Promise resolving to the Excalidraw scene.
   */
  async getSceneFromFile(file: TFile): Promise<{elements: ExcalidrawElement[]; appState: AppState;}> {
    if(!file) {
      errorMessage("file not found", "getScene()");
      return null;
    }
    if(!this.isExcalidrawFile(file)) {
      errorMessage("file is not an Excalidraw file", "getScene()");
      return null;
    }
    const template = await getTemplate(this.plugin,file.path,false,new EmbeddedFilesLoader(this.plugin),0);
    return {
      elements: template.elements,
      appState: template.appState
    }
  }

  /**
   * Gets all elements from ExcalidrawAutomate elementsDict.
   * @returns {Mutable<ExcalidrawElement>[]} Array of elements from elementsDict.
   */
  getElements(): Mutable<ExcalidrawElement>[] {
    const elements = [];
    const elementIds = Object.keys(this.elementsDict);
    for (let i = 0; i < elementIds.length; i++) {
      elements.push(this.elementsDict[elementIds[i]]);
    }
    return elements;
  };
  
  /**
   * Gets a single element from ExcalidrawAutomate elementsDict.
   * @param {string} id - The element ID to retrieve.
   * @returns {Mutable<ExcalidrawElement>} The element with the specified ID.
   */
  getElement(id: string): Mutable<ExcalidrawElement> {
    return this.elementsDict[id];
  };

  /**
   * Creates a drawing and saves it to the specified filename.
   * @param {Object} [params] - Parameters for creating the drawing.
   * @param {string} [params.filename] - The filename for the drawing. If null, default filename as defined in Excalidraw settings.
   * @param {string} [params.foldername] - The folder name for the drawing. If null, default folder as defined in Excalidraw settings.
   * @param {string} [params.templatePath] - The template path to use for the drawing.
   * @param {boolean} [params.onNewPane] - Whether to open the drawing in a new pane.
   * @param {boolean} [params.silent] - Whether to create the drawing silently.
   * @param {Object} [params.frontmatterKeys] - Frontmatter keys to include in the drawing.
   * @param {string} [params.plaintext] - Text to insert above the `# Text Elements` section.
   * @returns {Promise<string>} Promise resolving to the path of the created drawing.
   */
  async create(params?: {
    filename?: string;
    foldername?: string;
    templatePath?: string;
    onNewPane?: boolean;
    silent?: boolean;
    frontmatterKeys?: {
      "excalidraw-plugin"?: "raw" | "parsed";
      "excalidraw-link-prefix"?: string;
      "excalidraw-link-brackets"?: boolean;
      "excalidraw-url-prefix"?: string;
      "excalidraw-export-transparent"?: boolean;
      "excalidraw-export-dark"?: boolean;
      "excalidraw-export-padding"?: number;
      "excalidraw-export-pngscale"?: number;
      "excalidraw-export-embed-scene"?: boolean;
      "excalidraw-default-mode"?: "view" | "zen";
      "excalidraw-onload-script"?: string;
      "excalidraw-linkbutton-opacity"?: number;
      "excalidraw-autoexport"?: boolean;
      "excalidraw-mask"?: boolean;
      "excalidraw-open-md"?: boolean;
      "cssclasses"?: string;
    };
    plaintext?: string; //text to insert above the `# Text Elements` section
  }): Promise<string> {

    const template = params?.templatePath
      ? await getTemplate(
          this.plugin,
          params.templatePath,
          true,
          new EmbeddedFilesLoader(this.plugin),
          0
        )
      : null;
    if (template?.plaintext) {
      if(params.plaintext) {
        params.plaintext = params.plaintext + "\n\n" + template.plaintext;
      } else {
        params.plaintext = template.plaintext;
      }
    }
    let elements = template ? template.elements : [];
    elements = elements.concat(this.getElements());
    let frontmatter: string;
    if (params?.frontmatterKeys) {
      const keys = Object.keys(params.frontmatterKeys);
      if (!keys.includes("excalidraw-plugin")) {
        params.frontmatterKeys["excalidraw-plugin"] = "parsed";
      }
      frontmatter = "---\n\n";
      for (const key of Object.keys(params.frontmatterKeys)) {
        frontmatter += `${key}: ${
          //@ts-ignore
          params.frontmatterKeys[key] === ""
            ? '""'
            : //@ts-ignore
              params.frontmatterKeys[key]
        }\n`;
      }
      frontmatter += "\n---\n";
    } else {
      frontmatter = template?.frontmatter
        ? template.frontmatter
        : FRONTMATTER;
    }

    frontmatter += params.plaintext
      ? (params.plaintext.endsWith("\n\n")
        ? params.plaintext
        : (params.plaintext.endsWith("\n") 
          ? params.plaintext + "\n"
          : params.plaintext + "\n\n"))
      : "";
    if(template?.frontmatter && params?.frontmatterKeys) {
      //the frontmatter tags supplyed to create take priority
      frontmatter = mergeMarkdownFiles(template.frontmatter,frontmatter);
    }

    const templateAppstate = template?.appState ?? {};
    Object.keys(templateAppstate).forEach((key) => {
      if(templateAppstate[key] === undefined) {
        delete templateAppstate[key];
      }
    });
    const scene = {
      type: "excalidraw",
      version: 2,
      source: GITHUB_RELEASES+PLUGIN_VERSION,
      elements,
      appState: {
        ...templateAppstate,
        theme: templateAppstate.theme ?? this.canvas.theme,
        viewBackgroundColor:
          templateAppstate.viewBackgroundColor ??
          this.canvas.viewBackgroundColor,
        currentItemStrokeColor:
          templateAppstate.currentItemStrokeColor ??
          this.style.strokeColor,
        currentItemBackgroundColor:
          templateAppstate.currentItemBackgroundColor ??
          this.style.backgroundColor,
        currentItemFillStyle:
          templateAppstate.currentItemFillStyle ?? this.style.fillStyle,
        currentItemStrokeWidth:
          templateAppstate.currentItemStrokeWidth ??
          this.style.strokeWidth,
        currentItemStrokeStyle:
          templateAppstate.currentItemStrokeStyle ??
          this.style.strokeStyle,
        currentItemRoughness:
          templateAppstate.currentItemRoughness ?? this.style.roughness,
        currentItemOpacity:
          templateAppstate.currentItemOpacity ?? this.style.opacity,
        currentItemFontFamily:
          templateAppstate.currentItemFontFamily ?? this.style.fontFamily,
        currentItemFontSize:
          templateAppstate.currentItemFontSize ?? this.style.fontSize,
        currentItemTextAlign:
          templateAppstate.currentItemTextAlign ?? this.style.textAlign,
        currentItemStartArrowhead:
          templateAppstate.currentItemStartArrowhead ??
          this.style.startArrowHead,
        currentItemEndArrowhead:
          templateAppstate.currentItemEndArrowhead ??
          this.style.endArrowHead,
        currentItemRoundness:
          templateAppstate.currentItemLinearStrokeSharpness ??
          templateAppstate.currentItemStrokeSharpness ??
          templateAppstate.currentItemRoundness ??
          this.style.roundness ? "round":"sharp",
        gridSize: templateAppstate.gridSize ?? this.canvas.gridSize,
        colorPalette: templateAppstate.colorPalette ?? this.colorPalette,
      },
      files: template?.files ?? {},
    };

    const generateMD = ():string => {
      const textElements = this.getElements().filter(el => el.type === "text") as ExcalidrawTextElement[];
      let outString = `# Excalidraw Data\n\n## Text Elements\n`;
      textElements.forEach(te=> {
        outString += `${te.rawText ?? (te.originalText ?? te.text)} ^${te.id}\n\n`;
      });

      const elementsWithLinks = this.getElements().filter( el => el.type !== "text" && el.link)
      elementsWithLinks.forEach(el=>{
        outString += `${el.link} ^${el.id}\n\n`;
      })
  
      outString += Object.keys(this.imagesDict).length > 0
        ? `\n## Embedded Files\n`
        : "\n";
        
      const embeddedFile = (key: FileId, path: string, colorMap?:ColorMap): string => {
        return `${key}: [[${path}]]${colorMap ? " " + JSON.stringify(colorMap): ""}\n\n`;
      }

      Object.keys(this.imagesDict).forEach((key: FileId)=> {
        const item = this.imagesDict[key];
        if(item.latex) {
          outString += `${key}: $$${item.latex.trim()}$$\n\n`;  
        } else {
          if(item.file) {
            if(item.file instanceof TFile) {
              outString += embeddedFile(key,item.file.path, item.colorMap);
            } else {
              outString += embeddedFile(key,item.file, item.colorMap);
            }
          } else {
            const hyperlinkSplit = item.hyperlink.split("#");
            const file = this.plugin.app.vault.getAbstractFileByPath(hyperlinkSplit[0]);
            if(file && file instanceof TFile) {
              const hasFileRef = hyperlinkSplit.length === 2
              outString += hasFileRef
                ? embeddedFile(key,`${file.path}#${hyperlinkSplit[1]}`, item.colorMap)
                : embeddedFile(key,file.path, item.colorMap);
            } else {
              outString += `${key}: ${item.hyperlink}\n\n`;
            }
          }
        }
      })
      return outString + "%%\n";
    }

    const filename = params?.filename
      ? params.filename + (params.filename.endsWith(".md") ? "": ".excalidraw.md")
      : getDrawingFilename(this.plugin.settings);
    const foldername = params?.foldername ? params.foldername : this.plugin.settings.folder;
    const initData = this.plugin.settings.compatibilityMode
      ? JSON.stringify(scene, null, "\t")
      : frontmatter + generateMD() +
        getMarkdownDrawingSection(JSON.stringify(scene, null, "\t"),this.plugin.settings.compress)

    if(params.silent) {
      return (await this.plugin.createDrawing(filename,foldername,initData)).path;
    } else {
      return this.plugin.createAndOpenDrawing(
        filename,
        (params?.onNewPane ? params.onNewPane : false)?"new-pane":"active-pane",
        foldername,
        initData
      );
    }
  };

  /**
   * Returns the dimensions of a standard page size in pixels.
   *
   * @param {PageSize} pageSize - The standard page size. Possible values are "A0", "A1", "A2", "A3", "A4", "A5", "Letter", "Legal", "Tabloid".
   * @param {PageOrientation} orientation - The orientation of the page. Possible values are "portrait" and "landscape".
   * @returns {PageDimensions} - An object containing the width and height of the page in pixels.
   *
   * @typedef {Object} PageDimensions
   * @property {number} width - The width of the page in pixels.
   * @property {number} height - The height of the page in pixels.
   *
   * @example
   * const dimensions = getPageDimensions("A4", "portrait");
   * console.log(dimensions); // { width: 794.56, height: 1122.56 }
  */
  getPagePDFDimensions(pageSize: PageSize, orientation: PageOrientation): PageDimensions {
    return getPageDimensions(pageSize, orientation);
  }

  /**
   * Creates a PDF from the provided SVG elements with specified scaling and page properties.
   *
   * @param {Object} params - The parameters for creating the PDF.
   * @param {SVGSVGElement[]} params.SVG - An array of SVG elements to be included in the PDF.
   * @param {PDFExportScale} [params.scale={ fitToPage: 1, zoom: 1 }] - The scaling options for the SVG elements.
   * @param {PDFPageProperties} [params.pageProps] - The properties for the PDF pages.
   * @returns {Promise<ArrayBuffer>} - A promise that resolves to an ArrayBuffer containing the PDF data.
   *
   * @example
   * const pdfData = await createToPDF({
   *   SVG: [svgElement1, svgElement2],
   *   scale: { fitToPage: 1 },
   *   pageProps: {
   *     dimensions: { width: 794.56, height: 1122.56 },
   *     backgroundColor: "#ffffff",
   *     margin: { left: 20, right: 20, top: 20, bottom: 20 },
   *     alignment: "center",
   *   }
   *   filename: "example.pdf",
   * });
  */
  async createPDF({
    SVG,
    scale = { fitToPage: 1, zoom: 1 },
    pageProps,
    filename,
  }: {
    SVG: SVGSVGElement[];
    scale?: PDFExportScale;
    pageProps?: PDFPageProperties;
    filename: string;
  }): Promise<void> {
    if(!pageProps) {
      pageProps = {
        alignment: this.plugin.settings.pdfSettings.alignment,
        margin: getMarginValue(this.plugin.settings.pdfSettings.margin),
      };
    }

    if(!pageProps.dimensions) {
      pageProps.dimensions = getPageDimensions(
        this.plugin.settings.pdfSettings.pageSize,
        this.plugin.settings.pdfSettings.pageOrientation
      )
    }
    if(!pageProps.backgroundColor) {
      pageProps.backgroundColor = "#ffffff";
    }
    
    await exportToPDF({SVG, scale, pageProps, filename});
  }

 /**
  * Creates an SVG representation of the current view.
  *
  * @param {Object} options - The options for creating the SVG.
  * @param {boolean} [options.withBackground=true] - Whether to include the background in the SVG.
  * @param {"light" | "dark"} [options.theme] - The theme to use for the SVG.
  * @param {FrameRenderingOptions} [options.frameRendering={enabled: true, name: true, outline: true, clip: true}] - The frame rendering options.
  * @param {number} [options.padding] - The padding to apply around the SVG.
  * @param {boolean} [options.selectedOnly=false] - Whether to include only the selected elements in the SVG.
  * @param {boolean} [options.skipInliningFonts=false] - Whether to skip inlining fonts in the SVG.
  * @param {boolean} [options.embedScene=false] - Whether to embed the scene in the SVG.
  * @param {ExcalidrawElement[]} [options.elementsOverride] - Optional override for the elements to include in the SVG. Primary to support the Printable Layout Wizard script
  * @returns {Promise<SVGSVGElement>} A promise that resolves to the SVG element.
 */
  async createViewSVG({
    withBackground = true,
    theme,
    frameRendering = {enabled: true, name: true, outline: true, clip: true},
    padding,
    selectedOnly = false,
    skipInliningFonts = false,
    embedScene = false,
    elementsOverride,
  } : {
    withBackground?: boolean,
    theme?: "light" | "dark",
    frameRendering?: FrameRenderingOptions,
    padding?: number,
    selectedOnly?: boolean,
    skipInliningFonts?: boolean,
    embedScene?: boolean,
    elementsOverride?: ExcalidrawElement[]
  }): Promise<SVGSVGElement> {
    if(!this.targetView || !this.targetView.file || !this.targetView._loaded) {
      console.log("No view loaded");
      return;
    }
    const view = this.targetView;
    const scene = this.targetView.getScene(selectedOnly);

    if(elementsOverride) {
      scene.elements = elementsOverride;
    }
    const exportSettings: ExportSettings = {
      withBackground: view.getViewExportWithBackground(withBackground),
      withTheme: true,
      isMask: isMaskFile(this.plugin, view.file),
      skipInliningFonts,
      frameRendering,
    };

    return await getSVG(
      {
        ...scene,
        ...{
          appState: {
            ...scene.appState,
            theme: view.getViewExportTheme(theme),
            exportEmbedScene: view.getViewExportEmbedScene(embedScene),
          },
        },
      },
      exportSettings,
      view.getViewExportPadding(padding),
      view.file,
    );
  }

  /**
   * Creates an SVG image from the ExcalidrawAutomate elements and the template provided.
   * @param {string} [templatePath] - The template path to use for the SVG.
   * @param {boolean} [embedFont=false] - Whether to embed the font in the SVG.
   * @param {ExportSettings} [exportSettings] - Export settings for the SVG.
   * @param {EmbeddedFilesLoader} [loader] - Embedded files loader for the SVG.
   * @param {string} [theme] - The theme to use for the SVG.
   * @param {number} [padding] - The padding to use for the SVG.
   * @returns {Promise<SVGSVGElement>} Promise resolving to the created SVG element.
   */
  async createSVG(
    templatePath?: string,
    embedFont: boolean = false,
    exportSettings?: ExportSettings, 
    loader?: EmbeddedFilesLoader,
    theme?: string,
    padding?: number,
  ): Promise<SVGSVGElement> {
    if (!theme) {
      theme = this.plugin.settings.previewMatchObsidianTheme
        ? isObsidianThemeDark()
          ? "dark"
          : "light"
        : !this.plugin.settings.exportWithTheme
        ? "light"
        : undefined;
    }
    if (!exportSettings) {
      exportSettings = {
        withBackground: this.plugin.settings.exportWithBackground,
        withTheme: true,
        isMask: false,
        skipInliningFonts: !embedFont,
      };
    }  
    if (!loader) {
      loader = new EmbeddedFilesLoader(
        this.plugin,
        theme ? theme === "dark" : undefined,
      );
    }

    return await createSVG(
      templatePath,
      embedFont,
      exportSettings,
      loader,
      theme,
      this.canvas.theme,
      this.canvas.viewBackgroundColor,
      this.getElements(),
      this.plugin,
      0,
      padding,
      this.imagesDict
    );
  };

  
  /**
   * Creates a PNG image from the ExcalidrawAutomate elements and the template provided.
   * @param {string} [templatePath] - The template path to use for the PNG.
   * @param {number} [scale=1] - The scale factor for the PNG.
   * @param {ExportSettings} [exportSettings] - Export settings for the PNG.
   * @param {EmbeddedFilesLoader} [loader] - Embedded files loader for the PNG.
   * @param {string} [theme] - The theme to use for the PNG.
   * @param {number} [padding] - The padding to use for the PNG.
   * @returns {Promise<any>} Promise resolving to the created PNG image.
   */
  async createPNG(
    templatePath?: string,
    scale: number = 1,
    exportSettings?: ExportSettings,
    loader?: EmbeddedFilesLoader,
    theme?: string,
    padding?: number,
  ): Promise<any> {
    if (!theme) {
      theme = this.plugin.settings.previewMatchObsidianTheme
        ? isObsidianThemeDark()
          ? "dark"
          : "light"
        : !this.plugin.settings.exportWithTheme
        ? "light"
        : undefined;
    }
    if (theme && !exportSettings) {
      exportSettings = {
        withBackground: this.plugin.settings.exportWithBackground,
        withTheme: true,
        isMask: false,
      };
    }
    if (!loader) {
      loader = new EmbeddedFilesLoader(
        this.plugin,
        theme ? theme === "dark" : undefined,
      );
    }

    return await createPNG(
      templatePath,
      scale,
      exportSettings,
      loader,
      theme,
      this.canvas.theme,
      this.canvas.viewBackgroundColor,
      this.getElements(),
      this.plugin,
      0,
      padding,
      this.imagesDict,
    );
  };

  /**
   * Wrapper for createPNG() that returns a base64 encoded string designed to support LLM workflows.
   * @param {string} [templatePath] - The template path to use for the PNG.
   * @param {number} [scale=1] - The scale factor for the PNG.
   * @param {ExportSettings} [exportSettings] - Export settings for the PNG.
   * @param {EmbeddedFilesLoader} [loader] - Embedded files loader for the PNG.
   * @param {string} [theme] - The theme to use for the PNG.
   * @param {number} [padding] - The padding to use for the PNG.
   * @returns {Promise<string>} Promise resolving to the base64 encoded PNG string.
   */
  async createPNGBase64(
    templatePath?: string,
    scale: number = 1,
    exportSettings?: ExportSettings,
    loader?: EmbeddedFilesLoader,
    theme?: string,
    padding?: number,
  ): Promise<string> {
    const png = await this.createPNG(templatePath,scale,exportSettings,loader,theme,padding);
    return `data:image/png;base64,${await blobToBase64(png)}`
  }

  /**
   * Wraps text to a specified line length.
   * @param {string} text - The text to wrap.
   * @param {number} lineLen - The maximum line length.
   * @returns {string} The wrapped text.
   */
  wrapText(text: string, lineLen: number): string {
    return wrapTextAtCharLength(text, lineLen, this.plugin.settings.forceWrap);
  };

  /**
   * Utility function. Returns an element object using style settings and provided parameters.
   * @param {string} id - The element ID.
   * @param {string} eltype - The element type.
   * @param {number} x - The x-coordinate of the element.
   * @param {number} y - The y-coordinate of the element.
   * @param {number} w - The width of the element.
   * @param {number} h - The height of the element.
   * @param {string | null} [link=null] - The link associated with the element.
   * @param {[number, number]} [scale] - The scale of the element.
   * @returns {Object} The element object.
   */
  private boxedElement(
    id: string,
    eltype: any,
    x: number,
    y: number,
    w: number,
    h: number,
    link: string | null = null,
    scale?: [number, number],
  ) {
    return {
      id,
      type: eltype,
      x,
      y,
      width: w,
      height: h,
      angle: this.style.angle,
      strokeColor: this.style.strokeColor,
      backgroundColor: this.style.backgroundColor,
      fillStyle: this.style.fillStyle,
      strokeWidth: this.style.strokeWidth,
      strokeStyle: this.style.strokeStyle,
      roughness: this.style.roughness,
      opacity: this.style.opacity,
      roundness: this.style.strokeSharpness
        ? (this.style.strokeSharpness === "round"
          ? {type: ROUNDNESS.ADAPTIVE_RADIUS}
          : null)
        : this.style.roundness,
      seed: Math.floor(Math.random() * 100000),
      version: 1,
      versionNonce: Math.floor(Math.random() * 1000000000),
      updated: Date.now(),
      isDeleted: false,
      groupIds: [] as any,
      boundElements: [] as any,
      link,
      locked: false,
      frameId: null as string,
      ...scale ? {scale} : {},
    };
  }

  /**
   * Deprecated. Use addEmbeddable() instead.
   * Retained for backward compatibility.
   * @param {number} topX - The x-coordinate of the top-left corner.
   * @param {number} topY - The y-coordinate of the top-left corner.
   * @param {number} width - The width of the iframe.
   * @param {number} height - The height of the iframe.
   * @param {string} [url] - The URL of the iframe.
   * @param {TFile} [file] - The file associated with the iframe.
   * @returns {string} The ID of the added iframe element.
   */
  addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string {
    return this.addEmbeddable(topX, topY, width, height, url, file);
  }

  /**
   * Adds an embeddable element to the ExcalidrawAutomate instance.
   * @param {number} topX - The x-coordinate of the top-left corner.
   * @param {number} topY - The y-coordinate of the top-left corner.
   * @param {number} width - The width of the embeddable element.
   * @param {number} height - The height of the embeddable element.
   * @param {string} [url] - The URL of the embeddable element.
   * @param {TFile} [file] - The file associated with the embeddable element.
   * @param {EmbeddableMDCustomProps} [embeddableCustomData] - Custom properties for the embeddable element.
   * @returns {string} The ID of the added embeddable element.
   */
  public addEmbeddable(
    topX: number,
    topY: number,
    width: number,
    height: number,
    url?: string,
    file?: TFile,
    embeddableCustomData?: EmbeddableMDCustomProps,
  ): string {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "addEmbeddable()");
      return null;
    }

    if (!url && !file) {
      errorMessage("Either the url or the file must be set. If both are provided the URL takes precedence", "addEmbeddable()");
      return null;
    }

    const id = nanoid();
    this.elementsDict[id] = this.boxedElement(
      id,
      "embeddable",
      topX,
      topY,
      width,
      height,
      url ? url : file ? `[[${
        this.plugin.app.metadataCache.fileToLinktext(
          file,
          this.targetView.file.path,
          false, //file.extension === "md", //changed this to false because embedable link navigation in ExcaliBrain
        )
      }]]` : "",
      [1,1],
    );
    this.elementsDict[id].customData = {mdProps: embeddableCustomData ?? this.plugin.settings.embeddableMarkdownDefaults};
    return id;
  };

  /**
   * Add elements to frame.
   * @param {string} frameId - The ID of the frame element.
   * @param {string[]} elementIDs - Array of element IDs to add to the frame.
   */
  addElementsToFrame(frameId: string, elementIDs: string[]):void {
    if(!this.getElement(frameId)) return;
    elementIDs.forEach(elID => {
      const el = this.getElement(elID);
      if(el) {
        el.frameId = frameId;
      }
    });
  }

  /**
   * Adds a frame element to the ExcalidrawAutomate instance.
   * @param {number} topX - The x-coordinate of the top-left corner.
   * @param {number} topY - The y-coordinate of the top-left corner.
   * @param {number} width - The width of the frame.
   * @param {number} height - The height of the frame.
   * @param {string} [name] - The display name of the frame.
   * @returns {string} The ID of the added frame element.
   */
  addFrame(topX: number, topY: number, width: number, height: number, name?: string): string {
    const id = this.addRect(topX, topY, width, height);
    const frame = this.getElement(id) as Mutable<ExcalidrawFrameElement>;
    frame.type = "frame";
    frame.backgroundColor = "transparent";
    frame.strokeColor = "#000";
    frame.strokeStyle = "solid";
    frame.strokeWidth = 2;
    frame.roughness = 0;
    frame.roundness = null;
    if(name) frame.name = name;
    return id;
  }

  /**
   * Adds a rectangle element to the ExcalidrawAutomate instance.
   * @param {number} topX - The x-coordinate of the top-left corner.
   * @param {number} topY - The y-coordinate of the top-left corner.
   * @param {number} width - The width of the rectangle.
   * @param {number} height - The height of the rectangle.
   * @param {string} [id] - The ID of the rectangle element.
   * @returns {string} The ID of the added rectangle element.
   */
  addRect(topX: number, topY: number, width: number, height: number, id?: string): string {
    if(!id) id = nanoid();
    this.elementsDict[id] = this.boxedElement(
      id,
      "rectangle",
      topX,
      topY,
      width,
      height,
    );
    return id;
  };

  /**
   * Adds a diamond element to the ExcalidrawAutomate instance.
   * @param {number} topX - The x-coordinate of the top-left corner.
   * @param {number} topY - The y-coordinate of the top-left corner.
   * @param {number} width - The width of the diamond.
   * @param {number} height - The height of the diamond.
   * @param {string} [id] - The ID of the diamond element.
   * @returns {string} The ID of the added diamond element.
   */
  addDiamond(
    topX: number,
    topY: number,
    width: number,
    height: number,
    id?: string,
  ): string {
    if(!id) id = nanoid();
    this.elementsDict[id] = this.boxedElement(
      id,
      "diamond",
      topX,
      topY,
      width,
      height,
    );
    return id;
  };

  /**
   * Adds an ellipse element to the ExcalidrawAutomate instance.
   * @param {number} topX - The x-coordinate of the top-left corner.
   * @param {number} topY - The y-coordinate of the top-left corner.
   * @param {number} width - The width of the ellipse.
   * @param {number} height - The height of the ellipse.
   * @param {string} [id] - The ID of the ellipse element.
   * @returns {string} The ID of the added ellipse element.
   */
  addEllipse(
    topX: number,
    topY: number,
    width: number,
    height: number,
    id?: string,
  ): string {
    if(!id) id = nanoid();
    this.elementsDict[id] = this.boxedElement(
      id,
      "ellipse",
      topX,
      topY,
      width,
      height,
    );
    return id;
  };

  /**
   * Adds a blob element to the ExcalidrawAutomate instance.
   * @param {number} topX - The x-coordinate of the top-left corner.
   * @param {number} topY - The y-coordinate of the top-left corner.
   * @param {number} width - The width of the blob.
   * @param {number} height - The height of the blob.
   * @param {string} [id] - The ID of the blob element.
   * @returns {string} The ID of the added blob element.
   */
  addBlob(topX: number, topY: number, width: number, height: number, id?: string): string {
    const b = height * 0.5; //minor axis of the ellipsis
    const a = width * 0.5; //major axis of the ellipsis
    const sx = a / 9;
    const sy = b * 0.8;
    const step = 6;
    const p: any = [];
    const pushPoint = (i: number, dir: number) => {
      const x = i + Math.random() * sx - sx / 2;
      p.push([
        x + Math.random() * sx - sx / 2 + ((i % 2) * sx) / 6 + topX,
        dir * Math.sqrt(b * b * (1 - (x * x) / (a * a))) +
          Math.random() * sy -
          sy / 2 +
          ((i % 2) * sy) / 6 +
          topY,
      ]);
    };
    let i: number;
    for (i = -a + sx / 2; i <= a - sx / 2; i += a / step) {
      pushPoint(i, 1);
    }
    for (i = a - sx / 2; i >= -a + sx / 2; i -= a / step) {
      pushPoint(i, -1);
    }
    p.push(p[0]);
    const scale = (p: [[x: number, y: number]]): [[x: number, y: number]] => {
      const box = getLineBox(p);
      const scaleX = width / box.w;
      const scaleY = height / box.h;
      let i;
      for (i = 0; i < p.length; i++) {
        let [x, y] = p[i];
        x = (x - box.x) * scaleX + box.x;
        y = (y - box.y) * scaleY + box.y;
        p[i] = [x, y];
      }
      return p;
    };
    id = this.addLine(scale(p), id);
    this.elementsDict[id] = repositionElementsToCursor(
      [this.getElement(id)],
      { x: topX, y: topY },
      false,
    )[0];
    return id;
  };

  /**
   * Refreshes the size of a text element to fit its contents.
   * @param {string} id - The ID of the text element.
   */
  public refreshTextElementSize(id: string) {
    const element = this.getElement(id);
    if (element.type !== "text") {
      return;
    }
    const { w, h } = _measureText(
      element.text,
      element.fontSize,
      element.fontFamily,
      getLineHeight(element.fontFamily)
    );
    element.width = w;
    element.height = h;
  }

  /**
   * Adds a text element to the ExcalidrawAutomate instance.
   * @param {number} topX - The x-coordinate of the top-left corner.
   * @param {number} topY - The y-coordinate of the top-left corner.
   * @param {string} text - The text content of the element.
   * @param {Object} [formatting] - Formatting options for the text element.
   * @param {boolean} [formatting.autoResize=true] - Whether to auto-resize the text element.
   * @param {number} [formatting.wrapAt] - The character length to wrap the text at.
   * @param {number} [formatting.width] - The width of the text element.
   * @param {number} [formatting.height] - The height of the text element.
   * @param {"left" | "center" | "right"} [formatting.textAlign] - The text alignment.
   * @param {boolean | "box" | "blob" | "ellipse" | "diamond"} [formatting.box] - Whether to add a box around the text.
   * @param {number} [formatting.boxPadding] - The padding inside the box.
   * @param {string} [formatting.boxStrokeColor] - The stroke color of the box.
   * @param {"top" | "middle" | "bottom"} [formatting.textVerticalAlign] - The vertical alignment of the text.
   * @param {string} [id] - The ID of the text element.
   * @returns {string} The ID of the added text element.
   */
  addText(
    topX: number,
    topY: number,
    text: string,
    formatting?: {
      autoResize?: boolean; //Default is true. Setting this to false will wrap the text in the text element without the need for the containser. If set to false, you must set a width value as well.
      wrapAt?: number; //wrapAt is ignored if autoResize is set to false (and width is provided)
      width?: number;
      height?: number;
      textAlign?: "left" | "center" | "right";
      box?: boolean | "box" | "blob" | "ellipse" | "diamond";
      boxPadding?: number;
      boxStrokeColor?: string;
      textVerticalAlign?: "top" | "middle" | "bottom";
    },
    id?: string,
  ): string {
    id = id ?? nanoid();
    const originalText = text;
    const autoresize = ((typeof formatting?.width === "undefined") || formatting?.box)
      ? true
      : (formatting?.autoResize ?? true)
    text = (formatting?.wrapAt && autoresize) ? this.wrapText(text, formatting.wrapAt) : text;

    const { w, h } = _measureText(
      text,
      this.style.fontSize,
      this.style.fontFamily,
      getLineHeight(this.style.fontFamily)
    );
    const width = formatting?.width ? formatting.width : w;
    const height = formatting?.height ? formatting.height : h;

    let boxId: string = null;
    const strokeColor = this.style.strokeColor;
    this.style.strokeColor = formatting?.boxStrokeColor ?? strokeColor;
    const boxPadding = formatting?.boxPadding ?? 30;
    if (formatting?.box) {
      switch (formatting.box) {
        case "ellipse":
          boxId = this.addEllipse(
            topX - boxPadding,
            topY - boxPadding,
            width + 2 * boxPadding,
            height + 2 * boxPadding,
          );
          break;
        case "diamond":
          boxId = this.addDiamond(
            topX - boxPadding,
            topY - boxPadding,
            width + 2 * boxPadding,
            height + 2 * boxPadding,
          );
          break;
        case "blob":
          boxId = this.addBlob(
            topX - boxPadding,
            topY - boxPadding,
            width + 2 * boxPadding,
            height + 2 * boxPadding,
          );
          break;
        default:
          boxId = this.addRect(
            topX - boxPadding,
            topY - boxPadding,
            width + 2 * boxPadding,
            height + 2 * boxPadding,
          );
      }
    }
    this.style.strokeColor = strokeColor;
    const isContainerBound = boxId && formatting.box !== "blob";
    this.elementsDict[id] = {
      text,
      fontSize: this.style.fontSize,
      fontFamily: this.style.fontFamily,
      textAlign: formatting?.textAlign
        ? formatting.textAlign
        : this.style.textAlign ?? "left",
      verticalAlign: formatting?.textVerticalAlign ?? this.style.verticalAlign,
      ...this.boxedElement(id, "text", topX, topY, width, height),
      containerId: isContainerBound ? boxId : null,
      originalText: isContainerBound ? originalText : text,
      rawText: isContainerBound ? originalText : text,
      lineHeight: getLineHeight(this.style.fontFamily),
      autoResize: formatting?.box ? true : (formatting?.autoResize ?? true),
    };
    if (boxId && formatting?.box === "blob") {
      this.addToGroup([id, boxId]);
    }
    if (isContainerBound) {
      const box = this.elementsDict[boxId];
      if (!box.boundElements) {
        box.boundElements = [];
      }
      box.boundElements.push({ type: "text", id });
    }
    const textElement = this.getElement(id) as Mutable<ExcalidrawTextElement>;
    const container = (boxId && formatting.box !== "blob") ? this.getElement(boxId) as Mutable<ExcalidrawTextContainer>: undefined;
    const dimensions = refreshTextDimensions(
      textElement,
      container,
      arrayToMap(this.getElements()),
      originalText,
    );

    if(dimensions && !formatting?.width) {

      textElement.width = dimensions.width;
      textElement.height = dimensions.height;
      textElement.x = dimensions.x;
      textElement.y = dimensions.y;
      textElement.text = dimensions.text;
      if(container) {
        container.width = dimensions.width + 2 * boxPadding;
        container.height = dimensions.height + 2 * boxPadding;
      }
    }
    return boxId ?? id;
  };

  /**
   * Adds a line element to the ExcalidrawAutomate instance.
   * @param {[[x: number, y: number]]} points - Array of points defining the line.
   * @param {string} [id] - The ID of the line element.
   * @returns {string} The ID of the added line element.
   */
  addLine(points: [[x: number, y: number]], id?: string): string {
    const box = getLineBox(points);
    id = id ?? nanoid();
    this.elementsDict[id] = {
      points: normalizeLinePoints(points),
      lastCommittedPoint: null,
      startBinding: null,
      endBinding: null,
      startArrowhead: null,
      endArrowhead: null,
      ...this.boxedElement(id, "line", points[0][0], points[0][1], box.w, box.h),
    };
    return id;
  };

  /**
   * Adds an arrow element to the ExcalidrawAutomate instance.
   * @param {[x: number, y: number][]} points - Array of points defining the arrow.
   * @param {Object} [formatting] - Formatting options for the arrow element.
   * @param {"arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null} [formatting.startArrowHead] - The start arrowhead type.
   * @param {"arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null} [formatting.endArrowHead] - The end arrowhead type.
   * @param {string} [formatting.startObjectId] - The ID of the start object.
   * @param {string} [formatting.endObjectId] - The ID of the end object.
   * @param {string} [id] - The ID of the arrow element.
   * @returns {string} The ID of the added arrow element.
   */
  addArrow(
    points: [x: number, y: number][],
    formatting?: {
      startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
      endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
      startObjectId?: string;
      endObjectId?: string;
    },
    id?: string,
  ): string {
    const box = getLineBox(points);
    id = id ?? nanoid();
    const startPoint = points[0] as GlobalPoint;
    const endPoint = points[points.length - 1] as GlobalPoint;
    const elementsMap = arrayToMap(this.getElements());
    this.elementsDict[id] = {
      points: normalizeLinePoints(points),
      lastCommittedPoint: null,
      startBinding: {
        elementId: formatting?.startObjectId,
        focus: formatting?.startObjectId
          ? determineFocusDistance(
              this.getElement(formatting?.startObjectId) as ExcalidrawBindableElement,
              elementsMap,
              endPoint,
              startPoint,
            )
          : 0.1,
        gap: GAP,
      },
      endBinding: {
        elementId: formatting?.endObjectId,
        focus: formatting?.endObjectId
          ? determineFocusDistance(
              this.getElement(formatting?.endObjectId) as ExcalidrawBindableElement,
              elementsMap,
              startPoint,
              endPoint,
            )
          : 0.1,
        gap: GAP,
      },
      //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/388
      startArrowhead:
        typeof formatting?.startArrowHead !== "undefined"
          ? formatting.startArrowHead
          : this.style.startArrowHead,
      endArrowhead:
        typeof formatting?.endArrowHead !== "undefined"
          ? formatting.endArrowHead
          : this.style.endArrowHead,
      ...this.boxedElement(id, "arrow", points[0][0], points[0][1], box.w, box.h),
    };
    if (formatting?.startObjectId) {
      if (!this.elementsDict[formatting.startObjectId].boundElements) {
        this.elementsDict[formatting.startObjectId].boundElements = [];
      }
      this.elementsDict[formatting.startObjectId].boundElements.push({
        type: "arrow",
        id,
      });
    }
    if (formatting?.endObjectId) {
      if (!this.elementsDict[formatting.endObjectId].boundElements) {
        this.elementsDict[formatting.endObjectId].boundElements = [];
      }
      this.elementsDict[formatting.endObjectId].boundElements.push({
        type: "arrow",
        id,
      });
    }
    return id;
  };

  /**
   * Adds a mermaid diagram to ExcalidrawAutomate elements.
   * @param {string} diagram - The mermaid diagram string.
   * @param {boolean} [groupElements=true] - Whether to group the elements.
   * @returns {Promise<string[]|string>} Promise resolving to the IDs of the created elements or an error message.
   */
  async addMermaid(
    diagram: string,
    groupElements: boolean = true,
  ): Promise<string[]|string> {
    const result = await mermaidToExcalidraw(
      diagram, {
        themeVariables: {fontSize: `${this.style.fontSize}`},
        flowchart: {curve: this.style.roundness===null ? "linear" : "basis"},
      }
    );
    const ids:string[] = [];
    if(!result) return null;
    if(result?.error) return result.error;

    if(result?.elements) {
      result.elements.forEach(el=>{
        ids.push(el.id);
        this.elementsDict[el.id] = el;
      })
    }

    if(result?.files) {
      for (const key in result.files) {
        this.imagesDict[key as FileId] = {
          ...result.files[key],
          created: Date.now(),
          isHyperLink: false,
          hyperlink: null,
          file: null,
          hasSVGwithBitmap: false,
          latex: null,
        }
      } 
    }

    if(groupElements && result?.elements && ids.length > 1) {
      this.addToGroup(ids);
    }
    return ids;
  }

  /**
   * Adds an image element to the ExcalidrawAutomate instance.
   * @param {number | AddImageOptions} topXOrOpts - The x-coordinate of the top-left corner or an options object.
   * @param {number} topY - The y-coordinate of the top-left corner.
   * @param {TFile | string} imageFile - The image file or URL.
   * @param {boolean} [scale=true] - Whether to scale the image to MAX_IMAGE_SIZE.
   * @param {boolean} [anchor=true] - Whether to anchor the image at 100% size.
   * @returns {Promise<string>} Promise resolving to the ID of the added image element.
   */
  async addImage(
    topXOrOpts: number | AddImageOptions,
    topY: number,
    imageFile: TFile | string, //string may also be an Obsidian filepath with a reference such as folder/path/my.pdf#page=2
    scale: boolean = true, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
    anchor: boolean = true, //only has effect if scale is false. If anchor is true the image path will include |100%, if false the image will be inserted at 100%, but if resized by the user it won't pop back to 100% the next time Excalidraw is opened.
  ): Promise<string> {

    let colorMap: ColorMap;
    let topX: number;
    if(typeof topXOrOpts === "number") {
      topX = topXOrOpts;
    } else {
      topY = topXOrOpts.topY;
      topX = topXOrOpts.topX;
      imageFile = topXOrOpts.imageFile;
      scale = topXOrOpts.scale ?? true;
      anchor = topXOrOpts.anchor ?? true;
      colorMap = topXOrOpts.colorMap;
    }

    const id = nanoid();
    const loader = new EmbeddedFilesLoader(
      this.plugin,
      this.canvas.theme === "dark",
    );
    const image = (typeof imageFile === "string")
      ? await loader.getObsidianImage(new EmbeddedFile(this.plugin, "", imageFile),0)
      : await loader.getObsidianImage(imageFile,0);
      
    if (!image) {
      return null;
    }
    const fileId = typeof imageFile === "string"
      ? image.fileId
      : imageFile.extension === "md" || imageFile.extension.toLowerCase() === "pdf" ? fileid() as FileId : image.fileId;
    this.imagesDict[fileId] = {
      mimeType: image.mimeType,
      id: fileId,
      dataURL: image.dataURL,
      created: image.created,
      isHyperLink: typeof imageFile === "string",
      hyperlink: typeof imageFile === "string"
        ? imageFile
        : null,
      file: typeof imageFile === "string"
        ? null
        : imageFile.path + (scale || !anchor ? "":"|100%"),
      hasSVGwithBitmap: image.hasSVGwithBitmap,
      latex: null,
      size: { //must have the natural size here (e.g. for PDF cropping)
        height: image.size.height,
        width: image.size.width,
      },
      colorMap,
      pdfPageViewProps: image.pdfPageViewProps,
    };
    if (scale && (Math.max(image.size.width, image.size.height) > MAX_IMAGE_SIZE)) {
      const scale =
        MAX_IMAGE_SIZE / Math.max(image.size.width, image.size.height);
      image.size.width = scale * image.size.width;
      image.size.height = scale * image.size.height;
    }
    this.elementsDict[id] = this.boxedElement(
      id,
      "image",
      topX,
      topY,
      image.size.width,
      image.size.height,
    );
    const newEl = this.elementsDict[id] as Mutable<ExcalidrawImageElement>;
    newEl.fileId = fileId;
    newEl.scale = [1, 1];
    newEl.crop = null;
    if(!scale && anchor) {
      newEl.customData = {isAnchored: true}
    };
    return id;
  };

  /**
   * Adds a LaTeX equation as an image element to the ExcalidrawAutomate instance.
   * @param {number} topX - The x-coordinate of the top-left corner.
   * @param {number} topY - The y-coordinate of the top-left corner.
   * @param {string} tex - The LaTeX equation string.
   * @returns {Promise<string>} Promise resolving to the ID of the added LaTeX image element.
   */
  async addLaTex(topX: number, topY: number, tex: string): Promise<string> {
    const id = nanoid();
    const image = await tex2dataURL(tex, 4, this.plugin);
    if (!image) {
      return null;
    }
    this.imagesDict[image.fileId] = {
      mimeType: image.mimeType,
      id: image.fileId,
      dataURL: image.dataURL,
      created: image.created,
      file: null,
      hasSVGwithBitmap: false,
      latex: tex,
    };
    this.elementsDict[id] = this.boxedElement(
      id,
      "image",
      topX,
      topY,
      image.size.width,
      image.size.height,
    );
    this.elementsDict[id].fileId = image.fileId;
    this.elementsDict[id].scale = [1, 1];
    return id;
  };

  /**
   * Returns the base64 dataURL of the LaTeX equation rendered as an SVG.
   * @param {string} tex - The LaTeX equation string.
   * @param {number} [scale=4] - The scale factor for the image.
   * @returns {Promise<{mimeType: MimeType; fileId: FileId; dataURL: DataURL; created: number; size: { height: number; width: number };}>} Promise resolving to the LaTeX image data.
   */
  async tex2dataURL(
    tex: string,
    scale: number = 4 // Default scale value, adjust as needed
  ): Promise<{
    mimeType: MimeType;
    fileId: FileId;
    dataURL: DataURL;
    created: number;
    size: { height: number; width: number };
  }> {
    return await tex2dataURL(tex,scale, this.plugin);
  };

  /**
   * Connects two objects with an arrow.
   * @param {string} objectA - The ID of the first object.
   * @param {ConnectionPoint | null} connectionA - The connection point on the first object.
   * @param {string} objectB - The ID of the second object.
   * @param {ConnectionPoint | null} connectionB - The connection point on the second object.
   * @param {Object} [formatting] - Formatting options for the arrow.
   * @param {number} [formatting.numberOfPoints=0] - The number of points on the arrow.
   * @param {"arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null} [formatting.startArrowHead] - The start arrowhead type.
   * @param {"arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null} [formatting.endArrowHead] - The end arrowhead type.
   * @param {number} [formatting.padding=10] - The padding around the arrow.
   * @returns {string} The ID of the added arrow element.
   */
  connectObjects(
    objectA: string,
    connectionA: ConnectionPoint | null,
    objectB: string,
    connectionB: ConnectionPoint | null,
    formatting?: {
      numberOfPoints?: number;
      startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
      endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
      padding?: number;
    },
  ): string {
    if (!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
      return;
    }

    if (
      ["line", "arrow", "freedraw"].includes(
        this.elementsDict[objectA].type,
      ) ||
      ["line", "arrow", "freedraw"].includes(this.elementsDict[objectB].type)
    ) {
      return;
    }

    const padding = formatting?.padding ? formatting.padding : 10;
    const numberOfPoints = formatting?.numberOfPoints
      ? formatting.numberOfPoints
      : 0;
    const getSidePoints = (side: string, el: any) => {
      switch (side) {
        case "bottom":
          return [(el.x + (el.x + el.width)) / 2, el.y + el.height + padding];
        case "left":
          return [el.x - padding, (el.y + (el.y + el.height)) / 2];
        case "right":
          return [el.x + el.width + padding, (el.y + (el.y + el.height)) / 2];
        default:
          //"top"
          return [(el.x + (el.x + el.width)) / 2, el.y - padding];
      }
    };
    let aX;
    let aY;
    let bX;
    let bY;
    const elA = this.elementsDict[objectA];
    const elB = this.elementsDict[objectB];
    if (!connectionA || !connectionB) {
      const aCenterX = elA.x + elA.width / 2;
      const bCenterX = elB.x + elB.width / 2;
      const aCenterY = elA.y + elA.height / 2;
      const bCenterY = elB.y + elB.height / 2;
      if (!connectionA) {
        const intersect = intersectElementWithLine(
          elA,
          [bCenterX, bCenterY] as GlobalPoint,
          [aCenterX, aCenterY] as GlobalPoint,
          GAP,
        );
        if (intersect.length === 0) {
          [aX, aY] = [aCenterX, aCenterY];
        } else {
          [aX, aY] = intersect[0];
        }
      }

      if (!connectionB) {
        const intersect = intersectElementWithLine(
          elB,
          [aCenterX, aCenterY] as GlobalPoint,
          [bCenterX, bCenterY] as GlobalPoint,
          GAP,
        );
        if (intersect.length === 0) {
          [bX, bY] = [bCenterX, bCenterY];
        } else {
          [bX, bY] = intersect[0];
        }
      }
    }
    if (connectionA) {
      [aX, aY] = getSidePoints(connectionA, this.elementsDict[objectA]);
    }
    if (connectionB) {
      [bX, bY] = getSidePoints(connectionB, this.elementsDict[objectB]);
    }
    const numAP = numberOfPoints + 2; //number of break points plus the beginning and the end
    const points:[x:number, y:number][] = [];
    for (let i = 0; i < numAP; i++) {
      points.push([
        aX + (i * (bX - aX)) / (numAP - 1),
        aY + (i * (bY - aY)) / (numAP - 1),
      ]);
    }
    return this.addArrow(points, {
      startArrowHead: formatting?.startArrowHead,
      endArrowHead: formatting?.endArrowHead,
      startObjectId: objectA,
      endObjectId: objectB,
    });
  };

  /**
   * Adds a text label to a line or arrow. Currently only works with a straight (2 point - start & end - line).
   * @param {string} lineId - The ID of the line or arrow object.
   * @param {string} label - The label text.
   * @returns {string} The ID of the added text element.
   */
  addLabelToLine(lineId: string, label: string): string {
    const line = this.elementsDict[lineId];
    if(!line || !["arrow","line"].includes(line.type) || line.points.length !== 2) {
      return;
    }

    let angle = Math.atan2(line.points[1][1],line.points[1][0]);

    const size = this.measureText(label);
    //let delta = size.height/6;

    if(angle < 0) {
      if(angle < -Math.PI/2) {
        angle+= Math.PI;
      } /*else {
        delta = -delta;
      } */
    } else {
      if(angle > Math.PI/2) {
        angle-= Math.PI;
        //delta = -delta;
      }
    }
    this.style.angle = angle;
    const id = this.addText(
      line.x+line.points[1][0]/2-size.width/2,//+delta,
      line.y+line.points[1][1]/2-size.height,//-5*size.height/6,
      label
    );
    this.style.angle = 0;
    return id;
  }

  /**
   * Clears elementsDict and imagesDict only.
   */
  clear():void {
    this.elementsDict = {};
    this.imagesDict = {};
  };

  /**
   * Clears elementsDict and imagesDict, and resets all style values to default.
   */
  reset():void {
    this.clear();
    this.activeScript = null;
    this.style = {
      strokeColor: "#000000",
      backgroundColor: "transparent",
      angle: 0,
      fillStyle: "hachure",
      strokeWidth: 1,
      strokeStyle: "solid",
      roughness: 1,
      opacity: 100,
      roundness: null,
      fontFamily: 1,
      fontSize: 20,
      textAlign: "left",
      verticalAlign: "top",
      startArrowHead: null,
      endArrowHead: "arrow"
    };
    this.canvas = {
      theme: "light",
      viewBackgroundColor: "#FFFFFF",
      gridSize: 0
    };
  };

  /**
   * Returns true if the provided file is an Excalidraw file.
   * @param {TFile} f - The file to check.
   * @returns {boolean} True if the file is an Excalidraw file, false otherwise.
   */
  isExcalidrawFile(f: TFile): boolean {
    return this.plugin.isExcalidrawFile(f);
  };
  targetView: ExcalidrawView = null; //the view currently edited
  /**
   * Sets the target view for EA. All the view operations and the access to Excalidraw API will be performed on this view.
   * If view is null or undefined, the function will first try setView("active"), then setView("first").
   * @param {ExcalidrawView | "first" | "active"} [view] - The view to set as target.
   * @returns {ExcalidrawView} The target view.
   */
  setView(view?: ExcalidrawView | "first" | "active"): ExcalidrawView {
    if(!view) {
      const v = this.plugin.app.workspace.getActiveViewOfType(ExcalidrawView);
      if (v instanceof ExcalidrawView) {
        this.targetView = v;
      }
      else {
        this.targetView = getExcalidrawViews(this.plugin.app)[0];
      }
    }
    if (view == "active") {
      const v = this.plugin.app.workspace.getActiveViewOfType(ExcalidrawView);
      if (!(v instanceof ExcalidrawView)) {
        return;
      }
      this.targetView = v;
    }
    if (view == "first") {
      this.targetView = getExcalidrawViews(this.plugin.app)[0];
    }
    if (view instanceof ExcalidrawView) {
      this.targetView = view;
    }
    return this.targetView;
  };

  /**
   * Returns the Excalidraw API for the current view.
   * @returns {any} The Excalidraw API.
   */
  getExcalidrawAPI(): any {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "getExcalidrawAPI()");
      return null;
    }
    return (this.targetView as ExcalidrawView).excalidrawAPI;
  };

  /**
   * Gets elements in the current view.
   * @returns {ExcalidrawElement[]} Array of elements in the view.
   */
  getViewElements(): ExcalidrawElement[] {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "getViewElements()");
      return [];
    }
    return this.targetView.getViewElements();
  };

  /**
   * Deletes elements in the view by removing them from the scene (not by setting isDeleted to true).
   * @param {ExcalidrawElement[]} elToDelete - Array of elements to delete.
   * @returns {boolean} True if elements were deleted, false otherwise.
   */
  deleteViewElements(elToDelete: ExcalidrawElement[]): boolean {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "deleteViewElements()");
      return false;
    }
    const api = this.targetView?.excalidrawAPI as ExcalidrawImperativeAPI;
    if (!api) {
      return false;
    }
    const ids = elToDelete.map((e: ExcalidrawElement) => e.id);
    const el: ExcalidrawElement[] = api.getSceneElements() as ExcalidrawElement[];
    const st: AppState = api.getAppState();
    this.targetView.updateScene({
      elements: el.filter((e: ExcalidrawElement) => !ids.includes(e.id)),
      appState: st,
      captureUpdate: CaptureUpdateAction.IMMEDIATELY,
    });
    //this.targetView.save();
    return true;
  };

  /**
   * Adds a back of the note card to the current active view.
   * @param {string} sectionTitle - The title of the section.
   * @param {boolean} [activate=true] - Whether to activate the new Embedded Element after creation.
   * @param {string} [sectionBody] - The body of the section.
   * @param {EmbeddableMDCustomProps} [embeddableCustomData] - Custom properties for the embeddable element.
   * @returns {Promise<string>} Promise resolving to the ID of the embeddable element.
   */
  async addBackOfTheCardNoteToView(sectionTitle: string, activate: boolean = false, sectionBody?: string, embeddableCustomData?: EmbeddableMDCustomProps): Promise<string> {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "addBackOfTheCardNoteToView()");
      return null;
    }
    await this.targetView.forceSave(true);
    return addBackOfTheNoteCard(this.targetView, sectionTitle, activate, sectionBody, embeddableCustomData);
  }

  /**
   * Gets the selected element in the view. If more are selected, gets the first.
   * @returns {any} The selected element or null if none selected.
   */
  getViewSelectedElement(): any {
    const elements = this.getViewSelectedElements();
    return elements ? elements[0] : null;
  };

  /**
   * Gets the selected elements in the view.
   * @param {boolean} [includeFrameChildren=true] - Whether to include frame children in the selection.
   * @returns {any[]} Array of selected elements.
   */
  getViewSelectedElements(includeFrameChildren:boolean = true): any[] {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "getViewSelectedElements()");
      return [];
    }
    return this.targetView.getViewSelectedElements(includeFrameChildren);
  };

  /**
   * Gets the file associated with an image element in the view.
   * @param {ExcalidrawElement} el - The image element.
   * @returns {TFile | null} The file associated with the image element or null if not found.
   */
  getViewFileForImageElement(el: ExcalidrawElement): TFile | null {
    return getEmbeddedFileForImageElment(this,el)?.file;
  };

  /**
   * Gets the color map associated with an image element in the view.
   * @param {ExcalidrawElement} el - The image element.
   * @returns {ColorMap} The color map associated with the image element.
   */
  getColorMapForImageElement(el: ExcalidrawElement): ColorMap {
    const cm = getEmbeddedFileForImageElment(this,el)?.colorMap 
    if(!cm) {
      return {};
    }
    return cm;
  }

  /**
   * Updates the color map of SVG images in the view.
   * @param {ExcalidrawImageElement | ExcalidrawImageElement[]} elements - The image elements to update.
   * @param {ColorMap | SVGColorInfo | ColorMap[] | SVGColorInfo[]} colors - The new color map(s) for the images.
   * @returns {Promise<void>} Promise resolving when the update is complete.
   */
  async updateViewSVGImageColorMap(
    elements: ExcalidrawImageElement | ExcalidrawImageElement[],
    colors: ColorMap | SVGColorInfo | ColorMap[] | SVGColorInfo[]
  ): Promise<void> {
    const elementArray = Array.isArray(elements) ? elements : [elements];
    const colorArray = Array.isArray(colors) ? colors : [colors];
    let colorMaps: ColorMap[];

    if(colorArray.length !== elementArray.length) {
      errorMessage("Elements and colors arrays must have same length", "updateViewSVGImageColorMap()");
      return;
    }
    
    if (isSVGColorInfo(colorArray[0])) {
        colorMaps = (colors as SVGColorInfo[]).map(svgColorInfoToColorMap);
      } else {
        colorMaps = colors as ColorMap[];
      }

    const fileIDWhiteList = new Set<FileId>();
    for(let i = 0; i < elementArray.length; i++) {
      const el = elementArray[i];
      const colorMap = filterColorMap(colorMaps[i]);

      const ef = getEmbeddedFileForImageElment(this, el);
      if (!ef || !ef.file || !colorMap) {
        errorMessage("Must provide an image element and a colorMap as input", "updateViewSVGImageColorMap()");
        continue;
      }
      if (!colorMap || typeof colorMap !== 'object' || Object.keys(colorMap).length === 0) {
        ef.colorMap = null;
      } else {
        ef.colorMap = colorMap;
        //delete special mappings for default/SVG root color values
        if (ef.colorMap["fill"] === "black") {
          delete ef.colorMap["fill"];
        }
        if (ef.colorMap["stroke"] === "none") {
          delete ef.colorMap["stroke"];
        }
      }
      ef.resetImage(this.targetView.file.path, ef.linkParts.original);
      fileIDWhiteList.add(el.fileId);
    }

    if(fileIDWhiteList.size > 0) {
      this.targetView.setDirty();
      await new Promise<void>((resolve) => {
        this.targetView.loadSceneFiles(
          false,
          fileIDWhiteList,
          resolve
        );
      });
    }
    return;
  };

  /**
   * Gets the SVG color information for an image element in the view.
   * @param {ExcalidrawElement} el - The image element.
   * @returns {Promise<SVGColorInfo>} Promise resolving to the SVG color information.
   */
  async getSVGColorInfoForImgElement(el: ExcalidrawElement): Promise<SVGColorInfo> {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "getViewFileForImageElement()");
      return;
    }

    if (!el || el.type !== "image") {
      errorMessage(
        "Must provide an image element as input",
        "getViewFileForImageElement()",
      );
      return;
    }
    const ef = getEmbeddedFileForImageElment(this, el);

    const file = ef?.file;
    if(!file || !(file.extension === "svg" || this.isExcalidrawFile(file))) {
      errorMessage("Must provide an SVG or nested Excalidraw image element as input", "getColorMapForImgElement()");
      return;
    }

    if (file.extension === "svg") {
      const svgString = await this.plugin.app.vault.cachedRead(file);
      const svgColors = this.getColorsFromSVGString(svgString);
      return mergeColorMapIntoSVGColorInfo(ef.colorMap, svgColors);
    }
    const svgColors = await this.getColosFromExcalidrawFile(file, el);
    return mergeColorMapIntoSVGColorInfo(ef.colorMap, svgColors);
  };

  /**
   * Gets the color information from an Excalidraw file.
   * @param {TFile} file - The Excalidraw file.
   * @param {ExcalidrawImageElement} img - The image element.
   * @returns {Promise<SVGColorInfo>} Promise resolving to the SVG color information.
   */
  async getColosFromExcalidrawFile(file:TFile, img: ExcalidrawImageElement): Promise<SVGColorInfo> {
    if(!file || !this.isExcalidrawFile(file)) {
      errorMessage("Must provide an Excalidraw file as input", "getColosFromExcalidrawFile()");
      return;
    }
    const ef = getEmbeddedFileForImageElment(this, img);

    const ed = new ExcalidrawData(this.plugin);
    if(file.extension === "excalidraw") {
      await ed.loadLegacyData(await this.plugin.app.vault.cachedRead(file), file);
    } else {  
      await ed.loadData(await this.plugin.app.vault.cachedRead(file), file,TextMode.raw);
    }
    const svgColors: SVGColorInfo = new Map();
    if (!ed.loaded) {
      return svgColors;
    }
    ed.scene.elements.forEach((el:ExcalidrawElement) => {
      if("strokeColor" in el) {
        updateOrAddSVGColorInfo(svgColors, el.strokeColor, {stroke: true});
      }
      if("backgroundColor" in el) {
        updateOrAddSVGColorInfo(svgColors, el.backgroundColor, {fill: true});
      }
    });
    return svgColors;
  }

  /**
   * Extracts color information from an SVG string.
   * @param {string} svgString - The SVG string.
   * @returns {SVGColorInfo} The extracted color information.
   */
  getColorsFromSVGString(svgString: string): SVGColorInfo {
    const colorMap = new Map<string, {mappedTo: string, fill: boolean, stroke: boolean}>();

    if(!svgString) {
      return colorMap;
    }

    const parser = new DOMParser();
    const doc = parser.parseFromString(svgString, "image/svg+xml");
    
    // Function to process an element and extract its colors
    function processElement(element: Element, isRoot = false) {
      // Check for fill attribute
      const fillColor = element.getAttribute("fill");
      if (fillColor !== "none") {
        if (fillColor) {
          updateOrAddSVGColorInfo(colorMap, fillColor, {fill: true});
        } else if (isRoot) {
          // If the root element has no fill, assume it is white
          updateOrAddSVGColorInfo(colorMap, "fill", {fill: true, mappedTo: "black"});
        }
      }
      
      // Check for stroke attribute
      const strokeColor = element.getAttribute("stroke");
      if (strokeColor && strokeColor !== "none") {
        updateOrAddSVGColorInfo(colorMap, strokeColor, {stroke: true});
      }
      
      // Check for style attribute that might contain fill or stroke
      const style = element.getAttribute("style");
      if (style) {
        // Extract fill from style
        const fillMatch = style.match(/fill:\s*([^;}\s]+)/);
        if (fillMatch && fillMatch[1] !== "none") {
          updateOrAddSVGColorInfo(colorMap, fillMatch[1], {fill: true});
        }
        
        // Extract stroke from style
        const strokeMatch = style.match(/stroke:\s*([^;}\s]+)/);
        if (strokeMatch && strokeMatch[1] !== "none") {
          updateOrAddSVGColorInfo(colorMap, strokeMatch[1], {stroke: true});
        }
      }
      
      // Recursively process child elements
      for (const child of Array.from(element.children)) {
        processElement(child);
      }
    }
    
    // Process the root SVG element
    const svgElement = doc.documentElement;
    processElement(svgElement, true);
    
    return colorMap;
  }

  /**
   * Copies elements from the view to elementsDict for editing.
   * @param {ExcalidrawElement[]} elements - Array of elements to copy.
   * @param {boolean} [copyImages=false] - Whether to copy images as well.
   */
  copyViewElementsToEAforEditing(elements: ExcalidrawElement[], copyImages: boolean = false): void {
    if(copyImages && elements.some(el=>el.type === "image")) {
      if (!this.targetView || !this.targetView?._loaded) {
        errorMessage("targetView not set", "copyViewElementsToEAforEditing()");
        return;
      }
      const sceneFiles = this.targetView.getScene().files;
      elements.forEach((el) => {
        this.elementsDict[el.id] = cloneElement(el);
        if(el.type === "image") {
          const ef = this.targetView.excalidrawData.getFile(el.fileId);
          const imageWithRef = ef && ef.file && ef.linkParts && ef.linkParts.ref;
          const equation = this.targetView.excalidrawData.getEquation(el.fileId);
          const sceneFile = sceneFiles?.[el.fileId];
          this.imagesDict[el.fileId] = {
            mimeType: sceneFile.mimeType,
            id: el.fileId,
            dataURL: sceneFile.dataURL,
            created: sceneFile.created,
            hasSVGwithBitmap: ef ? ef.isSVGwithBitmap : false,
            ...ef ? {
              isHyperLink: ef.isHyperLink || Boolean(imageWithRef),
              hyperlink: imageWithRef ? `${ef.file.path}#${ef.linkParts.ref}` : ef.hyperlink,
              file: imageWithRef ? null : ef.file,
              latex: null,
            } : {},
            ...equation ? {
              file: null,
              isHyperLink: false,
              hyperlink: null,
              latex: equation.latex,
            } : {},
          }
        }
      });
    } else {
      elements.forEach((el) => {
        this.elementsDict[el.id] = cloneElement(el);
      });
    }
  };

  /**
   * Toggles full screen mode for the target view.
   * @param {boolean} [forceViewMode=false] - Whether to force view mode.
   */
  viewToggleFullScreen(forceViewMode: boolean = false): void {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "viewToggleFullScreen()");
      return;
    }
    const view = this.targetView as ExcalidrawView;
    const isFullscreen = view.isFullscreen();
    if (forceViewMode) {
      view.updateScene({
        //elements: ref.getSceneElements(),
        appState: {
          viewModeEnabled: !isFullscreen,
        },
        captureUpdate: CaptureUpdateAction.NEVER,
      });
      this.targetView.toolsPanelRef?.current?.setExcalidrawViewMode(!isFullscreen);
    }
    
    if (isFullscreen) {
      view.exitFullscreen();
    } else {
      view.gotoFullscreen();
    }
  };

  /**
   * Sets view mode enabled or disabled for the target view.
   * @param {boolean} enabled - Whether to enable view mode.
   */
  setViewModeEnabled(enabled: boolean): void {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "viewToggleFullScreen()");
      return;
    }
    const view = this.targetView as ExcalidrawView;
    view.updateScene({appState:{viewModeEnabled: enabled}, captureUpdate: CaptureUpdateAction.NEVER});
    view.toolsPanelRef?.current?.setExcalidrawViewMode(enabled);
  }

  /**
   * Updates the scene in the target view.
   * @param {Object} scene - The scene to load to Excalidraw.
   * @param {ExcalidrawElement[]} [scene.elements] - Array of elements in the scene.
   * @param {AppState} [scene.appState] - The app state of the scene.
   * @param {BinaryFileData} [scene.files] - The files in the scene.
   * @param {boolean} [scene.commitToHistory] - Whether to commit the scene to history. @deprecated Use scene.storageOption instead
   * @param {"capture" | "none" | "update"} [scene.storeAction] - The store action for the scene. @deprecated Use scene.storageOption instead
   * @param {"IMMEDIATELY" | "NEVER" | "EVENTUALLY"} [scene.captureUpdate] - The capture update action for the scene.
   * @param {boolean} [restore=false] - Whether to restore legacy elements in the scene.
   */
  viewUpdateScene (
    scene: {
      elements?: ExcalidrawElement[],
      appState?: AppState | {},
      files?: BinaryFileData,
      commitToHistory?: boolean,
      storeAction?: "capture" | "none" | "update",
      captureUpdate?: SceneData["captureUpdate"],
    },
    restore: boolean = false,
  ):void {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "viewToggleFullScreen()");
      return;
    }
    if (!Boolean(scene.storeAction)) {
      scene.storeAction = scene.commitToHistory ? "capture" : "update";  
    }

    this.targetView.updateScene({
      elements: scene.elements,
      appState: scene.appState,
      files: scene.files,
      storeAction: scene.storeAction,
      captureUpdate: scene.captureUpdate,
    },restore);
  }

  /**
   * Connects an object to the selected element in the view.
   * @param {string} objectA - The ID of the first object.
   * @param {ConnectionPoint | null} connectionA - The connection point on the first object.
   * @param {ConnectionPoint | null} connectionB - The connection point on the selected element.
   * @param {Object} [formatting] - Formatting options for the arrow.
   * @param {number} [formatting.numberOfPoints=0] - The number of points on the arrow.
   * @param {"arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null} [formatting.startArrowHead] - The start arrowhead type.
   * @param {"arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null} [formatting.endArrowHead] - The end arrowhead type.
   * @param {number} [formatting.padding=10] - The padding around the arrow.
   * @returns {boolean} True if the connection was successful, false otherwise.
   */
  connectObjectWithViewSelectedElement(
    objectA: string,
    connectionA: ConnectionPoint | null,
    connectionB: ConnectionPoint | null,
    formatting?: {
      numberOfPoints?: number;
      startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
      endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
      padding?: number;
    },
  ): boolean {
    const el = this.getViewSelectedElement();
    if (!el) {
      return false;
    }
    const id = el.id;
    this.elementsDict[id] = el;
    this.connectObjects(objectA, connectionA, id, connectionB, formatting);
    delete this.elementsDict[id];
    return true;
  };

  /**
   * Zooms the target view to fit the specified elements.
   * @param {boolean} selectElements - Whether to select the elements after zooming.
   * @param {ExcalidrawElement[]} elements - Array of elements to zoom to.
   */
  viewZoomToElements(
    selectElements: boolean,
    elements: ExcalidrawElement[]
  ):void {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "viewToggleFullScreen()");
      return;
    }
    this.targetView.zoomToElements(selectElements,elements);
  }

  /**
   * Adds elements from elementsDict to the current view.
   * @param {boolean} [repositionToCursor=false] - Whether to reposition the elements to the cursor.
   * @param {boolean} [save=true] - Whether to save the changes.
   * @param {boolean} [newElementsOnTop=false] - Whether to add new elements on top of existing elements.
   * @param {boolean} [shouldRestoreElements=false] - Whether to restore legacy elements in the scene.
   * @returns {Promise<boolean>} Promise resolving to true if elements were added, false otherwise.
   */
  async addElementsToView(
    repositionToCursor: boolean = false,
    save: boolean = true,
    newElementsOnTop: boolean = false,
    shouldRestoreElements: boolean = false,
  ): Promise<boolean> {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "addElementsToView()");
      return false;
    }
    const elements = this.getElements();
    if(elements.some(el=>el.type === "embeddable")) {
      patchMobileView(this.targetView);
    }
    const result = await this.targetView.addElements({
      newElements: elements,
      repositionToCursor,
      save,
      images: this.imagesDict,
      newElementsOnTop,
      shouldRestoreElements,
    });
    return result;
  };

  /**
   * Registers this instance of EA to use for hooks with the target view.
   * By default, ExcalidrawViews will check window.ExcalidrawAutomate for event hooks.
   * Using this method, you can set a different instance of Excalidraw Automate for hooks.
   * @returns {boolean} True if successful, false otherwise.
   */
  registerThisAsViewEA():boolean {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "addElementsToView()");
      return false;
    }
    this.targetView.setHookServer(this);
    return true;
  }

  /**
   * Sets the target view EA to window.ExcalidrawAutomate.
   * @returns {boolean} True if successful, false otherwise.
   */
  deregisterThisAsViewEA():boolean {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "addElementsToView()");
      return false;
    }
    this.targetView.setHookServer(this);
    return true;
  }

  /**
   * If set, this callback is triggered when the user closes an Excalidraw view.
   */
  onViewUnloadHook: (view: ExcalidrawView) => void = null;

  /**
   * If set, this callback is triggered, when the user changes the view mode.
   * You can use this callback in case you want to do something additional when the user switches to view mode and back.
   */
  onViewModeChangeHook: (isViewModeEnabled:boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void = null;

   /**
   * If set, this callback is triggered, when the user hovers a link in the scene.
   * You can use this callback in case you want to do something additional when the onLinkHover event occurs.
   * This callback must return a boolean value.
   * In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
   */
  onLinkHoverHook: (
    element: NonDeletedExcalidrawElement,
    linkText: string,
    view: ExcalidrawView,
    ea: ExcalidrawAutomate
  ) => boolean = null;

   /**
   * If set, this callback is triggered, when the user clicks a link in the scene.
   * You can use this callback in case you want to do something additional when the onLinkClick event occurs.
   * This callback must return a boolean value.
   * In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow.
   */
  onLinkClickHook:(
    element: ExcalidrawElement,
    linkText: string,
    event: MouseEvent,
    view: ExcalidrawView,
    ea: ExcalidrawAutomate
  ) => boolean = null;

  /**
   * If set, this callback is triggered, when Excalidraw receives an onDrop event. 
   * You can use this callback in case you want to do something additional when the onDrop event occurs.
   * This callback must return a boolean value.
   * In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow.
   */
  onDropHook: (data: {
    ea: ExcalidrawAutomate;
    event: React.DragEvent<HTMLDivElement>;
    draggable: any; //Obsidian draggable object
    type: "file" | "text" | "unknown";
    payload: {
      files: TFile[]; //TFile[] array of dropped files
      text: string; //string
    };
    excalidrawFile: TFile; //the file receiving the drop event
    view: ExcalidrawView; //the excalidraw view receiving the drop
    pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop
  }) => boolean = null;
 
  /**
   * If set, this callback is triggered, when Excalidraw receives an onPaste event.
   * You can use this callback in case you want to do something additional when the
   * onPaste event occurs.
   * This callback must return a boolean value.
   * In case you want to prevent the excalidraw onPaste action you must return false,
   * it will stop the native excalidraw onPaste management flow.
   */
   onPasteHook: (data: {
    ea: ExcalidrawAutomate;
    payload: ClipboardData;
    event: ClipboardEvent;
    excalidrawFile: TFile; //the file receiving the paste event
    view: ExcalidrawView; //the excalidraw view receiving the paste
    pointerPosition: { x: number; y: number }; //the pointer position on canvas
   }) => boolean = null;

  /**
   * If set, this callback is triggered when a image is being saved in Excalidraw.
   * You can use this callback to customize the naming and path of pasted images to avoid
   * default names like "Pasted image 123147170.png" being saved in the attachments folder,
   * and instead use more meaningful names based on the Excalidraw file or other criteria,
   * plus save the image in a different folder.
   * 
   * If the function returns null or undefined, the normal Excalidraw operation will continue
   * with the excalidraw generated name and default path.
   * If a filepath is returned, that will be used. Include the full Vault filepath and filename
   * with the file extension.
   * The currentImageName is the name of the image generated by excalidraw or provided during paste.
   * 
   * @param data - An object containing the following properties:
   *   @property {string} [currentImageName] - Default name for the image.
   *   @property {string} drawingFilePath - The file path of the Excalidraw file where the image is being used.
   * 
   * @returns {string} - The new filepath for the image including full vault path and extension.
   * 
   * Example usage:
   * ```
   * onImageFilePathHook: (data) => {
   *   const { currentImageName, drawingFilePath } = data;
   *   // Generate a new filepath based on the drawing file name and other criteria
   *   const ext = currentImageName.split('.').pop();
   *   return `${drawingFileName} - ${currentImageName || 'image'}.${ext}`;
   * }
   * ```
   */
   onImageFilePathHook: (data: {
    currentImageName: string; // Excalidraw generated name of the image, or the name received from the file system.
    drawingFilePath: string; // The full filepath of the Excalidraw file where the image is being used.
  }) => string | null = null;  

  /**
   * If set, this callback is triggered when the Excalidraw image is being exported to 
   * .svg, .png, or .excalidraw.
   * You can use this callback to customize the naming and path of the images. This allows
   * you to place images into an assets folder.
   * 
   * If the function returns null or undefined, the normal Excalidraw operation will continue
   * with the currentImageName and in the same folder as the Excalidraw file
   * If a filepath is returned, that will be used. Include the full Vault filepath and filename
   * with the file extension.
   * If the new folder path does not exist, excalidraw will create it - you don't need to worry about that.
   * ⚠️⚠️If an image already exists on the path, that will be overwritten. When returning
   * your own image path, you must take care of unique filenames (if that is a requirement) ⚠️⚠️
   * The current image name is the name generated by Excalidraw:
   * - my-drawing.png
   * - my-drawing.svg
   * - my-drawing.excalidraw
   * - my-drawing.dark.svg
   * - my-drawing.light.svg
   * - my-drawing.dark.png
   * - my-drawing.light.png
   * 
   * @param data - An object containing the following properties:
   *   @property {string} exportFilepath - Default export filepath for the image.
   *   @property {string} exportExtension - The file extension of the export (e.g., .dark.svg, .png, .excalidraw).
   *   @property {string} excalidrawFile - TFile: The Excalidraw file being exported.
   *   @property {string} oldExcalidrawPath - If action === "move" The old path of the Excalidraw file, else undefined
   *   @property {string} action - The action being performed: "export", "move", or "delete". move and delete reference the change to the Excalidraw file.
   * 
   * @returns {string} - The new filepath for the image including full vault path and extension.
   * 
   * Example usage:
   * ```
   * onImageFilePathHook: (data) => {
   *   const { currentImageName, drawingFilePath, frontmatter } = data;
   *   // Generate a new filepath based on the drawing file name and other criteria
   *   const ext = currentImageName.split('.').pop();
   *   if(frontmatter && frontmatter["my-custom-field"]) {
   *   }
   *   return `${drawingFileName} - ${currentImageName || 'image'}.${ext}`;
   * }
   * ```
   */
  onImageExportPathHook: (data: {
    exportFilepath: string; // Default export filepath for the image.
    exportExtension: string; // The file extension of the export (e.g., .dark.svg, .png, .excalidraw).
    excalidrawFile: TFile; // The Excalidraw file being exported.
    oldExcalidrawPath? : string; // The old path of the Excalidraw file, if it was moved/renamed. 
    action: "export" | "move" | "delete";
  }) => string | null = null;

  /**
   * Excalidraw supports auto-export of Excalidraw files to .png, .svg, and .excalidraw formats.
   * 
   * Auto-export of Excalidraw files can be controlled at multiple levels.
   * 1) In plugin settings where you can set up default auto-export applicable to all your Excalidraw files.
   * 2) However, if you do not want to auto-export every file, you can also control auto-export
   *    at the file level using the 'excalidraw-autoexport' frontmatter property.
   * 3) This hook gives you an additional layer of control over the auto-export process.
   * 
   * This hook is triggered when an Excalidraw file is being saved.
   * 
   * interface AutoexportConfig {
   *   png: boolean; // Whether to auto-export to PNG
   *   svg: boolean; // Whether to auto-export to SVG
   *   excalidraw: boolean; // Whether to auto-export to Excalidraw format
   *   theme: "light" | "dark" | "both"; // The theme to use for the export
   * }
   *
   * @param {Object} data - The data for the hook.
   * @param {AutoexportConfig} data.autoexportConfig - The current autoexport configuration.
   * @param {TFile} data.excalidrawFile - The Excalidraw file being auto-exported.
   * @returns {AutoexportConfig | null} - Return a modified AutoexportConfig to override the export behavior, or null to use the default.
   */
  onTriggerAutoexportHook: (data: {
    autoexportConfig: AutoexportConfig;
    excalidrawFile: TFile; // The Excalidraw file being auto-exported
  }) => AutoexportConfig | null = null;

  /**
   * if set, this callback is triggered, when an Excalidraw file is opened
   * You can use this callback in case you want to do something additional when the file is opened.
   * This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
   */
  onFileOpenHook: (data: {
    ea: ExcalidrawAutomate;
    excalidrawFile: TFile; //the file being loaded
    view: ExcalidrawView;
  }) => Promise<void>;


  /**
   * if set, this callback is triggered, when an Excalidraw file is created
   * see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
   */
  onFileCreateHook: (data: {
    ea: ExcalidrawAutomate;
    excalidrawFile: TFile; //the file being created
    view: ExcalidrawView;
  }) => Promise<void>;
    

  /**
   * If set, this callback is triggered whenever the active canvas color changes.
   * @param {ExcalidrawAutomate} ea - The ExcalidrawAutomate instance.
   * @param {ExcalidrawView} view - The Excalidraw view.
   * @param {string} color - The new canvas color.
   */
  onCanvasColorChangeHook: (
    ea: ExcalidrawAutomate,
    view: ExcalidrawView, //the excalidraw view 
    color: string,
  ) => void = null;

  /**
   * If set, this callback is triggered whenever a drawing is exported to SVG.
   * The string returned will replace the link in the exported SVG.
   * The hook is only executed if the link is to a file internal to Obsidian.
   * @param {Object} data - The data for the hook.
   * @param {string} data.originalLink - The original link in the SVG.
   * @param {string} data.obsidianLink - The Obsidian link in the SVG.
   * @param {TFile | null} data.linkedFile - The linked file in Obsidian.
   * @param {TFile} data.hostFile - The host file in Obsidian.
   * @returns {string} The updated link for the SVG.
   */
  onUpdateElementLinkForExportHook: (data: {
    originalLink: string,
    obsidianLink: string,
    linkedFile: TFile | null,
    hostFile: TFile,
  }) => string = null;

  /**
   * Utility function to generate EmbeddedFilesLoader object.
   * @param {boolean} [isDark] - Whether to use dark mode.
   * @returns {EmbeddedFilesLoader} The EmbeddedFilesLoader object.
   */
  getEmbeddedFilesLoader(isDark?: boolean): EmbeddedFilesLoader {
    return new EmbeddedFilesLoader(this.plugin, isDark);
  };

  /**
   * Utility function to generate ExportSettings object.
   * @param {boolean} withBackground - Whether to include the background in the export.
   * @param {boolean} withTheme - Whether to include the theme in the export.
   * @param {boolean} [isMask=false] - Whether the export is a mask.
   * @returns {ExportSettings} The ExportSettings object.
   */
  getExportSettings(
    withBackground: boolean,
    withTheme: boolean,
    isMask: boolean = false,
  ): ExportSettings {
    return { withBackground, withTheme, isMask };
  };


  /**
   * Gets the elements within a specific area.
   * @param elements - The elements to check.
   * @param param1 - The area to check against.
   * @returns The elements within the area.
   */
  getElementsInArea(
    elements: NonDeletedExcalidrawElement[],
    element: NonDeletedExcalidrawElement,    
  ):ExcalidrawElement[] {
    const {x, y, width, height, id} = element;
    return elements
      .filter(el => {
        if((el.type==="frame" && el.frameRole==="marker")) return false;
        if(el.id === id) return true;
        const {topX, topY, width:w, height:h} = this.getBoundingBox([el]);
        const elLeft = topX;
        const elTop = topY;
        const elRight = topX + w;
        const elBottom = topY + h;
        // overlap exists if rectangles intersect
        return !(elLeft >= x + width || elRight <= x || elTop >= y + height || elBottom <= y);
      });
  };

  /**
   * Gets the bounding box of the specified elements.
   * The bounding box is the box encapsulating all of the elements completely.
   * @param {ExcalidrawElement[]} elements - Array of elements to get the bounding box for.
   * @returns {{topX: number; topY: number; width: number; height: number}} The bounding box of the elements.
   */
  getBoundingBox(elements: ExcalidrawElement[]): {
    topX: number; 
    topY: number;
    width: number;
    height: number;
  } {
    const bb = getCommonBoundingBox(elements);
    return {
      topX: bb.minX,
      topY: bb.minY,
      width: bb.maxX - bb.minX,
      height: bb.maxY - bb.minY,
    };
  };

  /**
   * Gets elements grouped by the highest level groups.
   * @param {ExcalidrawElement[]} elements - Array of elements to group.
   * @returns {ExcalidrawElement[][]} Array of arrays of grouped elements.
   */
  getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][] {
    return getMaximumGroups(elements, arrayToMap(elements));
  };

  /**
   * Gets the largest element from a group.
   * Useful when a text element is grouped with a box, and you want to connect an arrow to the box.
   * @param {ExcalidrawElement[]} elements - Array of elements in the group.
   * @returns {ExcalidrawElement} The largest element in the group.
   */
  getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement {
    if (!elements || elements.length === 0) {
      return null;
    }
    let largestElement = elements[0];
    const getSize = (el: ExcalidrawElement): Number => {
      return el.height * el.width;
    };
    let largetstSize = getSize(elements[0]);
    for (let i = 1; i < elements.length; i++) {
      const size = getSize(elements[i]);
      if (size > largetstSize) {
        largetstSize = size;
        largestElement = elements[i];
      }
    }
    return largestElement;
  };

  /**
   * Intersects an element with a line.
   * @param {ExcalidrawBindableElement} element - The element to intersect.
   * @param {readonly [number, number]} a - The start point of the line.
   * @param {readonly [number, number]} b - The end point of the line.
   * @param {number} [gap] - The gap between the element and the line.
   * @returns {Point[]} Array of intersection points (2 or 0).
   */
  intersectElementWithLine(
    element: ExcalidrawBindableElement,
    a: readonly [number, number],
    b: readonly [number, number],
    gap?: number,
  ): Point[] {
    return intersectElementWithLine(
      element,
      a as GlobalPoint,
      b as GlobalPoint,
      gap
    );
  };

  /**
   * Gets the groupId for the group that contains all the elements, or null if such a group does not exist.
   * @param {ExcalidrawElement[]} elements - Array of elements to check.
   * @returns {string | null} The groupId or null if not found.
   */
  getCommonGroupForElements(elements: ExcalidrawElement[]): string {
    const groupId = elements.map(el=>el.groupIds).reduce((prev,cur)=>cur.filter(v=>prev.includes(v)));
    return groupId.length > 0 ? groupId[0] : null;
  }

  /**
   * Gets all the elements from elements[] that share one or more groupIds with the specified element.
   * @param {ExcalidrawElement} element - The element to check.
   * @param {ExcalidrawElement[]} elements - Array of elements to search.
   * @param {boolean} [includeFrameElements=false] - Whether to include frame elements in the search.
   * @returns {ExcalidrawElement[]} Array of elements in the same group as the specified element.
   */
  getElementsInTheSameGroupWithElement(
    element: ExcalidrawElement,
    elements: ExcalidrawElement[],
    includeFrameElements: boolean = false,
  ): ExcalidrawElement[] {
    if(!element || !elements) return [];
    const container = (element.type === "text" && element.containerId)
      ? elements.filter(el=>el.id === element.containerId)
      : [];
    if(element.groupIds.length === 0) {
      if(includeFrameElements && element.type === "frame") {
        return this.getElementsInFrame(element,elements,true);
      }
      if(container.length === 1) return [element,container[0]];
      return [element];
    }

    const conditionFN = container.length === 1
      ? (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id)) || el === container[0]
      : (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id));

    if(!includeFrameElements) {
      return elements.filter(el=>conditionFN(el));
    } else {
      //I use the set and the filter at the end to preserve scene layer seqeuence
      //adding frames could potentially mess up the sequence otherwise
      const elementIDs = new Set<string>();
      elements
        .filter(el=>conditionFN(el))
        .forEach(el=>{
          if(el.type === "frame") {
            this.getElementsInFrame(el,elements,true).forEach(el=>elementIDs.add(el.id))
          } else {
            elementIDs.add(el.id);
          }
        });
      return elements.filter(el=>elementIDs.has(el.id));
    }
  }

  /**
   * Gets all the elements from elements[] that are contained in the specified frame.
   * @param {ExcalidrawElement} frameElement - The frame element.
   * @param {ExcalidrawElement[]} elements - Array of elements to search.
   * @param {boolean} [shouldIncludeFrame=false] - Whether to include the frame element in the result.
   * @returns {ExcalidrawElement[]} Array of elements contained in the frame.
   */
  getElementsInFrame(
    frameElement: ExcalidrawElement,
    elements: ExcalidrawElement[],
    shouldIncludeFrame: boolean = false,
  ): ExcalidrawElement[] {
    if(!frameElement || !elements || frameElement.type !== "frame") return [];
    return elements.filter(el=>(el.frameId === frameElement.id) || (shouldIncludeFrame && el.id === frameElement.id));
  } 

  /**
   * Sets the active script for the ScriptEngine.
   * @param {string} scriptName - The name of the active script.
   */
  activeScript: string = null; 

  /**
   * Gets the script settings for the active script.
   * Saves settings in plugin settings, under the activeScript key.
   * @returns {Object} The script settings.
   */
  getScriptSettings(): {} {
    if (!this.activeScript) {
      return null;
    }
    return this.plugin.settings.scriptEngineSettings[this.activeScript] ?? {};
  };

  /**
   * Sets the script settings for the active script.
   * @param {Object} settings - The script settings to set.
   * @returns {Promise<void>} Promise resolving when the settings are saved.
   */
  async setScriptSettings(settings: any): Promise<void> {
    if (!this.activeScript) {
      return null;
    }
    this.plugin.settings.scriptEngineSettings[this.activeScript] = settings;
    await this.plugin.saveSettings();
  };

  /**
   * Opens a file in a new workspace leaf or reuses an existing adjacent leaf depending on Excalidraw Plugin Settings.
   * @param {TFile} file - The file to open.
   * @param {OpenViewState} [openState] - The open state for the file.
   * @returns {WorkspaceLeaf} The new or adjacent workspace leaf.
   */
  openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf {
    if (!file || !(file instanceof TFile)) {
      return null;
    }
    if (!this.targetView) {
      return null;
    }

    const {leaf, promise} = openLeaf({
      plugin: this.plugin,
      fnGetLeaf: () => getNewOrAdjacentLeaf(this.plugin, this.targetView.leaf),
      file,
      openState: openState ?? {active: true}
    });
    return leaf;
  };

  /**
   * Measures the size of the specified text based on current style settings.
   * @param {string} text - The text to measure.
   * @returns {{width: number; height: number}} The width and height of the text.
   */
  measureText(text: string): { width: number; height: number } {
    const size = _measureText(
      text,
      this.style.fontSize,
      this.style.fontFamily,
      getLineHeight(this.style.fontFamily),
    );
    return { width: size.w ?? 0, height: size.h ?? 0 };
  };

  /**
   * Returns the size of the image element at 100% (i.e. the original size), or undefined if the data URL is not available.
   * @param {ExcalidrawImageElement} imageElement - The image element from the active scene on targetView.
   * @param {boolean} [shouldWaitForImage=false] - Whether to wait for the image to load before returning the size.
   * @returns {Promise<{width: number; height: number}>} Promise resolving to the original size of the image.
   */
  async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}> {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "getOriginalImageSize()");
      return null;
    }
    if(!imageElement || imageElement.type !== "image") {
      errorMessage("Please provide a single image element as input", "getOriginalImageSize()");
      return null;
    }
    const ef = this.targetView.excalidrawData.getFile(imageElement.fileId);
    if(!ef) {
      errorMessage("Please provide a single image element as input", "getOriginalImageSize()");
      return null;
    }
    const isDark = this.getExcalidrawAPI().getAppState().theme === "dark";
    let dataURL = ef.getImage(isDark);
    if(!dataURL && !shouldWaitForImage) return;
    if(!dataURL) {
      let watchdog = 0;
      while(!dataURL && watchdog < 50) {
        await sleep(100);
        dataURL = ef.getImage(isDark);
        watchdog++;
      }
      if(!dataURL) return;
    }
    return await getImageSize(dataURL);
  }

  /**
   * Resets the image to its original aspect ratio.
   * If the image is resized then the function returns true.
   * If the image element is not in EA (only in the view), then if image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]).
   * Note you need to run await ea.addElementsToView(false); to add the modified image to the view.
   * @param {ExcalidrawImageElement} imgEl - The EA image element to be resized.
   * @returns {Promise<boolean>} Promise resolving to true if the image was changed, false otherwise.
   */
  async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise<boolean> {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "resetImageAspectRatio()");
      return null;
    }

    let originalArea, originalAspectRatio;
    if(imgEl.crop) {
      originalArea = imgEl.width * imgEl.height;
      originalAspectRatio = imgEl.crop.width / imgEl.crop.height;
    } else {
      const size = await this.getOriginalImageSize(imgEl, true);
      if (!size) { return false; }
      originalArea = imgEl.width * imgEl.height;
      originalAspectRatio = size.width / size.height;
    }
    let newWidth = Math.sqrt(originalArea * originalAspectRatio);
    let newHeight = Math.sqrt(originalArea / originalAspectRatio);
    const centerX = imgEl.x + imgEl.width / 2;
    const centerY = imgEl.y + imgEl.height / 2;

    if (newWidth !== imgEl.width || newHeight !== imgEl.height) {
      if(!this.getElement(imgEl.id)) {
        this.copyViewElementsToEAforEditing([imgEl]);
      }
      const eaEl = this.getElement(imgEl.id);
      eaEl.width = newWidth;
      eaEl.height = newHeight;
      eaEl.x = centerX - newWidth / 2;
      eaEl.y = centerY - newHeight / 2;
      return true;
    }
    return false;
  }

  /**
   * Verifies if the plugin version is greater than or equal to the required version.
   * Excample usage in a script: if (!ea.verifyMinimumPluginVersion("1.5.20")) { console.error("Please update the Excalidraw Plugin to the latest version."); return; }
   * @param {string} requiredVersion - The required plugin version.
   * @returns {boolean} True if the plugin version is greater than or equal to the required version, false otherwise.
   */
  verifyMinimumPluginVersion(requiredVersion: string): boolean {
    return verifyMinimumPluginVersion(requiredVersion);
  };

  /**
   * Checks if the provided view is an instance of ExcalidrawView.
   * @param {any} view - The view to check.
   * @returns {boolean} True if the view is an instance of ExcalidrawView, false otherwise.
   */
  isExcalidrawView(view: any): boolean {
    return view instanceof ExcalidrawView;
  }

  /**
   * Sets the selection in the view.
   * @param {ExcalidrawElement[] | string[]} elements - Array of elements or element IDs to select.
   */
  selectElementsInView(elements: ExcalidrawElement[] | string[]): void {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "selectElementsInView()");
      return;
    }
    if (!elements || elements.length === 0) {
      return;
    }
    const API: ExcalidrawImperativeAPI = this.getExcalidrawAPI();
    if(typeof elements[0] === "string") {
      const els = this.getViewElements().filter(el=>(elements as string[]).includes(el.id));
      API.selectElements(els);
    } else {
      API.selectElements(elements as ExcalidrawElement[]);
    }
  };

  /**
   * Generates a random 8-character long element ID.
   * @returns {string} The generated element ID.
   */
  generateElementId(): string {
    return nanoid();
  };

  /**
   * Clones the specified element with a new ID.
   * @param {ExcalidrawElement} element - The element to clone.
   * @returns {ExcalidrawElement} The cloned element with a new ID.
   */
  cloneElement(element: ExcalidrawElement): ExcalidrawElement {
    const newEl = JSON.parse(JSON.stringify(element));
    newEl.id = nanoid();
    return newEl;
  };

  /**
   * Moves the specified element to a specific position in the z-index.
   * @param {number} elementId - The ID of the element to move.
   * @param {number} newZIndex - The new z-index position for the element.
   */
  moveViewElementToZIndex(elementId: number, newZIndex: number): void {
    if (!this.targetView || !this.targetView?._loaded) {
      errorMessage("targetView not set", "moveViewElementToZIndex()");
      return;
    }
    const API = this.getExcalidrawAPI();
    const elements = this.getViewElements();
    const elementToMove = elements.filter((el: any) => el.id === elementId);
    if (elementToMove.length === 0) {
      errorMessage(
        `Element (id: ${elementId}) not found`,
        "moveViewElementToZIndex",
      );
      return;
    }
    if (newZIndex >= elements.length) {
      API.bringToFront(elementToMove);
      return;
    }
    if (newZIndex < 0) {
      API.sendToBack(elementToMove);
      return;
    }

    const oldZIndex = elements.indexOf(elementToMove[0]);
    elements.splice(newZIndex, 0, elements.splice(oldZIndex, 1)[0]);
    this.targetView.updateScene({
      elements,
      captureUpdate: CaptureUpdateAction.IMMEDIATELY,
    });
  };

  /**
   * Converts a hex color string to an RGB array.
   * @deprecated Use getCM / ColorMaster instead.
   * @param {string} color - The hex color string.
   * @returns {number[]} The RGB array.
   */
  hexStringToRgb(color: string): number[] {
    const res = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
    return [parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)];
  };

  /**
   * Converts an RGB array to a hex color string.
   * @deprecated Use getCM / ColorMaster instead.
   * @param {number[]} color - The RGB array.
   * @returns {string} The hex color string.
   */
  rgbToHexString(color: number[]): string {
    const cm = CM({r:color[0], g:color[1], b:color[2]});
    return cm.stringHEX({alpha: false});
  };

  /**
   * Converts an HSL array to an RGB array.
   * @deprecated Use getCM / ColorMaster instead.
   * @param {number[]} color - The HSL array.
   * @returns {number[]} The RGB array.
   */
  hslToRgb(color: number[]): number[] {
    const cm = CM({h:color[0], s:color[1], l:color[2]});
    return [cm.red, cm.green, cm.blue];
  };

  /**
   * Converts an RGB array to an HSL array.
   * @deprecated Use getCM / ColorMaster instead.
   * @param {number[]} color - The RGB array.
   * @returns {number[]} The HSL array.
   */
  rgbToHsl(color: number[]): number[] {
    const cm = CM({r:color[0], g:color[1], b:color[2]});
    return [cm.hue, cm.saturation, cm.lightness];
  };

  /**
   * Converts a color name to a hex color string.
   * @param {string} color - The color name.
   * @returns {string} The hex color string.
   */
  colorNameToHex(color: string): string {
    if (COLOR_NAMES.has(color.toLowerCase().trim())) {
      return COLOR_NAMES.get(color.toLowerCase().trim());
    }
    return color.trim();
  };

  /**
   * Creates a ColorMaster object for manipulating colors.
   * @param {TInput} color - The color input.
   * @returns {ColorMaster} The ColorMaster object.
   */
  getCM(color:TInput): ColorMaster {
    if(!color) {
      log("Creates a CM object. Visit https://github.com/lbragile/ColorMaster for documentation.");
      return;
    }
    if(typeof color === "string") {
      color = this.colorNameToHex(color);
    }
    
    const cm = CM(color);
    //ColorMaster converts #FFFFFF00 to #FFFFFF, which is not what we want
    //same is true for rgba and hsla transparent colors
    if(isColorStringTransparent(color as string)) {
      return cm.alphaTo(0);
    }
    return cm;
  }

  /**
   * Gets the PolyBool class from https://github.com/velipso/polybooljs.
   * @returns {PolyBool} The PolyBool class.
   */
  getPolyBool() {
    const defaultEpsilon = 0.0000000001;
    PolyBool.epsilon(defaultEpsilon);
    return PolyBool;
  }

  /**
   * Imports an SVG string into ExcalidrawAutomate elements.
   * @param {string} svgString - The SVG string to import.
   * @returns {boolean} True if the import was successful, false otherwise.
   */
  importSVG(svgString:string):boolean {
    const res:ConversionResult =  svgToExcalidraw(svgString);
    if(res.hasErrors) {
      new Notice (`There were errors while parsing the given SVG:\n${res.errors}`);
      return false;
    }
    this.copyViewElementsToEAforEditing(res.content);
    return true;
  }

  /**
   * Destroys the ExcalidrawAutomate instance, clearing all references and data.
   */
  destroy(): void {
    this.targetView = null;
    this.plugin = null;
    this.elementsDict = {};
    this.imagesDict = {};
    this.mostRecentMarkdownSVG = null;
    this.activeScript = null;
    //@ts-ignore
    this.style = {};
    //@ts-ignore
    this.canvas = {};
    this.colorPalette = {};
  }  
};
