import type {
  CompleteEvent,
  SlashCompletionOption,
  SlashCompletions,
} from "../../plug-api/types.ts";
import {
  editor,
  markdown,
  space,
  system,
  YAML,
} from "@silverbulletmd/silverbullet/syscalls";
import type { AttributeCompletion } from "../index/attributes.ts";
import { queryObjects } from "../index/plug_api.ts";
import type { TemplateObject } from "./types.ts";
import { loadPageObject } from "./page.ts";
import { renderTemplate } from "./api.ts";
import {
  extractFrontmatter,
  prepareFrontmatterDispatch,
} from "@silverbulletmd/silverbullet/lib/frontmatter";
import type { SnippetConfig } from "./types.ts";
import { deepObjectMerge } from "@silverbulletmd/silverbullet/lib/json";

export async function snippetSlashComplete(
  completeEvent: CompleteEvent,
): Promise<SlashCompletions> {
  const allTemplates = await queryObjects<TemplateObject>("template", {
    // where hooks.snippet.slashCommand exists
    filter: ["attr", ["attr", ["attr", "hooks"], "snippet"], "slashCommand"],
  }, 5);
  const options: SlashCompletionOption[] = [];
  for (const template of allTemplates) {
    const snippetTemplate = template.hooks!.snippet!;

    if (
      snippetTemplate.onlyContexts && !snippetTemplate.onlyContexts.some(
        (context) =>
          completeEvent.parentNodes.some((node) => node.startsWith(context)),
      )
    ) {
      continue;
    }
    if (
      snippetTemplate.exceptContexts && snippetTemplate.exceptContexts.some(
        (context) =>
          completeEvent.parentNodes.some((node) => node.startsWith(context)),
      )
    ) {
      continue;
    }

    options.push({
      label: snippetTemplate.slashCommand,
      detail: template.description,
      order: snippetTemplate.order || 0,
      templatePage: template.ref,
      pageName: completeEvent.pageName,
      invoke: "template.insertSnippetTemplate",
    });
  }
  return { options };
}

export async function insertSnippetTemplate(
  slashCompletion: SlashCompletionOption,
) {
  const pageObject = await loadPageObject(
    slashCompletion.pageName || (await editor.getCurrentPage()),
  );

  const config = await system.getSpaceConfig();

  const templateText = await space.readPage(slashCompletion.templatePage);
  const { renderedFrontmatter, text: replacementText, frontmatter } =
    await renderTemplate(
      templateText,
      pageObject,
      { page: pageObject, config },
    );
  const snippetTemplate: SnippetConfig = frontmatter.hooks.snippet;
  let cursorPos = await editor.getCursor();

  if (renderedFrontmatter) {
    let parsedFrontmatter: Record<string, any> = {};
    try {
      parsedFrontmatter = await YAML.parse(renderedFrontmatter);
    } catch (e: any) {
      console.error(
        `Invalid rendered for ${slashCompletion.templatePage}:`,
        e.message,
        "for frontmatter",
        renderedFrontmatter,
      );
      await editor.flashNotification(
        `Invalid frontmatter for ${slashCompletion.templatePage}, won't insert snippet`,
        "error",
      );
      return;
    }
    const pageText = await editor.getText();
    const tree = await markdown.parseMarkdown(pageText);
    const currentFrontmatter = await extractFrontmatter(
      tree,
      parsedFrontmatter,
    );
    if (!currentFrontmatter.tags?.length) {
      delete currentFrontmatter.tags;
    }
    const newFrontmatter = deepObjectMerge(
      currentFrontmatter,
      parsedFrontmatter,
    );

    const dispatch = await prepareFrontmatterDispatch(
      tree,
      newFrontmatter,
    );
    if (cursorPos === 0) {
      dispatch.selection = { anchor: renderedFrontmatter.length + 9 };
    }
    await editor.dispatch(dispatch);
    // update cursor position
    cursorPos = await editor.getCursor();
  }

  await applySnippetTemplate(replacementText, snippetTemplate);
}

export async function applySnippetTemplate(
  templateText: string,
  config: {
    insertAt?: string;
    match?: string;
    matchRegex?: string;
  },
) {
  let cursorPos = await editor.getCursor();

  if (config.insertAt) {
    switch (config.insertAt) {
      case "page-start":
        await editor.moveCursor(0);
        break;
      case "page-end":
        await editor.moveCursor((await editor.getText()).length);
        break;
      case "line-start": {
        const pageText = await editor.getText();
        let startOfLine = cursorPos;
        while (startOfLine > 0 && pageText[startOfLine - 1] !== "\n") {
          startOfLine--;
        }
        await editor.moveCursor(startOfLine);
        break;
      }
      case "line-end": {
        const pageText = await editor.getText();
        let endOfLine = cursorPos;
        while (endOfLine < pageText.length && pageText[endOfLine] !== "\n") {
          endOfLine++;
        }
        await editor.moveCursor(endOfLine);
        break;
      }
      default:
        // Deliberate no-op
    }
  }

  cursorPos = await editor.getCursor();

  if (config.match || config.matchRegex) {
    const pageText = await editor.getText();

    // Regex matching mode
    const matchRegex = new RegExp(
      (config.match || config.matchRegex)!,
    );

    let startOfLine = cursorPos;
    while (startOfLine > 0 && pageText[startOfLine - 1] !== "\n") {
      startOfLine--;
    }
    let endOfLine = cursorPos;
    while (endOfLine < pageText.length && pageText[endOfLine] !== "\n") {
      endOfLine++;
    }
    let currentLine = pageText.slice(startOfLine, endOfLine);
    const caretParts = templateText.split("|^|");
    const emptyLine = !currentLine;
    currentLine = currentLine.replace(matchRegex, caretParts[0]);

    let newSelection = emptyLine
      ? {
        anchor: startOfLine + currentLine.length,
      }
      : undefined;

    if (caretParts.length === 2) {
      // The semantics of a caret in a replacement are:
      // 1. It's a caret, so we need to move the cursor there
      // 2. It's a placeholder, so we need to remove it
      // 3. Any text after the caret should be inserted after the caret
      const caretPos = currentLine.length;
      // Now add the text after the caret
      currentLine += caretParts[1];
      newSelection = {
        anchor: startOfLine + caretPos,
      };
    }

    await editor.dispatch({
      changes: {
        from: startOfLine,
        to: endOfLine,
        insert: currentLine,
      },
      selection: newSelection,
    });
  } else {
    const carretPos = templateText.indexOf("|^|");
    templateText = templateText.replace("|^|", "");
    await editor.insertAtCursor(templateText);
    if (carretPos !== -1) {
      await editor.moveCursor(cursorPos + carretPos);
    }
  }
}

export function attributeCompletionsToCMCompletion(
  completions: AttributeCompletion[],
) {
  return completions.map(
    (completion) => ({
      label: completion.name,
      detail: `${completion.attributeType} (${completion.source})`,
      type: "attribute",
    }),
  );
}
