import {
  App,
  Instruction,
  normalizePath,
  TAbstractFile,
  TFile,
} from "obsidian";
import { PLUGIN_ID } from "../constants/constants";
import ExcalidrawView from "../view/ExcalidrawView";
import ExcalidrawPlugin from "../core/main";
import { GenericInputPrompt, GenericSuggester } from "./Dialogs/Prompt";
import { getIMGFilename } from "../utils/fileUtils";
import { splitFolderAndFilename } from "../utils/fileUtils";
import { getEA } from "src/core";
import { ExcalidrawAutomate } from "../shared/ExcalidrawAutomate";
import { WeakArray } from "./WeakArray";
import {
  getExcalidrawViews,
  stripYamlFrontmatter,
} from "../utils/obsidianUtils";
import { ButtonDefinition, InputPromptOptions } from "src/types/promptTypes";
import { errorlog } from "src/utils/coreUtils";

export type ScriptIconMap = {
  [key: string]: { name: string; group: string; svgString: string };
};

export class ScriptEngine {
  private plugin: ExcalidrawPlugin;
  private app: App;
  private scriptPath: string;
  //https://stackoverflow.com/questions/60218638/how-to-force-re-render-if-map-value-changes
  public scriptIconMap: ScriptIconMap;
  eaInstances = new WeakArray<ExcalidrawAutomate>();

  constructor(plugin: ExcalidrawPlugin) {
    this.plugin = plugin;
    this.app = plugin.app;
    this.scriptIconMap = {};
    this.loadScripts();
    this.registerEventHandlers();
  }

  public removeViewEAs(view: ExcalidrawView) {
    const eas = new Set<ExcalidrawAutomate>();
    this.eaInstances.forEach((ea) => {
      if (ea.targetView === view) {
        eas.add(ea);
        if (ea.sidepanelTab) {
          ea.targetView = null;
          ea.sidepanelTab.onExcalidrawViewClosed();
        } else {
          ea.destroy();
        }
      }
    });
    this.eaInstances.removeObjects(eas);
  }

  public destroy() {
    this.eaInstances.forEach((ea) => ea.destroy());
    this.eaInstances.clear();
    this.eaInstances = null;
    this.scriptIconMap = null;
    this.plugin = null;
    this.scriptPath = null;
  }

  private handleSvgFileChange(path: string) {
    if (!path.endsWith(".svg")) {
      return;
    }
    const scriptFile = this.app.vault.getAbstractFileByPath(
      getIMGFilename(path, "md"),
    );
    if (scriptFile && scriptFile instanceof TFile) {
      this.unloadScript(this.getScriptName(scriptFile), scriptFile.path);
      this.loadScript(scriptFile);
    }
  }

  private async deleteEventHandler(file: TFile) {
    if (!(file instanceof TFile)) {
      return;
    }
    if (!file.path.startsWith(this.scriptPath)) {
      return;
    }
    this.unloadScript(this.getScriptName(file), file.path);
    this.handleSvgFileChange(file.path);
  }

  private async createEventHandler(file: TFile) {
    if (!(file instanceof TFile)) {
      return;
    }
    if (!file.path.startsWith(this.scriptPath)) {
      return;
    }
    this.loadScript(file);
    this.handleSvgFileChange(file.path);
  }

  private async renameEventHandler(file: TAbstractFile, oldPath: string) {
    if (!(file instanceof TFile)) {
      return;
    }
    const oldFileIsScript = oldPath.startsWith(this.scriptPath);
    const newFileIsScript = file.path.startsWith(this.scriptPath);
    if (oldFileIsScript) {
      this.unloadScript(this.getScriptName(oldPath), oldPath);
      this.handleSvgFileChange(oldPath);
    }
    if (newFileIsScript) {
      this.loadScript(file);
      this.handleSvgFileChange(file.path);
    }
  }

