Source: core/formula/dag-util.js

/*
 * Copyright 2026 The Ray Optics Simulation authors and contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Validate a DAG node label.
 *
 * Labels are lookup/output keys. They only need to be non-empty strings and
 * unique within the DAG.
 *
 * @param {unknown} label - Candidate node label.
 * @param {string} [fieldName="label"] - Field name used in error messages.
 * @returns {string} The validated label.
 * @throws {TypeError} If the label is invalid.
 */
export function validateDagNodeLabel(label, fieldName = "label") {
  if (typeof label !== "string" || label.length === 0) {
    throw new TypeError(`Invalid ${fieldName}: ${JSON.stringify(label)}`);
  }
  return label;
}

export function collectParameterNames(dag) {
  return new Set(
    dag.nodes
      .filter((node) => node.kind === "parameter")
      .map((node) => node.name),
  );
}

/**
 * Collect parameters which are dependencies of the selected output labels.
 *
 * @param {Object} dag
 * @param {string[]} labels
 * @returns {Set<string>}
 */
export function collectReferencedParameterNames(dag, labels) {
  const labelIds = collectNodeLabels(dag);
  const outputIds = labels.map(label => {
    const id = labelIds.get(label);
    if (id === undefined) {
      throw new TypeError(`Unknown label: ${JSON.stringify(label)}`);
    }
    return id;
  });
  const reachable = collectReachableNodeIds(dag, outputIds);
  return new Set(
    dag.nodes
      .filter(node =>
        reachable.has(node.id) && node.kind === "parameter"
      )
      .map(node => node.name),
  );
}

export function collectReachableNodeIds(dag, outputIds) {
  const reachable = new Set();
  const stack = [...outputIds];
  while (stack.length > 0) {
    const id = stack.pop();
    if (reachable.has(id)) continue;
    reachable.add(id);
    for (const childId of dag.nodes[id].args ?? []) {
      stack.push(childId);
    }
  }
  return reachable;
}

export function collectNodeLabels(dag) {
  const labels = new Map();
  for (const node of dag.nodes) {
    if (node.label === undefined) continue;
    if (labels.has(node.label)) {
      throw new TypeError(`Duplicate node label: ${JSON.stringify(node.label)}`);
    }
    labels.set(node.label, node.id);
  }
  return labels;
}

export function findNodeByLabel(dag, label) {
  const labels = collectNodeLabels(dag);
  const id = labels.get(label);
  if (id === undefined) throw new TypeError(`Unknown source label: ${JSON.stringify(label)}`);
  return id;
}

export function validateDagShape(dag) {
  if (!dag || !Array.isArray(dag.nodes)) throw new TypeError("dag must have a nodes array");
  if (dag.root !== undefined) assertNodeId(dag, dag.root, "root");

  const labels = new Set();
  for (const [index, node] of dag.nodes.entries()) {
    if (!node || node.id !== index) throw new TypeError(`dag.nodes[${index}] must have matching id`);
    if (node.label !== undefined) {
      validateDagNodeLabel(node.label, `node ${index} label`);
      if (labels.has(node.label)) throw new TypeError(`Duplicate node label: ${JSON.stringify(node.label)}`);
      labels.add(node.label);
    }
    for (const childId of node.args ?? []) assertNodeId(dag, childId, `node ${index} child`);
  }
}

export function assertNodeId(dag, id, name) {
  if (!Number.isInteger(id) || id < 0 || id >= dag.nodes.length) {
    throw new TypeError(`${name} must be an existing node id`);
  }
}

export function cloneNode(node) {
  return {
    ...node,
    args: node.args ? [...node.args] : node.args,
  };
}