// Static type propagation for the synthetic node-graph editor. This mirrors the
// Go runtime/compile rules in internal/dsp/types.go (OpOutputType) so the editor
// can colour wires by data type and flag type-incompatible wirings before save.
// Keep the two in sync — a parity test guards the pair.

import type { SynthGraph, SynthGraphNode } from './types';

export type SynthType = 'scalar' | 'array' | 'unknown';

// reductionOps collapse an array (or scalar) to a single scalar.
const REDUCTION_OPS = new Set(['index', 'length', 'sum', 'mean', 'min', 'max']);
// arrayProducerOps require an array input and yield an array.
const ARRAY_PRODUCER_OPS = new Set(['fft', 'slice']);
// scalarOnlyOps reject array inputs and yield a scalar (stateful filters + lua).
const SCALAR_ONLY_OPS = new Set(['moving_average', 'rms', 'lowpass', 'derivative', 'integrate', 'lua']);

// opOutputType reports an op's output type given its input types, plus an error
// message when the inputs are definitely incompatible with the op. 'unknown'
// inputs (a source whose real type isn't known yet) never trigger an error —
// runtime typing is authoritative.
export function opOutputType(op: string, inputs: SynthType[]): { type: SynthType; error?: string } {
  if (REDUCTION_OPS.has(op)) return { type: 'scalar' };

  if (ARRAY_PRODUCER_OPS.has(op)) {
    if (inputs.some(t => t === 'scalar')) return { type: 'unknown', error: `${op} requires an array input` };
    return { type: 'array' };
  }

  if (SCALAR_ONLY_OPS.has(op)) {
    if (inputs.some(t => t === 'array')) return { type: 'unknown', error: `${op} does not accept an array input` };
    return { type: 'scalar' };
  }

  // Elementwise stateless ops (gain, offset, add, subtract, multiply, divide,
  // clamp, threshold, expr): array if any input is an array, scalar if all
  // inputs are definitely scalar, otherwise unknown.
  if (inputs.some(t => t === 'array')) return { type: 'array' };
  if (inputs.some(t => t === 'unknown')) return { type: 'unknown' };
  return { type: 'scalar' };
}

// portAccept reports the data type an op's input ports accept:
//   'array'  — reductions & array producers require an array input
//   'scalar' — stateful filters & lua require a scalar input
//   'any'    — elementwise stateless ops accept either type
// Used to colour input ports and to reject definitely-incompatible wirings.
export function portAccept(op: string): SynthType | 'any' {
  if (REDUCTION_OPS.has(op) || ARRAY_PRODUCER_OPS.has(op)) return 'array';
  if (SCALAR_ONLY_OPS.has(op)) return 'scalar';
  return 'any';
}

// typesCompatible reports whether a wire carrying `from` may terminate on a port
// accepting `accept`. 'unknown' is always allowed (runtime typing is final), as
// is an 'any' port — only a definite scalar↔array mismatch is rejected.
export function typesCompatible(from: SynthType, accept: SynthType | 'any'): boolean {
  if (accept === 'any' || from === 'unknown') return true;
  return from === accept;
}

// inferNodeTypes walks the DAG in dependency order and assigns each node an
// output type. Source node types come from `sourceType` (resolved from upstream
// signal metadata); unresolved sources default to 'unknown'. `errors` collects
// per-node type-conflict messages for the editor's validation.
export function inferNodeTypes(
  g: SynthGraph,
  sourceType: (n: SynthGraphNode) => SynthType,
): { types: Map<string, SynthType>; errors: Map<string, string> } {
  const types = new Map<string, SynthType>();
  const errors = new Map<string, string>();
  const byId = new Map(g.nodes.map(n => [n.id, n]));

  // Kahn topological order over node inputs.
  const indeg = new Map<string, number>();
  const succ = new Map<string, string[]>();
  for (const n of g.nodes) indeg.set(n.id, 0);
  for (const n of g.nodes) {
    for (const from of n.inputs ?? []) {
      if (!byId.has(from)) continue;
      indeg.set(n.id, (indeg.get(n.id) ?? 0) + 1);
      succ.set(from, [...(succ.get(from) ?? []), n.id]);
    }
  }
  const queue = g.nodes.filter(n => (indeg.get(n.id) ?? 0) === 0).map(n => n.id);
  while (queue.length) {
    const id = queue.shift()!;
    const n = byId.get(id)!;
    if (n.kind === 'source') {
      types.set(id, sourceType(n));
    } else if (n.kind === 'output') {
      const from = (n.inputs ?? [])[0];
      types.set(id, (from && types.get(from)) || 'unknown');
    } else {
      const inTypes = (n.inputs ?? []).map(f => types.get(f) ?? 'unknown');
      const { type, error } = opOutputType(n.op ?? '', inTypes);
      types.set(id, type);
      if (error) errors.set(id, error);
    }
    for (const s of succ.get(id) ?? []) {
      indeg.set(s, (indeg.get(s) ?? 0) - 1);
      if ((indeg.get(s) ?? 0) === 0) queue.push(s);
    }
  }
  return { types, errors };
}