  registerEventHandlers() {
    this.plugin.registerEvent(
      this.app.vault.on("delete", (file: TFile) =>
        this.deleteEventHandler(file),
      ),
    );
    this.plugin.registerEvent(
      this.app.vault.on("create", (file: TFile) =>
        this.createEventHandler(file),
      ),
    );
    this.plugin.registerEvent(
      this.app.vault.on("rename", (file: TAbstractFile, oldPath: string) =>
        this.renameEventHandler(file, oldPath),
      ),
    );
  }

  updateScriptPath() {
    if (this.scriptPath === this.plugin.settings.scriptFolderPath) {
      return;
    }
    if (this.scriptPath) {
      this.unloadScripts();
    }
    this.loadScripts();
  }

  public getListofScripts(): TFile[] {
    this.scriptPath = this.plugin.settings.scriptFolderPath;
    if (!this.scriptPath) {
      return;
    }
    this.scriptPath = normalizePath(this.scriptPath);
    if (!this.app.vault.getAbstractFileByPath(this.scriptPath)) {
      return;
    }
    return this.app.vault
      .getFiles()
      .filter(
        (f: TFile) =>
          f.path.startsWith(`${this.scriptPath}/`) && f.extension === "md",
      );
  }

  loadScripts() {
    this.getListofScripts()?.forEach((f) => this.loadScript(f));
  }

  public getScriptName(f: TFile | string): string {
    let basename = "";
    let path = "";
    if (f instanceof TFile) {
      basename = f.basename;
      path = f.path;
    } else {
      basename = splitFolderAndFilename(f).basename;
      path = f;
    }

    const subpath = path.split(`${this.scriptPath}/`)[1];
    if (!subpath) {
      console.warn(
        `ScriptEngine.getScriptName unexpected basename: ${basename}; path: ${path}`,
      );
    }
    const lastSlash = subpath?.lastIndexOf("/");
    if (lastSlash > -1) {
      return subpath.substring(0, lastSlash + 1) + basename;
    }
    return basename;
  }

  public getScriptFileByName(scriptName: string): TFile | null {
    return (
      this.getListofScripts()?.find(
        (file) => this.getScriptName(file) === scriptName,
      ) ?? null
    );
  }

  async addScriptIconToMap(scriptPath: string, name: string) {
    const svgFilePath = getIMGFilename(scriptPath, "svg");
    const file = this.app.vault.getAbstractFileByPath(svgFilePath);
    const svgString: string =
      file && file instanceof TFile ? await this.app.vault.read(file) : null;
    this.scriptIconMap = {
      ...this.scriptIconMap,
    };
    const splitname = splitFolderAndFilename(name);
    this.scriptIconMap[scriptPath] = {
      name: splitname.filename,
      group: splitname.folderpath,
      svgString,
    };
    this.updateToolPannels();
  }

  loadScript(f: TFile) {
    if (f.extension !== "md") {
      return;
    }
    const scriptName = this.getScriptName(f);
    void this.addScriptIconToMap(f.path, scriptName);
    this.plugin.addCommand({
      id: scriptName,
      name: `(Script) ${scriptName}`,
      checkCallback: (checking: boolean) => {
        if (checking) {
          return Boolean(
            this.app.workspace.getActiveViewOfType(ExcalidrawView),
          );
        }
        const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
        if (view) {
          void (async () => {
            const script = stripYamlFrontmatter(await this.app.vault.read(f));
            if (script) {
              await this.executeScript(view, script, scriptName, f);
            }
          })();
          return true;
        }
        return false;
      },
    });
  }

  unloadScripts() {
    const scripts = this.app.vault
      .getFiles()
      .filter((f: TFile) => f.path.startsWith(this.scriptPath));
    scripts.forEach((f) => {
      this.unloadScript(this.getScriptName(f), f.path);
    });
  }

  unloadScript(basename: string, path: string) {
    if (!path.endsWith(".md")) {
      return;
    }
    delete this.scriptIconMap[path];
    this.scriptIconMap = { ...this.scriptIconMap };
    this.updateToolPannels();

    const commandId = `${PLUGIN_ID}:${basename}`;
    if (!this.app.commands.commands[commandId]) {
      return;
    }
    delete this.app.commands.commands[commandId];
  }

