import { type PDFAcroField, type PDFField, PDFName, PDFString } from "pdf-lib";
import { loadPdf, loadPdfForm, savePdf } from "util/saveLoadPdf.ts";
import { callWithArgPrompt } from "util/call.ts";
import { TerminalBlock } from "../cli/TerminalLayout.ts";
import { forceArgs } from "../cli/forceArgs.ts";
import { colorize } from "../cli/style.ts";
import { cliAlert, cliLog, cliPrompt } from "../cli/prompts.ts";
import { multiSelectMenuInteractive } from "../cli/selectMenu.ts";
import type { callback, ITool } from "../types.ts";
import { toCase } from "util/caseManagement.ts";

async function renameFields(
  path: string,
  pattern: string | RegExp,
  change: string,
) {
  if (typeof pattern === "string") pattern = new RegExp(pattern);
  const form = await loadPdfForm(path);
  const fields = form.getFields();
  let changesMade = false;
  for (const field of fields) {
    const name = field.getName();
    if (pattern.test(name)) {
      console.log(name + " %cfound", "color: red");
      const segments = name.split(".");
      const matchingSegments = segments.filter((s) => pattern.test(s));
      let cField: PDFAcroField | undefined = field.acroField;
      while (cField) {
        if (
          cField.getPartialName() &&
          matchingSegments.includes(cField.getPartialName()!)
        ) {
          const mName = cField.getPartialName()?.replace(pattern, change);
          if (mName) {
            changesMade = true;
            cField.dict.set(PDFName.of("T"), PDFString.of(mName));
          }
        }
        cField = cField.getParent();
      }
    }
  }
  if (changesMade) {
    savePdf(form.doc, path);
  }
}

function applyRename(
  field: PDFField,
  name: string,
  pattern: RegExp,
  change: string,
) {
  const segments = name.split(".");
  const matchingSegments = segments.filter((s) => pattern.test(s));
  let cField: PDFAcroField | undefined = field.acroField;
  while (cField) {
    if (
      cField.getPartialName() &&
      matchingSegments.includes(cField.getPartialName()!)
    ) {
      const mName = cField.getPartialName()?.replace(pattern, change);
      if (mName) {
        cField.dict.set(PDFName.of("T"), PDFString.of(mName));
        // console.log(cField.getPartialName())
      }
    }
    cField = cField.getParent();
    // console.log(cField?.getPartialName())
  }
}

/***
 * Evaluates the change string with the match array
 *
 * @description The change string can include the following variables:
 *
 * - $<int> - capture groups, indexed from 1
 * - $<int>i - capture groups, indexed from 1, transforming an integer to an index
 * - $<int>s - capture groups, indexed from 1, transforming a string to snake case
 * - $<int>c - capture groups, indexed from 1, transforming a string to camel case
 * - $<int>l - capture groups, indexed from 1, transforming a string to lower case
 * - $<int>u - capture groups, indexed from 1, transforming a string to upper case
 * - $<int>t - capture groups, indexed from 1, transforming a string to title case
 */
function evaluateChange(change: string, match: RegExpExecArray) {
  return change.replace(
    /\$(\d+)([icslut]?)/g,
    (_, i, indexed) => {
      switch (indexed) {
        case "i":
          return (parseInt(match[i])
            ? (parseInt(match[i]) - 1).toString()
            : match[i]);
        case "s":
          return toCase(match[i], "snake");
        case "c":
          return toCase(match[i], "camel");
        case "t":
          return toCase(match[i], "title");
        case "l":
          return match[i].toLowerCase();
        case "u":
          return match[i].toUpperCase();
        default:
          return match[i];
      }
    },
  );
}

class RenameFields implements ITool {
  name = "renamefields";
  description = "Renames fields in a PDF form";
  block: TerminalBlock | undefined;

  setBlock(block: TerminalBlock) {
    this.block = block;
  }

  async help(standalone = false) {
    await cliAlert(
      "Usage: rename-fields <pdfPath> <pattern> <change>\n",
      standalone ? undefined : this.block,
    );
  }
  async run(pdfPath: string = "", pattern: string = "", change: string = "") {
    if (!this.block) {
      this.block = new TerminalBlock();
    }
    this.block.setPreserveHistory(true);

    [pdfPath, pattern, change] = await forceArgs(
      [pdfPath, pattern, change],
      [
        [
          "Please provide path to PDF (comma separated for multiple):",
          (p) => !!p && p.endsWith(".pdf"),
        ],
        "Please provide search string:",
        "Please provide requested change:",
      ],
      this.block,
    );

    const paths = pdfPath.split(",");

    for (const pdfPath of paths) {
      const patternRegex = new RegExp(pattern);

      const pdf = await loadPdf(pdfPath);
      const form = pdf.getForm();
      const fields = form.getFields();

      const foundUpdates: [string, callback][] = [];
      let changesMade = false;

      for (const field of fields) {
        const name = field.getName();
        const match = patternRegex.exec(name);
        if (match) {
          const toChange = evaluateChange(change, match);
          const preview = name.replace(new RegExp(patternRegex), toChange);
          foundUpdates.push([
            `${colorize(name, "red")} -> ${colorize(preview, "green")}`,
            () => {
              applyRename(field, name, patternRegex, toChange);
              changesMade = true;
            },
          ]);
        }
      }

      if (foundUpdates.length) {
        cliLog("Found updates:", this.block);
        await multiSelectMenuInteractive(
          "Please select an option to apply",
          foundUpdates,
          { terminalBlock: this.block },
        );
      }

      if (changesMade) {
        const path = await cliPrompt(
          "Save to path (or hit enter to keep current):",
          this.block,
        );
        await savePdf(pdf, path || pdfPath);
      } else {
        cliLog("No changes made, skipping", this.block);
      }
    }
  }
}
export default new RenameFields();

if (import.meta.main) {
  // await call(renameFields)
  // while (!path || !path.endsWith('.pdf')) path = prompt("Please provide path to PDF:") || '';
  // while (!pattern) pattern = prompt("Please provide search string:") || '';
  // while (!change) change = prompt("Please provide requested change:") || '';
  await callWithArgPrompt(renameFields, [
    ["Please provide path to PDF:", (p) => !!p && p.endsWith(".pdf")],
    "Please provide search string:",
    "Please provide requested change:",
  ]);
}
