// Compile-time injection of an aspect's runtime name. A user aspect carries its accessor name in the
// (type-erased) generic — `class Health extends Aspect<'health', Sprite> {…}` — so neither the
// bundler nor the runtime can see it after types are stripped. This pass injects the matching
// `static aspect = "health"` so node.aspect()/node.health work at runtime, exactly like the SDK's
// built-in aspects declare it by hand. Systems (`class Hud extends System<'hud'>`) are the same
// thing on a scene and get the same injection.
//
// Runs on PROJECT files only (the SDK's own aspects already declare `static aspect`). Idempotent:
// skips a class that already declares one. String-based, like assetMacro — no parser dependency.
// The generic tail is walked with a bracket counter (not a regex) because the third parameter is
// an event map — an object type with braces and `=>` arrows: `Aspect<'barrel', Node, { explode:
// (b: Barrel) => void }>` — and `> implements Hittable {` may follow the closing bracket.

// `class X extends Aspect<'name'` / `extends System<'name'` — up to and including the name literal.
const HEAD_RE = /\bclass\s+\w+\s+extends\s+(?:Aspect|System)\s*<\s*(["'])((?:[^"'\\]|\\.)*)\1/g

/** Cheap pre-check so we don't scan files that define no aspects. */
export const hasAspectExtends = (src: string): boolean => /\bextends\s+(?:Aspect|System)\s*</.test(src)

/** Inject `static aspect = "<name>"` into each `class … extends Aspect<'<name>', …>` (or System). */
export const injectAspectNames = (src: string): string => {
  let out = ""
  let last = 0
  HEAD_RE.lastIndex = 0
  let m: RegExpExecArray | null
  while ((m = HEAD_RE.exec(src)) !== null) {
    const braceEnd = classBodyStart(src, m.index + m[0].length)
    if (braceEnd < 0) continue
    HEAD_RE.lastIndex = braceEnd
    if (!classBodyDeclaresAspect(src, braceEnd)) {
      out += src.slice(last, braceEnd) + ` static aspect = ${JSON.stringify(m[2])};`
      last = braceEnd
    }
  }
  return last === 0 ? src : out + src.slice(last)
}

/** From just past the name literal (inside the `<…>`), walk to the end of the type-argument list,
 *  then past an optional `implements …` clause, and return the index just past the class body's
 *  opening `{` — or -1 if the source doesn't look like a class header. */
const classBodyStart = (src: string, from: number): number => {
  let angle = 1   // we are inside the `<` that HEAD_RE opened
  let i = from
  for (; i < src.length && angle > 0; i++) {
    const c = src[i]
    if (c === '"' || c === "'" || c === "`") { i = skipString(src, i) - 1; continue }
    if (c === "=" && src[i + 1] === ">") { i++; continue }   // an arrow, not a closing bracket
    if (c === "<") angle++
    else if (c === ">") angle--
  }
  if (angle !== 0) return -1
  // `>` closed the generic; the class body's `{` follows, possibly after `implements A, B<C>`.
  // Scan to the first `{` at angle depth 0.
  angle = 0
  for (; i < src.length; i++) {
    const c = src[i]
    if (c === "<") angle++
    else if (c === ">") angle--
    else if (c === "{" && angle === 0) return i + 1
    else if (c === ";" || c === "}") return -1
  }
  return -1
}

/** Index just past a string literal starting at `at` (its opening quote). */
const skipString = (src: string, at: number): number => {
  const q = src[at]
  let i = at + 1
  for (; i < src.length; i++) {
    if (src[i] === "\\") { i++; continue }
    if (src[i] === q) return i + 1
  }
  return i
}

/** Does the class body starting just after `from` (its `{`) already declare a `static aspect`? */
const classBodyDeclaresAspect = (src: string, from: number): boolean => {
  let depth = 1
  let i = from
  for (; i < src.length && depth > 0; i++) {
    const c = src[i]
    if (c === "{") depth++
    else if (c === "}") depth--
  }
  return /\bstatic\b[^;{}()=]*\baspect\b/.test(src.slice(from, i))
}
