import { Tree } from "@weborigami/async-tree";
import { formatError } from "@weborigami/language";
import ConstantTree from "../common/ConstantTree.js";
import getTreeArgument from "../common/getTreeArgument.js";

/**
 * Let a tree (e.g., of files) respond to changes.
 *
 * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
 * @typedef {import("@weborigami/async-tree").Treelike} Treelike
 * @typedef {import("../../index.ts").Invocable} Invocable
 *
 * @this {AsyncTree|null}
 * @param {Treelike} [treelike]
 * @param {Invocable} [fn]
 */
export default async function watch(treelike, fn) {
  /** @type {any} */
  const container = await getTreeArgument(
    this,
    arguments,
    treelike,
    "dev:watch"
  );

  // Watch the indicated tree.
  await /** @type {any} */ (container).watch?.();

  if (fn === undefined) {
    return container;
  }

  // The caller supplied a function to reevaluate whenever the tree changes.
  let tree = await evaluateTree(container, fn);

  // We want to return a stable reference to the tree, so we'll use a prototype
  // chain that will always point to the latest tree. We'll extend the tree's
  // prototype chain with an empty object, and use that as a handle (pointer to
  // a pointer) to the tree. This is what we'll return to the caller.
  const handle = Object.create(tree);

  // Reevaluate the function whenever the tree changes.
  container.addEventListener?.("change", async () => {
    const tree = await evaluateTree(container, fn);
    updateIndirectPointer(handle, tree);
  });

  return handle;
}

async function evaluateTree(parent, fn) {
  let tree;
  let message;
  let result;
  try {
    result = await fn();
  } catch (/** @type {any} */ error) {
    message = formatError(error);
  }
  tree = result ? Tree.from(result, { parent }) : undefined;
  if (tree) {
    return tree;
  }
  if (!message) {
    message = `warning: watch expression did not return a tree`;
  }
  console.warn(message);
  tree = new ConstantTree(message);
  tree.parent = parent;
  return tree;
}

// Update an indirect pointer to a target.
function updateIndirectPointer(indirect, target) {
  // Clean the pointer of any named properties or symbols that have been set
  // directly on it.
  try {
    for (const key of Object.getOwnPropertyNames(indirect)) {
      delete indirect[key];
    }
    for (const key of Object.getOwnPropertySymbols(indirect)) {
      delete indirect[key];
    }
  } catch {
    // Ignore errors.
  }

  Object.setPrototypeOf(indirect, target);
}
