// Partial pre-rendering allows a component with dynamic properties to
// have its static components rendered statically in a two pass render.
// The pre-rendering pass takes in `placeholder` objects (eg, request
// context or view data) and captures which components access those
// states, serializing the inputs alongside. This process tracks what
// file and export name the component came from so that rendering may
// resume from another process.

/** Placeholders can be passed around in component props,
 *  but throw a `PlaceholderError` when trying to read it. */
export function placeholder<T extends object>(id: string): T {
  return new Proxy({ [kPlaceholder]: id }, throwOnInteractHandlers) as T;
}

class PlaceholderError extends Error {
  /* @ts-expect-error */
  declare stack: NodeJS.CallSite[];

  constructor(public id: string) {
    super("This action must be delayed to runtime.");

    const _prepareStackTrace = Error.prepareStackTrace;
    Error.prepareStackTrace = (_, stack) => stack;
    void (/* volatile */ this.stack);
    Error.prepareStackTrace = _prepareStackTrace;
  }
}

const kPlaceholder = Symbol("Placeholder");
function throwSelf(target: { [kPlaceholder]: string }): never {
  throw new PlaceholderError(target[kPlaceholder]);
}
const throwOnInteractHandlers: ProxyHandler<{ [kPlaceholder]: string }> = {
  get: (t, k) => (k === kPlaceholder ? t[kPlaceholder] : throwSelf(t)),
  apply: throwSelf,
  construct: throwSelf,
  defineProperty: throwSelf,
  deleteProperty: throwSelf,
  getOwnPropertyDescriptor: throwSelf,
  getPrototypeOf: throwSelf,
  has: throwSelf,
  isExtensible: throwSelf,
  ownKeys: throwSelf,
  preventExtensions: throwSelf,
  set: throwSelf,
  setPrototypeOf: throwSelf,
};
function isToken(token: unknown): string | null {
  if (!!token && typeof token === "object") {
    const value = (token as { [kPlaceholder]: unknown })[kPlaceholder];
    if (typeof value === "string") return value;
  }
  return null;
}

interface State extends render.State {
  prerenderStack: Frame[];
}
interface Frame {}

export function Component({ meow }: { meow: number }) {
  return (
    <div>
      <h1>app shell</h1>
      <Thing bwaa={meow} />
    </div>
  );
}

export function Thing({ bwaa }: { bwaa: number }) {
  return <div>Thing is {bwaa * 2}</div>;
}

export async function main() {
  prepare(<Component meow={placeholder<number>("meow")} />);
}

interface Slot {
  file: string;
  key: string;
  props: unknown;
}

function prepare(node: render.Node) {
  const token = "\0" + crypto.randomUUID().slice(0, 8);
  const slots: Slot[] = [];
  const state = render.init(true, {});
  state.resolveNode = (s, node) => {
    try {
      return render.resolveNode(s, node);
    } catch (err) {
      if (err instanceof PlaceholderError) {
        // If not a component element, forward the error higher.
        if (
          !Array.isArray(node) ||
          node[0] != render.kElement ||
          typeof node[1] !== "function"
        )
          throw err;
        const fn = node[1];
        // Locate the file in the stack trace. Since it was
        // called directly by `resolveNode`, it should be here.
        const frame = UNWRAP(err.stack.find((f) => f.getFunction() === fn));
        // Use reflection to get the module's export name.
        const fileName = UNWRAP(frame.getFileName());
        const module = UNWRAP(require.cache[fileName]);
        const key = Object.entries(module.exports)
          .find(([, v]) => v === fn)?.[0]
          ?.toString();
        if (!key) {
          throw new Error(
            `To enable partial pre-rendering of <${fn.name} />, ` +
              `export it in ${incr.toRel(fileName)} so resuming will be ` +
              `able to import and reference it directly.`,
          );
        }
        slots.push({ file: incr.toRel(fileName), key, props: node[2] });
        // Return a 'render token' in place. The output stream is split on these.
        return token + (slots.length - 1).toString().padStart(4, "0");
      }
      throw err;
    }
  };

  const statics = [];
  const resumes = [];
  {
    const treeWithPpr = state.resolveNode(state, node);
    const baseShell = render.stringifyNode(treeWithPpr);
    const regexp = new RegExp(`${token}(\\d{4})`, "g");
    let last = 0;
    for (const match of baseShell.matchAll(regexp)) {
      const {
        index,
        [0]: { length },
        [1]: slotIndexString,
      } = match;
      const slotIndex = parseInt(UNWRAP(slotIndexString), 10);
      const slot = UNWRAP(slots[slotIndex], `Slot ${slotIndex}`);
      resumes.push(slot);
      statics.push(baseShell.slice(last, index));
      last = index + length;
    }
    const end = baseShell.slice(last);
    if (end) statics.push(end);
  }

  console.log({ parts: statics, slots });
}

import * as render from "#engine/render";
import * as incr from "../incremental.ts";
