// Fine-grained reactivity for the retained-mode UI: signal() / computed() / effect() globals plus
// the element-binding helpers UINode/UIText use for function-valued style props and text.
//
// Design notes (the parts that are deliberate, not incidental):
// - Dependents (effects, computeds) hold their sources STRONGLY; sources hold dependents WEAKLY
//   (a WeakRef in the subscriber set). An element owns its binding effects, the effect's closure
//   owns the element — so when user code drops a subtree, the whole element+effect subgraph
//   becomes unreachable and collects, and the signal prunes the dead WeakRef on its next write.
//   No unmount hook is needed and re-appending a kept subtree keeps its bindings alive.
// - Free-standing effect() computations are additionally held in a strong root set (otherwise a
//   `effect(() => ...)` whose handle the user discards would be GC'd mid-flight); dispose()
//   releases them. Element bindings are NOT in the root set — the element is their owner.
// - Writes are batched: a signal write schedules a microtask flush; each dirty effect runs once
//   per flush. The first run of any effect/binding is synchronous, so element styles hold
//   concrete values immediately after construction (hosts serialize _style at mount).
// - QuickJS-safe: promise microtasks (the hosts drain pending jobs), WeakRef with a strong-ref
//   fallback for engines without it (then stale subscribers just live until the run ends —
//   every run executes in a fresh JSRuntime anyway).

interface WeakRefLike<T extends object> { deref(): T | undefined }

const WeakRefImpl: new <T extends object>(target: T) => WeakRefLike<T> =
  typeof WeakRef !== "undefined" ? WeakRef : class StrongRef<T extends object> {
    private _t: T
    constructor(target: T) { this._t = target }
    deref(): T | undefined { return this._t }
  }

interface Computation {
  _ref: WeakRefLike<Computation>
  _deps: Dep[]
  _disposed: boolean
  _invalidate(): void
}

let activeComputation: Computation | null = null

/** Removes `c` from every source it subscribed to on its last run. */
const cleanupDeps = (c: Computation) => {
  for (const dep of c._deps) dep._subs.delete(c._ref)
  c._deps.length = 0
}

class Dep {
  _subs = new Set<WeakRefLike<Computation>>()

  _track() {
    const c = activeComputation
    if (c === null || this._subs.has(c._ref)) return
    this._subs.add(c._ref)
    c._deps.push(this)
  }

  _notify() {
    for (const ref of [ ...this._subs ]) {
      const c = ref.deref()
      if (c === undefined || c._disposed) {
        this._subs.delete(ref)   // lazy pruning of collected/disposed subscribers
      } else {
        c._invalidate()
      }
    }
  }
}

// ---- effect scheduling (microtask batch) ----

const queue = new Set<EffectImpl>()
let flushScheduled = false

const flush = () => {
  // Effects may write signals, re-filling the queue mid-flush; drain until empty with a guard
  // against write-loops (effect A invalidating itself / a cycle of effects).
  let runs = 0
  while (queue.size > 0) {
    if (++runs > 10000) {
      queue.clear()
      console.warn("signals: effect flush exceeded 10000 runs — possible infinite update loop, remaining effects dropped")
      break
    }
    const first: EffectImpl = queue.values().next().value!
    queue.delete(first)
    first._run()
  }
  flushScheduled = false
}

const schedule = (fx: EffectImpl) => {
  queue.add(fx)
  if (!flushScheduled) {
    flushScheduled = true
    Promise.resolve().then(flush)
  }
}

/** Runs every pending effect now instead of waiting for the microtask (test/host hook). */
export const flushEffects = (): void => {
  if (queue.size > 0) flush()
}

class EffectImpl implements Computation {
  _ref: WeakRefLike<Computation>
  _deps: Dep[] = []
  _disposed = false
  private _fn: () => void

  constructor(fn: () => void) {
    this._fn = fn
    this._ref = new WeakRefImpl(this)
  }

  _run() {
    if (this._disposed) return
    cleanupDeps(this)
    const prev = activeComputation
    activeComputation = this
    try {
      this._fn()
    } finally {
      activeComputation = prev
    }
  }

