/*
This script automates the creation of customizable tables in Excalidraw with template support.

Workflow:
1. Opens a configuration modal where you can select a saved template or manually specify rows, columns, and heading locations.
2. Opens a data-entry grid modal pre-populated with template headers where you can type or paste values. 
3. Includes a "Save Template" option inside the data entry modal that launches a stylized naming modal to store your custom table structures permanently.
4. Auto-sizes cells dynamically on generation, capping column widths at 300px using Excalidraw's wrapping engine.
5. Generates the table at your mouse pointer position using Excalidraw's active colors and styling settings.
*/

if (!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.12.0")) {
  new Notice("This script requires a newer version of the Excalidraw plugin. Please update.");
  return;
}

const ExcalidrawLib = window.ExcalidrawLib;

// Helper to determine if a specific cell coordinate is a header
const isHeader = (r, c, headerMode) => {
  if (headerMode === "Top" && r === 0) return true;
  if (headerMode === "Left" && c === 0) return true;
  if (headerMode === "Both" && (r === 0 || c === 0)) return true;
  return false;
};

// Saves template configuration to local script settings
async function saveTemplateToSettings(name, rows, cols, headerMode, headers) {
  const settings = ea.getScriptSettings() || {};
  if (!settings["Table Templates"]) {
    settings["Table Templates"] = { value: {} };
  }
  settings["Table Templates"].value[name] = {
    rows,
    cols,
    headerMode,
    headers
  };
  await ea.setScriptSettings(settings);
}

// Measures text height/width while temporarily overriding global styling constraints
const measureAndWrapCell = (text, isHeaderVal) => {
  const fontFam = isHeaderVal ? 4 : 2; // ID 4: Local Font, ID 2: Helvetica
  const fontSz = isHeaderVal ? 28 : 20; // Large for headers, Medium for body cells

  const origFam = ea.style.fontFamily;
  const origSz = ea.style.fontSize;

  ea.style.fontFamily = fontFam;
  ea.style.fontSize = fontSz;

  let fontName = "Helvetica";
  if (ExcalidrawLib && typeof ExcalidrawLib.getFontFamilyString === "function") {
    fontName = ExcalidrawLib.getFontFamilyString({ fontFamily: fontFam });
  } else {
    fontName = fontFam === 4 ? "Local Font" : "Helvetica";
  }
  const fontString = `${fontSz}px ${fontName}`;

  let wrappedText = text;
  if (ExcalidrawLib && typeof ExcalidrawLib.wrapText === "function") {
    wrappedText = ExcalidrawLib.wrapText(text, fontString, 300);
  }

  const metrics = ea.measureText(wrappedText);

  // Restore original styling settings to global ea object
  ea.style.fontFamily = origFam;
  ea.style.fontSize = origSz;

  return {
    text: wrappedText,
    width: metrics.width,
    height: metrics.height
  };
};

