/**
 * Locating composition arrays and appending a view to one (docs/architecture/08 →
 * editor augmentation). Powers the "＋ Add view" CodeLens: an always-visible
 * affordance over each `layer`/`hconcat`/`vconcat`/`concat` for adding a sibling
 * view — the complement to the wrap refactors (which create a composition; this
 * grows an existing one).
 *
 * Portable core. `compositionArrays` uses jsonc-parser to find each array and its
 * path (so the CodeLens knows where to sit and what to grow); `appendView` is a
 * plain immutable push at that path. JSON-text formatting is the editor's job.
 */

import { parseTree, type Node } from 'jsonc-parser';
import { isJsonObject, type JsonObject } from './spec-config';
import { ARRAY_COMPOSITIONS, placeholderView } from './spec-transforms';

/** A spec path: object keys and array indices, from the root. */
export type SpecPath = (string | number)[];

/** A composition array found in the spec text. */
interface CompositionArray {
  /** The operator key (`layer`, `hconcat`, …). */
  key: string;
  /** Start offset of the array node, for placing the affordance. */
  offset: number;
  /** Path to the array, for `appendView`. */
  path: SpecPath;
}

/** Every composition array in the spec, with its path and start offset. */
export function compositionArrays(text: string): CompositionArray[] {
  const tree = parseTree(text);
  if (!tree) return [];
  const found: CompositionArray[] = [];

  const walk = (node: Node, path: SpecPath): void => {
    if (node.type === 'object') {
      for (const prop of node.children ?? []) {
        const key: unknown = prop.children?.[0]?.value;
        const value = prop.children?.[1];
        if (typeof key !== 'string' || !value) continue;
        if (ARRAY_COMPOSITIONS.includes(key) && value.type === 'array') {
          found.push({ key, offset: value.offset, path: [...path, key] });
        }
        walk(value, [...path, key]);
      }
    } else if (node.type === 'array') {
      (node.children ?? []).forEach((child, i) => walk(child, [...path, i]));
    }
  };

  walk(tree, []);
  return found;
}

/**
 * Append an empty placeholder view to the array at `path`. Returns a new spec, or
 * null when the path does not lead to an array (the text changed since the path
 * was computed). Input is not mutated.
 */
export function appendView(spec: JsonObject, path: SpecPath): JsonObject | null {
  const next = JSON.parse(JSON.stringify(spec)) as JsonObject;
  let node: unknown = next;
  for (const segment of path) {
    if (Array.isArray(node)) node = node[segment as number];
    else if (isJsonObject(node)) node = node[segment as string];
    else return null;
  }
  if (!Array.isArray(node)) return null;
  node.push(placeholderView());
  return next;
}
