import type { GraphNode, ParameterValue, Vec2 } from "../core/types.js";
import type { FxNodeDefinition, FxNodeUiRow, FxNodeValueSchema, FxNodeVisibility } from "../composition/types.js";
import { GEOMETRY as G } from "./constants.js";

const title = (value: string) => value.replace(/-/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
const textWidth = (value: string) => value.length * 6.5;
export const nodeRowUnits = (item: FxNodeUiRow, definition?: FxNodeDefinition): number =>
  item.kind === "text" && item.variant === "header"
    ? 2
    : item.kind === "widget"
      ? item.widget === "grading-wheels"
        ? 7
        : 8
      : item.kind === "resource"
        ? 4
        : item.kind === "socket" &&
            definition?.sockets[item.socket]?.direction === "input" &&
            definition.sockets[item.socket]!.maxIncomingLinks > 1
          ? 2
          : 1;
const controlWidth = (schema: FxNodeValueSchema | undefined, ramp = false) =>
  !schema
    ? 80
    : schema.type === "vector"
      ? 180
      : schema.type === "color"
        ? 80
        : ramp
          ? 300
          : schema.type === "string"
            ? Math.max(80, ...(schema.enum ?? []).map((value) => textWidth(value) + 28))
            : 80;
export const visibleWhen = (expression: FxNodeVisibility | undefined, values: GraphNode["parameters"]): boolean =>
  !expression
    ? true
    : "all" in expression
      ? expression.all.every((item) => visibleWhen(item, values))
      : "any" in expression
        ? expression.any.some((item) => visibleWhen(item, values))
        : "equals" in expression
          ? (values[expression.parameter] as ParameterValue | undefined)?.value === expression.equals
          : expression.in.includes((values[expression.parameter] as ParameterValue | undefined)?.value as never);
export const visibleNodeItems = (
  definition: FxNodeDefinition,
  node: Pick<GraphNode, "parameters" | "sockets">,
): readonly FxNodeUiRow[] =>
  definition.ui
    .filter((item) => item.kind !== "hidden" && visibleWhen(item.visibleWhen, node.parameters))
    .filter(
      (item) => item.kind !== "socket" || node.sockets.some((socket) => socket.key === item.socket && socket.visible),
    );
export function minimumNodeSize(
  definition: FxNodeDefinition,
  node: Pick<GraphNode, "label" | "parameters" | "sockets">,
): Vec2 {
  if (definition.behavior === "reroute") return { x: 10, y: 10 };
  if (definition.behavior === "frame") return { x: G.frameMinimum, y: G.frameMinimum };
  const items = visibleNodeItems(definition, node);
  let width = Math.max(G.minWidth, textWidth(node.label) + 42);
  for (const item of items) {
    const label =
      "title" in item
        ? item.title
        : item.kind === "parameter" || item.kind === "resource"
          ? title(item.parameter)
          : item.kind === "socket"
            ? title(item.socket)
            : "";
    if (label) width = Math.max(width, (textWidth(label) + 18) / 0.4);
    if (item.kind === "parameter" || item.kind === "resource") {
      const schema = definition.parameters[item.parameter];
      width = Math.max(
        width,
        controlWidth(
          schema,
          item.kind === "parameter" &&
            definition.ui.some(
              (r) => r.kind === "widget" && r.widget === "color-ramp" && r.parameter === item.parameter,
            ),
        ) / 0.53,
      );
    } else if (item.kind === "socket") {
      const socket = definition.sockets[item.socket];
      if (socket?.value && socket.showValue) width = Math.max(width, controlWidth(socket.value) / 0.53);
    } else if (item.kind === "widget") width = Math.max(width, item.widget === "grading-wheels" ? 400 : 320);
  }
  return {
    x: Math.min(G.maxWidth, Math.ceil(width)),
    y: G.header + items.reduce((sum, item) => sum + nodeRowUnits(item, definition), 0) * G.row + G.gap,
  };
}
export function initialNodeSize(
  definition: FxNodeDefinition,
  node: Pick<GraphNode, "label" | "parameters" | "sockets">,
): Vec2 {
  if (definition.behavior === "frame") return { x: G.initialFrameWidth, y: G.initialNodeHeight };
  if (definition.behavior === "reroute") return { x: G.reroute * 2, y: G.reroute * 2 };
  const minimum = minimumNodeSize(definition, node);
  return { x: Math.max(G.initialNodeWidth, minimum.x), y: Math.max(G.initialNodeHeight, minimum.y) };
}