// Generates the table and coordinates on the canvas
async function drawTableOnCanvas(rows, cols, headerMode, gridData) {
  const colWidths = new Array(cols).fill(100);
  const rowHeights = new Array(rows).fill(40);
  const cellData = [];

  // 1. Calculate cell boundaries
  for (let r = 0; r < rows; r++) {
    cellData[r] = [];
    for (let c = 0; c < cols; c++) {
      const text = gridData[r][c];
      const headerVal = isHeader(r, c, headerMode);
      const measured = measureAndWrapCell(text, headerVal);
      cellData[r][c] = measured;

      colWidths[c] = Math.max(colWidths[c], measured.width + 30); // 15px left/right padding
      rowHeights[r] = Math.max(rowHeights[r], measured.height + 30); // 15px top/bottom padding
    }
  }

  // 2. Map coordinates relative to start position
  const colOffsets = [0];
  for (let c = 0; c < cols; c++) {
    colOffsets.push(colOffsets[c] + colWidths[c]);
  }
  const rowOffsets = [0];
  for (let r = 0; r < rows; r++) {
    rowOffsets.push(rowOffsets[r] + rowHeights[r]);
  }

  const W_total = colOffsets[cols];
  const H_total = rowOffsets[rows];

  // Resolve mouse coordinate location or fall back to view center
  let ptr = ea.getViewLastPointerPosition();
  if (!ptr || (ptr.x === 0 && ptr.y === 0)) {
    ptr = ea.getViewCenterPosition();
  }
  const startX = ptr.x;
  const startY = ptr.y;

  // Cache old global brush styles
  const origStrokeColor = ea.style.strokeColor;
  const origBackgroundColor = ea.style.backgroundColor;
  const origFillStyle = ea.style.fillStyle;
  const origStrokeWidth = ea.style.strokeWidth;
  const origStrokeStyle = ea.style.strokeStyle;
  const origRoughness = ea.style.roughness;
  const origRoundness = ea.style.roundness;
  const origFontFamily = ea.style.fontFamily;
  const origFontSize = ea.style.fontSize;
  const origTextAlign = ea.style.textAlign;
  const origVerticalAlign = ea.style.verticalAlign;

  const st = ea.getExcalidrawAPI().getAppState();
  ea.style.strokeColor = st.currentItemStrokeColor || "#000000";
  ea.style.backgroundColor = st.currentItemBackgroundColor || "transparent";
  ea.style.fillStyle = st.currentItemFillStyle || "solid";
  ea.style.strokeWidth = st.currentItemStrokeWidth || 1;
  ea.style.strokeStyle = st.currentItemStrokeStyle || "solid";
  ea.style.roughness = st.currentItemRoughness || 0;
  ea.style.roundness = st.currentItemRoundness || null;

  // 3. Draw outer bounding box
  const outerBoxId = ea.addRect(startX, startY, W_total, H_total);
  const addedIds = [outerBoxId];

  // 4. Draw separator lines
  ea.style.backgroundColor = "transparent";
  ea.style.fillStyle = "solid";

  if (headerMode === "Top" || headerMode === "Both") {
    const lineY = startY + rowHeights[0];
    const hLineId = ea.addLine([[startX, lineY], [startX + W_total, lineY]]);
    addedIds.push(hLineId);
  }
  if (headerMode === "Left" || headerMode === "Both") {
    const lineX = startX + colWidths[0];
    const vLineId = ea.addLine([[lineX, startY], [lineX, startY + H_total]]);
    addedIds.push(vLineId);
  }

  // 5. Draw text elements
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const cell = cellData[r][c];
      const valText = cell.text.trim() === "" ? " " : cell.text;

      const headerVal = isHeader(r, c, headerMode);
      ea.style.fontFamily = headerVal ? 4 : 2;
      ea.style.fontSize = headerVal ? 28 : 20;
      
      // Set headers explicitly to black; body cells use Excalidraw's active stroke color
      ea.style.strokeColor = headerVal ? "#000000" : (st.currentItemStrokeColor || "#000000");
      ea.style.textAlign = "left";
      ea.style.verticalAlign = "middle";

      const textX = startX + colOffsets[c] + 15;
      const textY = startY + rowOffsets[r] + (rowHeights[r] - cell.height) / 2;

      const textId = ea.addText(textX, textY, valText, {
        autoResize: false,
        width: colWidths[c] - 30,
        height: cell.height
      });
      addedIds.push(textId);
    }
  }

  // 6. Group all elements and restore brush defaults
  ea.addToGroup(addedIds);

  ea.style.strokeColor = origStrokeColor;
  ea.style.backgroundColor = origBackgroundColor;
  ea.style.fillStyle = origFillStyle;
  ea.style.strokeWidth = origStrokeWidth;
  ea.style.strokeStyle = origStrokeStyle;
  ea.style.roughness = origRoughness;
  ea.style.roundness = origRoundness;
  ea.style.fontFamily = origFontFamily;
  ea.style.fontSize = origFontSize;
  ea.style.textAlign = origTextAlign;
  ea.style.verticalAlign = origVerticalAlign;

  await ea.addElementsToView(false, true, true);
}