  _invalidate() {
    schedule(this)
  }

  _dispose() {
    if (this._disposed) return
    this._disposed = true
    cleanupDeps(this)
    queue.delete(this)
    rootEffects.delete(this)
  }
}

// Free-standing effect() handles the user may never store — kept alive here until disposed.
const rootEffects = new Set<EffectImpl>()

// ---- public surface ----

export interface Signal<T> {
  value: T,
  /** Read without subscribing the current effect/computed. */
  peek(): T,
}

export interface Computed<T> {
  readonly value: T,
  /** Read without subscribing the current effect/computed (still recomputes if stale). */
  peek(): T,
}

class SignalImpl<T> {
  private _dep = new Dep()
  private _v: T

  constructor(value: T) {
    this._v = value
  }

  get value(): T {
    this._dep._track()
    return this._v
  }

  set value(next: T) {
    if (Object.is(next, this._v)) return
    this._v = next
    this._dep._notify()
  }

  peek(): T {
    return this._v
  }
}

class ComputedImpl<T> implements Computation {
  _ref: WeakRefLike<Computation>
  _deps: Dep[] = []
  _disposed = false
  private _dep = new Dep()
  private _fn: () => T
  private _stale = true
  private _v!: T

  constructor(fn: () => T) {
    this._fn = fn
    this._ref = new WeakRefImpl(this)
  }

  get value(): T {
    this._dep._track()
    if (this._stale) this._recompute()
    return this._v
  }

  peek(): T {
    if (this._stale) this._recompute()
    return this._v
  }

  private _recompute() {
    cleanupDeps(this)
    const prev = activeComputation
    activeComputation = this
    try {
      this._v = this._fn()
      this._stale = false
    } finally {
      activeComputation = prev
    }
  }

  // Lazy: a dep change only marks the memo stale and passes the wave downstream; the next read
  // recomputes.
  _invalidate() {
    if (this._stale) return
    this._stale = true
    this._dep._notify()
  }
}

/**
 * A reactive value. Reading `.value` inside an `effect`/`computed` (or a function-valued style
 * prop / `UIText` content) subscribes it; writing `.value` re-runs subscribers (batched — once
 * per microtask).
 */
export function signal<T>(initialValue: T): Signal<T>
export function signal<T = undefined>(): Signal<T | undefined>
export function signal<T>(initialValue?: T): Signal<T | undefined> {
  return new SignalImpl(initialValue)
}

/** A lazily-cached derived value: recomputes on read after any of its dependencies changed. */
export function computed<T>(fn: () => T): Computed<T> {
  return new ComputedImpl(fn)
}

/**
 * Runs `fn` now and again whenever any signal/computed it read changes (batched per microtask).
 * Returns a dispose function; an undisposed effect lives for the rest of the app run.
 */
export function effect(fn: () => void): () => void {
  const fx = new EffectImpl(fn)
  rootEffects.add(fx)
  fx._run()
  return () => fx._dispose()
}

// ---- element bindings (internal — used by UINode/UIText for function-valued props) ----

/** The shape UINode's Element exposes for binding ownership. */
export interface BindingHost {
  /** @internal */ _fx?: Map<string, EffectImpl>
}

/**
 * @internal Creates (or replaces) the reactive binding stored under `key` on the element. The
 * effect is owned by the element (strong ref in `_fx`), NOT by the root set — discard the element
 * and the binding collects with it. Runs synchronously once.
 */
export function createBinding(el: BindingHost, key: string, run: () => void): void {
  const map = el._fx ?? (el._fx = new Map())
  map.get(key)?._dispose()
  const fx = new EffectImpl(run)
  map.set(key, fx)
  fx._run()
}

/** @internal Disposes the binding stored under `key`, if any (static value overriding a binding). */
export function disposeBinding(el: BindingHost, key: string): void {
  const fx = el._fx?.get(key)
  if (fx === undefined) return
  fx._dispose()
  el._fx!.delete(key)
}
