/*
 * languages.js — what the render window can preview, one entry per language.
 *
 * Each entry turns a fence's contents into the <body> of the sandboxed frame:
 *
 *   toBody(value) -> { html, userOffset }
 *
 * `userOffset` is how many lines of that body come before the user's own code.
 * The frame reports runtime errors by line number and those numbers are
 * document-relative, so without this an error in a JSX component would be
 * reported at some line deep inside the inlined Preact build. The caller adds
 * the lines of document shell above the body and hands the total to the
 * bootstrap, which subtracts it before reporting.
 *
 * A `toBody` may throw: JSX that doesn't parse has no preview to show. The
 * caller catches and shows the message in place of the frame.
 *
 * The backend keeps a matching registry (PREVIEW_LANGS in synapse/tools.py)
 * that says how each language is *validated* rather than rendered. Neither
 * depends on the other at runtime; tests/test_tools.py asserts the key sets
 * stay equal.
 */
import { transform } from "./jsx-transform.js";
import { PREACT_RUNTIME } from "./runtime.js";

const countNewlines = (text) => (text.match(/\n/g) || []).length;

/** Markup languages: the fence is already a document body. */
const markup = (value) => ({ html: value, userOffset: 0 });

/**
 * Pick what to mount. An explicit default export wins, then a component named
 * App, then the last capitalized declaration - models tend to define helpers
 * first and the thing they were asked for last.
 */
function mountTarget({ defaultExport, components }) {
  if (defaultExport) return defaultExport;
  if (components.includes("App")) return "App";
  if (components.length) return components[components.length - 1];
  throw new Error(
    "No component found to render. Name one `App`, or `export default` it.",
  );
}

/**
 * One line of stubs for every binding an import would have provided.
 *
 * Imports are dropped — there is no module loader in the sandbox — so a name
 * that came from a package is simply missing, and the first use of it reports
 * "useInView is not defined" at a line far from the import that explains it.
 * Each stub throws with the module name instead, and `||` means anything the
 * runtime already provides (useState and friends) keeps its real implementation.
 */
function importStubs(imports) {
  const names = new Map();
  for (const { names: bound, module } of imports || []) {
    for (const name of bound) if (!names.has(name)) names.set(name, module);
  }
  if (!names.size) return "";
  const lines = [...names].map(([name, module]) => {
    const why = JSON.stringify(
      `${name} came from ${module ? `"${module}"` : "an import"}, which the preview ` +
      "cannot load — it has no module loader and no network. Inline what you need, " +
      "or use the built-in hooks, which are already in scope.",
    );
    return `window[${JSON.stringify(name)}] = window[${JSON.stringify(name)}] ` +
      `|| function () { throw new Error(${why}); };`;
  });
  return `<script>${lines.join("")}</script>\n`;
}

function jsxBody(value) {
  const result = transform(value);
  const target = mountTarget(result);

  const head =
    '<div id="root"></div>\n' +
    `<script>${PREACT_RUNTIME}</script>\n` +
    importStubs(result.imports) +
    "<script>\n";

  return {
    html:
      head +
      result.code +
      `\n;render(h(${target}, null), document.getElementById("root"));\n` +
      "</script>",
    userOffset: countNewlines(head),
  };
}

export const PREVIEW_LANGS = {
  html: { toBody: markup },
  svg: { toBody: markup },
  jsx: { toBody: jsxBody },
  tsx: { toBody: jsxBody },
};

export const RENDERABLE_LANGS = new Set(Object.keys(PREVIEW_LANGS));