// Modal 3: Prompts for the template name
function openTemplateNamingModal(rows, cols, headerMode, headersMap) {
  const nameModal = new ea.FloatingModal(ea.plugin.app);
  nameModal.titleEl.setText("Save Template");
  nameModal.modalEl.style.width = "350px";

  nameModal.onOpen = () => {
    const contentEl = nameModal.contentEl;
    contentEl.empty();

    let templateName = "";

    new ea.obsidian.Setting(contentEl)
      .setName("Template Name")
      .setDesc("Enter a unique name to save these headers")
      .addText(text => text
        .setPlaceholder("e.g. Weekly Schedule")
        .onChange(val => { templateName = val.trim(); })
      );

    const buttonRow = contentEl.createDiv({
      attr: { style: "display: flex; justify-content: flex-end; gap: 10px; margin-top: 15px;" }
    });

    const cancelBtn = buttonRow.createEl("button", { text: "Cancel" });
    cancelBtn.addEventListener("click", () => {
      nameModal.close();
    });

    const saveBtn = buttonRow.createEl("button", {
      text: "Save",
      cls: "mod-cta"
    });
    saveBtn.addEventListener("click", async () => {
      if (!templateName) {
        new Notice("Template name is required.");
        return;
      }
      nameModal.close();
      await saveTemplateToSettings(templateName, rows, cols, headerMode, headersMap);
      new Notice(`Template "${templateName}" saved successfully.`);
    });
  };

  nameModal.open();
}

// Modal 2: Renders the data grid matching rows and columns
function openDataEntryModal(rows, cols, headerMode, activeTemplate, savedTemplates) {
  const dataModal = new ea.FloatingModal(ea.plugin.app);
  dataModal.titleEl.setText("Create Table - Step 2");

  dataModal.onOpen = () => {
    const contentEl = dataModal.contentEl;
    contentEl.empty();

    contentEl.createEl("style", {
      text: `
        .create-table-grid {
          display: grid;
          gap: 8px;
          margin-bottom: 15px;
          max-height: 400px;
          overflow-y: auto;
          padding: 5px;
        }
        .create-table-cell {
          width: 100%;
          padding: 6px;
          border: 1px solid var(--background-modifier-border);
          border-radius: 4px;
          background-color: var(--background-primary);
          color: var(--text-normal);
        }
        .create-table-header-cell {
          background-color: var(--background-secondary);
          font-weight: bold;
        }
      `
    });

    const gridContainer = contentEl.createDiv({ cls: "create-table-grid" });
    gridContainer.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;

    const inputs = [];
    const tpl = activeTemplate ? savedTemplates[activeTemplate] : null;

    for (let r = 0; r < rows; r++) {
      inputs[r] = [];
      for (let c = 0; c < cols; c++) {
        const isHeaderCell = isHeader(r, c, headerMode);
        let val = "";
        
        // Populate inputs with saved template headers if available
        if (isHeaderCell && tpl && tpl.headers && tpl.headers[`${r},${c}`]) {
          val = tpl.headers[`${r},${c}`];
        }

        const input = gridContainer.createEl("input", {
          type: "text",
          cls: isHeaderCell ? "create-table-cell create-table-header-cell" : "create-table-cell",
          placeholder: isHeaderCell ? `Header [${r+1},${c+1}]` : `Cell [${r+1},${c+1}]`,
          value: val
        });
        inputs[r][c] = input;
      }
    }

    const buttonRow = contentEl.createDiv({
      attr: { style: "display: flex; justify-content: flex-end; gap: 10px; margin-top: 15px;" }
    });

    const cancelBtn = buttonRow.createEl("button", { text: "Back" });
    cancelBtn.addEventListener("click", () => {
      dataModal.close();
      run(); 
    });

    const saveTplBtn = buttonRow.createEl("button", { text: "Save Template" });
    saveTplBtn.addEventListener("click", () => {
      const headersMap = {};
      for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
          if (isHeader(r, c, headerMode)) {
            headersMap[`${r},${c}`] = inputs[r][c].value || "";
          }
        }
      }
      openTemplateNamingModal(rows, cols, headerMode, headersMap);
    });

    const createBtn = buttonRow.createEl("button", {
      text: "Create Table",
      cls: "mod-cta"
    });
    createBtn.addEventListener("click", async () => {
      dataModal.close();

      const gridData = [];
      for (let r = 0; r < rows; r++) {
        gridData[r] = [];
        for (let c = 0; c < cols; c++) {
          gridData[r][c] = inputs[r][c].value || "";
        }
      }

      await drawTableOnCanvas(rows, cols, headerMode, gridData);
    });
  };
  dataModal.open();
}