  async executeScript(
    view: ExcalidrawView = undefined,
    script: string,
    title: string,
    file: TFile,
  ) {
    if (!script || !title) {
      return;
    }
    //addresses the situation when after paste text element IDs are not updated to 8 characters
    //linked to onPaste save issue with the false parameter
    if (
      view &&
      view
        .getScene()
        .elements.some(
          (el) => !el.isDeleted && el.type === "text" && el.id.length > 8,
        )
    ) {
      await view.save(false, true);
    }

    script = stripYamlFrontmatter(script);
    const ea = getEA(view);
    this.eaInstances.push(ea);
    ea.activeScript = title;

    //https://stackoverflow.com/questions/45381204/get-asyncfunction-constructor-in-typescript changed tsconfig to es2017
    //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction
    const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
    let result = null;
    //try {
    result = await new AsyncFunction("ea", "utils", script)(ea, {
      inputPrompt: (
        header: string | InputPromptOptions,
        placeholder?: string,
        value?: string,
        buttons?: ButtonDefinition[],
        lines?: number,
        displayEditorButtons?: boolean,
        customComponents?: (container: HTMLElement) => void,
        blockPointerInputOutsideModal?: boolean,
        controlsOnTop?: boolean,
        draggable?: boolean,
      ) => {
        if (typeof header === "object") {
          const options = header;
          header = options.header;
          placeholder = options.placeholder;
          value = options.value;
          buttons = options.buttons;
          lines = options.lines;
          displayEditorButtons = options.displayEditorButtons;
          customComponents = options.customComponents;
          blockPointerInputOutsideModal = options.blockPointerInputOutsideModal;
          controlsOnTop = options.controlsOnTop;
          draggable = options.draggable;
        }
        return ScriptEngine.inputPrompt(
          view,
          this.plugin,
          this.app,
          header,
          placeholder,
          value,
          buttons,
          lines,
          displayEditorButtons,
          customComponents,
          blockPointerInputOutsideModal,
          controlsOnTop,
          draggable,
        );
      },
      suggester: (
        displayItems: string[],
        items: unknown[],
        hint?: string,
        instructions?: Instruction[],
      ) =>
        ScriptEngine.suggester(
          this.app,
          displayItems,
          items,
          hint,
          instructions,
        ),
      scriptFile: file,
    });
    /*} catch (e) {
      new Notice(t("SCRIPT_EXECUTION_ERROR"), 4000);
      errorlog({ script: this.plugin.ea.activeScript, error: e });
  }*/
    return result;
  }

  private updateToolPannels() {
    const excalidrawViews = getExcalidrawViews(this.app, true);
    excalidrawViews.forEach((excalidrawView) => {
      excalidrawView.toolsPanelRef?.current?.updateScriptIconMap(
        this.scriptIconMap,
      );
    });
  }

  public static async inputPrompt(
    view: ExcalidrawView,
    plugin: ExcalidrawPlugin,
    app: App,
    header: string,
    placeholder?: string,
    value?: string,
    buttons?: ButtonDefinition[],
    lines?: number,
    displayEditorButtons?: boolean,
    customComponents?: (container: HTMLElement) => void,
    blockPointerInputOutsideModal?: boolean,
    controlsOnTop?: boolean,
    draggable: boolean = false,
  ) {
    try {
      return await GenericInputPrompt.Prompt(
        view,
        plugin,
        app,
        header,
        placeholder,
        value,
        buttons,
        lines,
        displayEditorButtons,
        customComponents,
        blockPointerInputOutsideModal,
        controlsOnTop,
        draggable,
      );
    } catch {
      return undefined;
    }
  }

  public static async suggester<T>(
    app: App,
    displayItems: string[],
    items: T[],
    hint?: string,
    instructions?: Instruction[],
  ): Promise<T | undefined> {
    try {
      return await GenericSuggester.Suggest(
        app,
        displayItems,
        items,
        hint,
        instructions,
      );
    } catch (error: unknown) {
      errorlog({
        message: "unexpected error in suggester",
        where: "ScriptEngine.suggester",
        error,
      });
      return undefined;
    }
  }
}