// Modal 1: Configurations setup (Entrypoint)
function run() {
  const configModal = new ea.FloatingModal(ea.plugin.app);
  configModal.titleEl.setText("Create Table - Step 1");

  configModal.onOpen = () => {
    const contentEl = configModal.contentEl;
    contentEl.empty();

    let settings = ea.getScriptSettings() || {};
    const savedTemplates = settings["Table Templates"]?.value || {};
    const templateNames = Object.keys(savedTemplates);

    let rows = 3;
    let cols = 3;
    let headerMode = "Top";
    let activeTemplate = null;

    let rowsInput, colsInput, headerModeDropdown, templateDropdown;

    // Template Selector
    if (templateNames.length > 0) {
      new ea.obsidian.Setting(contentEl)
        .setName("Load Template")
        .setDesc("Choose an existing template structure or build a custom layout")
        .addDropdown(dropdown => {
          templateDropdown = dropdown;
          dropdown.addOption("None", "None (Custom Table)");
          templateNames.forEach(name => dropdown.addOption(name, name));
          dropdown.setValue("None");
          dropdown.onChange(val => {
            if (val !== "None") {
              const tpl = savedTemplates[val];
              rowsInput.setValue(String(tpl.rows));
              colsInput.setValue(String(tpl.cols));
              headerModeDropdown.setValue(tpl.headerMode);
              
              rows = tpl.rows;
              cols = tpl.cols;
              headerMode = tpl.headerMode;
              activeTemplate = val;
            } else {
              activeTemplate = null;
            }
          });
        });
    }

    const rowsSetting = new ea.obsidian.Setting(contentEl)
      .setName("Rows")
      .setDesc("Number of rows in the table")
      .addText(text => {
        rowsInput = text;
        text.setValue(String(rows))
          .onChange(val => { 
            rows = parseInt(val) || 3; 
            if (templateDropdown) templateDropdown.setValue("None");
            activeTemplate = null;
          });
      });

    const colsSetting = new ea.obsidian.Setting(contentEl)
      .setName("Columns")
      .setDesc("Number of columns in the table")
      .addText(text => {
        colsInput = text;
        text.setValue(String(cols))
          .onChange(val => { 
            cols = parseInt(val) || 3; 
            if (templateDropdown) templateDropdown.setValue("None");
            activeTemplate = null;
          });
      });

    const headersSetting = new ea.obsidian.Setting(contentEl)
      .setName("Headers")
      .setDesc("Where should headings be located?")
      .addDropdown(dropdown => {
        headerModeDropdown = dropdown;
        dropdown.addOption("Top", "Top")
          .addOption("Left", "Left")
          .addOption("Both", "Both")
          .addOption("None", "None")
          .setValue(headerMode)
          .onChange(val => { 
            headerMode = val; 
            if (templateDropdown) templateDropdown.setValue("None");
            activeTemplate = null;
          });
      });

    const buttonRow = contentEl.createDiv({
      attr: { style: "display: flex; justify-content: flex-end; margin-top: 15px;" }
    });

    const nextBtn = buttonRow.createEl("button", {
      text: "Next",
      cls: "mod-cta"
    });
    nextBtn.addEventListener("click", () => {
      configModal.close();
      openDataEntryModal(rows, cols, headerMode, activeTemplate, savedTemplates);
    });
  };

  configModal.open();
}

run();