Core/Reconciler.cs
using System;
using System.Collections.Generic;
using System.Collections.Immutable;

namespace Goo;

public readonly record struct DiffResult(IReadOnlyList<Op> Ops, IReadOnlyList<string> Warnings);

// Lazy-rented diff output buffers. Identical-tree iterations never add an Op or warning,
// so we don't even touch the pool on the hot path. The pooled lists outlive Reconciler.Diff
// and are read by the caller; BuildContext.ReturnAll returns them after Applier.Apply runs.
internal struct DiffBuffers
{
    public List<Op>? Ops;
    public List<string>? Warnings;

    public void AddOp(Op op) => (Ops ??= BuildContext.Current.RentOpList()).Add(op);
    public void AddWarning(string warning) => (Warnings ??= BuildContext.Current.RentWarningList()).Add(warning);
}

public static class Reconciler
{
    internal static DiffResult Diff<TRoot>(ref Fiber? rootFiber, in TRoot intentRoot)
        where TRoot : struct, IBlob
    {
        DiffBuffers bufs = default;
        var ctx = BuildContext.Current;
        ctx.BeginDiff();
        var path = ctx.RentPath();

        try
        {
            Frame rootFrame = default;
            intentRoot.WriteTo(ref rootFrame);  // direct call, monomorphized - no cast, no boxing

            DiffNode(ref rootFiber, ref rootFrame, 0, path, ref bufs);
            return new DiffResult(
                bufs.Ops ?? (IReadOnlyList<Op>)Array.Empty<Op>(),
                bufs.Warnings ?? (IReadOnlyList<string>)Array.Empty<string>());
        }
        finally
        {
            ctx.ReturnPath(path);
        }
    }

    static void DiffNode(ref Fiber? fiber, ref Frame frame, int index, List<int> path, ref DiffBuffers bufs)
    {
        if (frame.Kind == BlobKind.Cell)
        {
            DiffCell(ref fiber, ref frame, index, path, ref bufs);
            return;
        }

        if (fiber is null)
        {
            fiber = AllocateFiberFor(in frame);
            EmitCreate(in frame, fiber, index, path, ref bufs);
            return;
        }

        if (fiber.Kind != frame.Kind)
        {
            var snap = Snapshot(path);
            bufs.AddOp(Op.RemoveAt(index, snap));
            TeardownTree(fiber);
            fiber = AllocateFiberFor(in frame);
            EmitCreate(in frame, fiber, index, path, ref bufs);
            return;
        }

        switch (frame.Kind)
        {
            case BlobKind.Text:
                if (fiber.Content != frame.Content)
                {
                    bufs.AddOp(Op.UpdateText(index, frame.Content, Snapshot(path)));
                    fiber.Content = frame.Content;
                }
                if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
                {
                    path.Add(index);
                    bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                    UpdateFiberStyle(fiber, frame.Style);
                    path.RemoveAt(path.Count - 1);
                }
                path.Add(index);
                EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                path.RemoveAt(path.Count - 1);
                fiber.Key = frame.Key;
                break;

            case BlobKind.Container:
                path.Add(index);
                DiffChildren(fiber, frame.Children!, path, ref bufs);
                if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
                {
                    bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                    UpdateFiberStyle(fiber, frame.Style);
                }
                EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                {
                    bool drawChanged =
                        !ReferenceEquals(fiber.Material, frame.Material) ||
                        fiber.Draw != frame.Draw ||
                        !UniformValue.SequenceEqual(fiber.Uniforms, frame.Uniforms);
                    if (drawChanged)
                    {
                        bufs.AddOp(Op.SetDrawState(frame.Material, frame.Uniforms, frame.Draw, Snapshot(path)));
                        fiber.Material = frame.Material;
                        fiber.Uniforms = frame.Uniforms;
                        fiber.Draw     = frame.Draw;
                    }
                }
                fiber.Key = frame.Key;
                path.RemoveAt(path.Count - 1);
                break;

            case BlobKind.Image:
                if (!ReferenceEquals(fiber.Texture, frame.Texture) || fiber.Path != frame.Path)
                {
                    if ((frame.Texture is not null) == (frame.Path is not null))
                    {
                        bufs.AddWarning($"Image at HostPath=[{string.Join(",", path)},{index}] must have exactly one of Texture/Path set; update emitted with both null.");
                        bufs.AddOp(Op.UpdateImage(index, null, null, Snapshot(path)));
                    }
                    else
                    {
                        bufs.AddOp(Op.UpdateImage(index, frame.Texture, frame.Path, Snapshot(path)));
                    }
                    fiber.Texture = frame.Texture;
                    fiber.Path = frame.Path;
                }
                if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
                {
                    path.Add(index);
                    bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                    UpdateFiberStyle(fiber, frame.Style);
                    path.RemoveAt(path.Count - 1);
                }
                path.Add(index);
                EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                path.RemoveAt(path.Count - 1);
                fiber.Key = frame.Key;
                break;

            case BlobKind.ScenePanel:
                bool sceneChanged = !ReferenceEquals(fiber.Scene, frame.Scene) || fiber.Path != frame.Path;
                bool renderOnceChanged = fiber.RenderOnce != frame.RenderOnce;
                if (sceneChanged || renderOnceChanged)
                {
                    if ((frame.Scene is not null) == (frame.Path is not null))
                    {
                        bufs.AddWarning($"ScenePanel at HostPath=[{string.Join(",", path)},{index}] must have exactly one of Scene/ScenePath set; update emitted with both null.");
                        bufs.AddOp(Op.UpdateScenePanel(index, null, null, frame.RenderOnce, Snapshot(path)));
                    }
                    else
                    {
                        bufs.AddOp(Op.UpdateScenePanel(index, frame.Scene, frame.Path, frame.RenderOnce, Snapshot(path)));
                    }
                    fiber.Scene = frame.Scene;
                    fiber.Path = frame.Path;
                    fiber.RenderOnce = frame.RenderOnce;
                }
                if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
                {
                    path.Add(index);
                    bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                    UpdateFiberStyle(fiber, frame.Style);
                    path.RemoveAt(path.Count - 1);
                }
                path.Add(index);
                EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                path.RemoveAt(path.Count - 1);
                fiber.Key = frame.Key;
                break;

            case BlobKind.SvgPanel:
                bool pathChanged  = fiber.Path  != frame.Path;
                bool colorChanged = fiber.Color != frame.Color;
                if (pathChanged || colorChanged)
                {
                    bufs.AddOp(Op.UpdateSvgPanel(index, frame.Path, frame.Color, Snapshot(path)));
                    fiber.Path  = frame.Path;
                    fiber.Color = frame.Color;
                }
                if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
                {
                    path.Add(index);
                    bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                    UpdateFiberStyle(fiber, frame.Style);
                    path.RemoveAt(path.Count - 1);
                }
                path.Add(index);
                EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                path.RemoveAt(path.Count - 1);
                fiber.Key = frame.Key;
                break;

            case BlobKind.Sector:
                if (fiber.Shape != frame.Shape)
                {
                    bufs.AddOp(Op.UpdateSector(index, in frame.Shape, Snapshot(path)));
                    fiber.Shape = frame.Shape;
                }
                if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
                {
                    path.Add(index);
                    bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                    UpdateFiberStyle(fiber, frame.Style);
                    path.RemoveAt(path.Count - 1);
                }
                path.Add(index);
                EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                path.RemoveAt(path.Count - 1);
                fiber.Key = frame.Key;
                break;

            case BlobKind.Arc:
                if (fiber.Shape != frame.Shape)
                {
                    bufs.AddOp(Op.UpdateArc(index, in frame.Shape, Snapshot(path)));
                    fiber.Shape = frame.Shape;
                }
                if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
                {
                    path.Add(index);
                    bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                    UpdateFiberStyle(fiber, frame.Style);
                    path.RemoveAt(path.Count - 1);
                }
                path.Add(index);
                EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                path.RemoveAt(path.Count - 1);
                fiber.Key = frame.Key;
                break;

            case BlobKind.Polygon:
                if (!ReferenceEquals(fiber.Points, frame.Points))
                {
                    bufs.AddOp(Op.UpdatePolygon(index, frame.Points, Snapshot(path)));
                    fiber.Points = frame.Points;
                }
                if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
                {
                    path.Add(index);
                    bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                    UpdateFiberStyle(fiber, frame.Style);
                    path.RemoveAt(path.Count - 1);
                }
                path.Add(index);
                EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                path.RemoveAt(path.Count - 1);
                fiber.Key = frame.Key;
                break;

            case BlobKind.WebPanel:
                if (fiber.Path != frame.Path || fiber.Paused != frame.Paused)
                {
                    bufs.AddOp(Op.UpdateWebPanel(index, frame.Path, frame.Paused, Snapshot(path)));
                    fiber.Path = frame.Path;
                    fiber.Paused = frame.Paused;
                }
                if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
                {
                    path.Add(index);
                    bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                    UpdateFiberStyle(fiber, frame.Style);
                    path.RemoveAt(path.Count - 1);
                }
                path.Add(index);
                EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                path.RemoveAt(path.Count - 1);
                fiber.Key = frame.Key;
                break;

            case BlobKind.TextEntry:
                {
                    if (frame.ValueAndInitialTextBothSet)
                        bufs.AddWarning(
                            $"TextEntry at HostPath=[{string.Join(",", path)},{index}] " +
                            "has both Value and InitialText set; using Value (controlled mode). " +
                            "Set only one: Value for controlled, InitialText for uncontrolled.");
                    if (frame.Multiline && frame.OnSubmit is not null)
                        bufs.AddWarning(
                            $"TextEntry at HostPath=[{string.Join(",", path)},{index}] " +
                            "has Multiline=true with an OnSubmit handler; OnSubmit will not fire " +
                            "(Enter inserts newline in multiline mode). Use OnChange to observe edits.");
                    bool placeholderChanged = fiber.Placeholder != frame.Placeholder;
                    bool maxLengthChanged = fiber.MaxLength != frame.MaxLength;
                    bool disabledChanged = fiber.Disabled != frame.Disabled;
                    bool numericChanged = fiber.Numeric != frame.Numeric;
                    bool minValueChanged = fiber.MinValue != frame.MinValue;
                    bool maxValueChanged = fiber.MaxValue != frame.MaxValue;
                    bool numberFormatChanged = fiber.NumberFormat != frame.NumberFormat;
                    bool multilineChanged = fiber.Multiline != frame.Multiline;
                    bool onChangeChanged = !ReferenceEquals(fiber.OnChange, frame.OnChange);
                    bool onSubmitChanged = !ReferenceEquals(fiber.OnSubmit, frame.OnSubmit);
                    bool onFocusChanged = !ReferenceEquals(fiber.OnFocus, frame.OnFocus);
                    bool onBlurChanged = !ReferenceEquals(fiber.OnBlur, frame.OnBlur);
                    bool onCancelChanged = !ReferenceEquals(fiber.OnCancel, frame.OnCancel);
                    bool isControlledChanged = fiber.IsControlled != frame.IsControlled;

                    // Controlled mode re-emits Value every render so the engine's HasFocus
                    // guard can no-op in-flight typing. Uncontrolled mode never re-emits
                    // the text on Update (InitialText is one-shot at Create time).
                    bool emit = frame.IsControlled
                              || placeholderChanged || maxLengthChanged || disabledChanged
                              || numericChanged || minValueChanged || maxValueChanged
                              || numberFormatChanged || multilineChanged
                              || onChangeChanged || onSubmitChanged
                              || onFocusChanged || onBlurChanged || onCancelChanged
                              || isControlledChanged;

                    if (emit)
                    {
                        // Uncontrolled mode carries the fiber's last-known text so the Applier has valid state either way.
                        string? textPayload = frame.IsControlled ? frame.Path : fiber.Path;

                        bufs.AddOp(Op.UpdateTextEntry(
                            index,
                            text:         textPayload,
                            isControlled: frame.IsControlled,
                            placeholder:  frame.Placeholder,
                            maxLength:    frame.MaxLength,
                            disabled:     frame.Disabled,
                            numeric:      frame.Numeric,
                            minValue:     frame.MinValue,
                            maxValue:     frame.MaxValue,
                            numberFormat: frame.NumberFormat,
                            multiline:    frame.Multiline,
                            onChange:     frame.OnChange,
                            onSubmit:     frame.OnSubmit,
                            onFocus:      frame.OnFocus,
                            onBlur:       frame.OnBlur,
                            onCancel:     frame.OnCancel,
                            hostPath:     Snapshot(path)));

                        // Always sync fiber state to frame after emit (covers callback
                        // ref rotation, controlled-value updates, all other fields).
                        fiber.Path = textPayload;
                        fiber.Placeholder = frame.Placeholder;
                        fiber.MaxLength = frame.MaxLength;
                        fiber.Disabled = frame.Disabled;
                        fiber.Numeric = frame.Numeric;
                        fiber.MinValue = frame.MinValue;
                        fiber.MaxValue = frame.MaxValue;
                        fiber.NumberFormat = frame.NumberFormat;
                        fiber.Multiline = frame.Multiline;
                        fiber.OnChange = frame.OnChange;
                        fiber.OnSubmit = frame.OnSubmit;
                        fiber.OnFocus = frame.OnFocus;
                        fiber.OnBlur = frame.OnBlur;
                        fiber.OnCancel = frame.OnCancel;
                        fiber.IsControlled = frame.IsControlled;
                    }
                    if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
                    {
                        path.Add(index);
                        bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                        UpdateFiberStyle(fiber, frame.Style);
                        path.RemoveAt(path.Count - 1);
                    }
                    path.Add(index);
                    EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                    path.RemoveAt(path.Count - 1);
                    fiber.Key = frame.Key;
                }
                break;

        }
    }

    // Cell: expand a self-owning stateful instance in place at this slot; its state lives on the fiber.
    static void DiffCell(ref Fiber? fiber, ref Frame frame, int index, List<int> path, ref DiffBuffers bufs)
    {
        // 1. Reuse the fiber's instance when its runtime type matches frame.CellType, else dispose old and create fresh.
        Cell instance;
        if (fiber?.Instance is Cell prior && prior.GetType() == frame.CellType)
            instance = prior;
        else
        {
            if (fiber?.Instance is IDisposable replaced) replaced.Dispose();
            instance = frame.CellFactory!();
        }

        // 2. Refresh props on the persistent instance.
        frame.Configure?.Invoke(instance);

        // 3. Wire the rebuild hook (root marks itself dirty).
        instance.SetRebuildHook(BuildContext.Current.RootRebuild);

        // 4. Detach before recursing so an inner-kind-change teardown cannot dispose the instance we re-attach.
        if (fiber is not null) fiber.Instance = null;

        // 5. Expand to the inner Blob and recurse at the same slot; the normal arms handle it.
        Frame inner = default;
        instance.ExpandInto(ref inner);
        DiffNode(ref fiber, ref inner, index, path, ref bufs);

        // 6. Re-attach the instance and stamp the cell's key onto the slot fiber (keyed reorder needs it).
        // fiber is non-null here: every DiffNode arm assigns it before returning.
        fiber!.Instance = instance;
        fiber.Key = frame.Key;
    }

    // Skip when both sides are all-null. Otherwise emit only on a structural handler change (Delegate.Equals per slot): method-groups and instance-field closures compare equal and skip; local-capture closures differ each Build and correctly rewire.
    static void EmitSetEventsIfDelta(in Frame frame, Fiber fiber, List<int> targetPath, ref DiffBuffers bufs)
    {
        if (!frame.Events.AnyNonNull && !fiber.PrevEvents.AnyNonNull) return;
        if (BlobEvents.ContentsEqual(in fiber.PrevEvents, in frame.Events))
        {
            fiber.PrevEvents = frame.Events;
            return;
        }
        bufs.AddOp(Op.SetEvents(frame.Events, Snapshot(targetPath)));
        fiber.PrevEvents = frame.Events;
    }

    static void DiffChildren(Fiber parent, Children intentChildren, List<int> path, ref DiffBuffers bufs)
    {
        parent.Children ??= new List<Fiber>();
        var prev = parent.Children;
        int nextCount = intentChildren.Count;

        bool prevAllUnkeyed = AllUnkeyed(prev);
        bool nextAllUnkeyed = AllUnkeyed(in intentChildren);
        bool allUnkeyed = prevAllUnkeyed && nextAllUnkeyed;

        bool prevAllKeyed = AllKeyed(prev);
        bool nextAllKeyed = AllKeyed(in intentChildren);
        bool allKeyed = prevAllKeyed && nextAllKeyed;

        if (allKeyed)
        {
            DiffKeyed(parent, in intentChildren, path, ref bufs);
            return;
        }

        if (!allUnkeyed)
        {
            bufs.AddWarning($"Mixed keyed/unkeyed children at HostPath=[{string.Join(",", path)}]; use all-keyed or all-unkeyed children per list. {DescribeKeyMix(in intentChildren)}");
            DiffPositional(parent, in intentChildren, path, ref bufs, warnLengthChange: false);
            return;
        }

        DiffPositional(parent, in intentChildren, path, ref bufs, warnLengthChange: true);
    }

    static void DiffPositional(Fiber parent, in Children intentChildren, List<int> path, ref DiffBuffers bufs, bool warnLengthChange)
    {
        var prev = parent.Children!;
        int nextCount = intentChildren.Count;

        if (warnLengthChange && prev.Count != nextCount)
            bufs.AddWarning($"Keyless child list at HostPath=[{string.Join(",", path)}] changed length {prev.Count}->{nextCount}.");

        int common = Math.Min(prev.Count, nextCount);

        for (int i = 0; i < common; i++)
        {
            Fiber? f = prev[i];
            DiffNode(ref f, ref intentChildren[i], i, path, ref bufs);
            prev[i] = f!;
        }

        for (int i = common; i < nextCount; i++)
        {
            Fiber? f = null;
            DiffNode(ref f, ref intentChildren[i], i, path, ref bufs);
            prev.Add(f!);
        }

        for (int i = prev.Count - 1; i >= common && prev.Count > nextCount; i--)
        {
            bufs.AddOp(Op.RemoveAt(i, Snapshot(path)));
            TeardownTree(prev[i]);
            prev.RemoveAt(i);
        }
    }

    // Dispose every IDisposable Cell instance in a fiber tree; entry point for panel teardown on disable/remount.
    internal static void TeardownFiberTree(Fiber? fiber)
    {
        if (fiber is not null) TeardownTree(fiber);
    }

    // Dispose any IDisposable Cell instance in a fiber subtree that is leaving the tree for good.
    static void TeardownTree(Fiber fiber)
    {
        if (fiber.Instance is IDisposable d) d.Dispose();
        if (fiber.Children is { } kids)
            for (int i = 0; i < kids.Count; i++) TeardownTree(kids[i]);
    }

    static void DiffKeyed(Fiber parent, in Children intentChildren, List<int> path, ref DiffBuffers bufs)
    {
        var prev = parent.Children!;
        int nextCount = intentChildren.Count;
        var ctx = BuildContext.Current;

        // prevByKey: identity lookup of the surviving Fiber for a key (stays valid through prev mutation).
        // nextKeys:  membership probe for the removal pass.
        // The redundant "current" key mirror that the old code carried is gone; IndexOfKey scans prev directly.
        var prevByKey = ctx.RentKeyedDict();
        var nextKeys  = ctx.RentKeySet();

        try
        {
            foreach (var f in prev) prevByKey[f.Key!] = f;
            for (int i = 0; i < nextCount; i++) nextKeys.Add(intentChildren[i].Key!);

            for (int i = prev.Count - 1; i >= 0; i--)
            {
                if (!nextKeys.Contains(prev[i].Key!))
                {
                    bufs.AddOp(Op.RemoveAt(i, Snapshot(path)));
                    TeardownTree(prev[i]);
                    prev.RemoveAt(i);
                }
            }

            for (int newIdx = 0; newIdx < nextCount; newIdx++)
            {
                var nKey = intentChildren[newIdx].Key!;

                if (!prevByKey.ContainsKey(nKey))
                {
                    Fiber? f = null;
                    DiffNode(ref f, ref intentChildren[newIdx], newIdx, path, ref bufs);
                    prev.Insert(newIdx, f!);
                    prevByKey[nKey] = f!;
                    continue;
                }

                int oldIdx = IndexOfKey(prev, nKey);
                if (oldIdx != newIdx)
                {
                    bufs.AddOp(Op.MoveAt(oldIdx, newIdx, Snapshot(path)));
                    var moved = prev[oldIdx];
                    prev.RemoveAt(oldIdx);
                    prev.Insert(newIdx, moved);
                }

                Fiber existing = prevByKey[nKey];
                Fiber? slot = existing;
                DiffNode(ref slot, ref intentChildren[newIdx], newIdx, path, ref bufs);
                prev[newIdx] = slot!;
            }
        }
        finally
        {
            ctx.ReturnKeyedDict(prevByKey);
            ctx.ReturnKeySet(nextKeys);
        }
    }

    static int IndexOfKey(List<Fiber> prev, string key)
    {
        for (int i = 0; i < prev.Count; i++) if (prev[i].Key == key) return i;
        return -1;
    }

    static void EmitCreate(in Frame frame, Fiber fiber, int index, List<int> path, ref DiffBuffers bufs)
    {
        switch (frame.Kind)
        {
            case BlobKind.Text:
                bufs.AddOp(Op.CreateText(index, frame.Content, Snapshot(path)));
                if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
                {
                    path.Add(index);
                    bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                    UpdateFiberStyle(fiber, frame.Style);
                    path.RemoveAt(path.Count - 1);
                }
                path.Add(index);
                EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                path.RemoveAt(path.Count - 1);
                break;

            case BlobKind.Container:
                {
                    bufs.AddOp(Op.CreateContainer(index, Snapshot(path)));
                    path.Add(index);
                    fiber.Children = new List<Fiber>();
                    var kids = frame.Children!;
                    for (int i = 0; i < kids.Count; i++)
                    {
                        Fiber? child = null;
                        DiffNode(ref child, ref kids[i], i, path, ref bufs);
                        fiber.Children.Add(child!);
                    }
                    if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
                    {
                        bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                        UpdateFiberStyle(fiber, frame.Style);
                    }
                    EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                    if (frame.Material is not null || !frame.Uniforms.IsDefaultOrEmpty || frame.Draw is not null)
                    {
                        bufs.AddOp(Op.SetDrawState(frame.Material, frame.Uniforms, frame.Draw, Snapshot(path)));
                    }
                    path.RemoveAt(path.Count - 1);
                }
                break;

            case BlobKind.Image:
                {
                    if ((frame.Texture is not null) == (frame.Path is not null))
                    {
                        bufs.AddWarning($"Image at HostPath=[{string.Join(",", path)}] must have exactly one of Texture/Path set; emitted with both null.");
                        bufs.AddOp(Op.CreateImage(index, null, null, Snapshot(path)));
                    }
                    else
                    {
                        bufs.AddOp(Op.CreateImage(index, frame.Texture, frame.Path, Snapshot(path)));
                    }
                    if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
                    {
                        path.Add(index);
                        bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                        UpdateFiberStyle(fiber, frame.Style);
                        path.RemoveAt(path.Count - 1);
                    }
                    fiber.Texture = frame.Texture;
                    fiber.Path = frame.Path;
                    path.Add(index);
                    EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                    path.RemoveAt(path.Count - 1);
                }
                break;

            case BlobKind.ScenePanel:
                {
                    if ((frame.Scene is not null) == (frame.Path is not null))
                    {
                        bufs.AddWarning($"ScenePanel at HostPath=[{string.Join(",", path)}] must have exactly one of Scene/ScenePath set; emitted with both null.");
                        bufs.AddOp(Op.CreateScenePanel(index, null, null, frame.RenderOnce, Snapshot(path)));
                    }
                    else
                    {
                        bufs.AddOp(Op.CreateScenePanel(index, frame.Scene, frame.Path, frame.RenderOnce, Snapshot(path)));
                    }
                    if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
                    {
                        path.Add(index);
                        bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                        UpdateFiberStyle(fiber, frame.Style);
                        path.RemoveAt(path.Count - 1);
                    }
                    fiber.Scene = frame.Scene;
                    fiber.Path = frame.Path;
                    fiber.RenderOnce = frame.RenderOnce;
                    path.Add(index);
                    EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                    path.RemoveAt(path.Count - 1);
                }
                break;

            case BlobKind.SvgPanel:
                {
                    bufs.AddOp(Op.CreateSvgPanel(index, frame.Path, frame.Color, Snapshot(path)));
                    if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
                    {
                        path.Add(index);
                        bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                        UpdateFiberStyle(fiber, frame.Style);
                        path.RemoveAt(path.Count - 1);
                    }
                    fiber.Path = frame.Path;
                    fiber.Color = frame.Color;
                    path.Add(index);
                    EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                    path.RemoveAt(path.Count - 1);
                }
                break;

            case BlobKind.Sector:
                {
                    bufs.AddOp(Op.CreateSector(index, in frame.Shape, Snapshot(path)));
                    if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
                    {
                        path.Add(index);
                        bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                        UpdateFiberStyle(fiber, frame.Style);
                        path.RemoveAt(path.Count - 1);
                    }
                    fiber.Shape = frame.Shape;
                    path.Add(index);
                    EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                    path.RemoveAt(path.Count - 1);
                }
                break;

            case BlobKind.Arc:
                {
                    bufs.AddOp(Op.CreateArc(index, in frame.Shape, Snapshot(path)));
                    if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
                    {
                        path.Add(index);
                        bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                        UpdateFiberStyle(fiber, frame.Style);
                        path.RemoveAt(path.Count - 1);
                    }
                    fiber.Shape = frame.Shape;
                    path.Add(index);
                    EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                    path.RemoveAt(path.Count - 1);
                }
                break;

            case BlobKind.Polygon:
                {
                    bufs.AddOp(Op.CreatePolygon(index, frame.Points, Snapshot(path)));
                    if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
                    {
                        path.Add(index);
                        bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                        UpdateFiberStyle(fiber, frame.Style);
                        path.RemoveAt(path.Count - 1);
                    }
                    fiber.Points = frame.Points;
                    path.Add(index);
                    EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                    path.RemoveAt(path.Count - 1);
                }
                break;

            case BlobKind.WebPanel:
                {
                    bufs.AddOp(Op.CreateWebPanel(index, frame.Path, frame.Paused, Snapshot(path)));
                    if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
                    {
                        path.Add(index);
                        bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                        UpdateFiberStyle(fiber, frame.Style);
                        path.RemoveAt(path.Count - 1);
                    }
                    fiber.Path = frame.Path;
                    fiber.Paused = frame.Paused;
                    path.Add(index);
                    EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                    path.RemoveAt(path.Count - 1);
                }
                break;

            case BlobKind.TextEntry:
                {
                    if (frame.ValueAndInitialTextBothSet)
                        bufs.AddWarning(
                            $"TextEntry at HostPath=[{string.Join(",", path)},{index}] " +
                            "has both Value and InitialText set; using Value (controlled mode). " +
                            "Set only one: Value for controlled, InitialText for uncontrolled.");
                    if (frame.Multiline && frame.OnSubmit is not null)
                        bufs.AddWarning(
                            $"TextEntry at HostPath=[{string.Join(",", path)},{index}] " +
                            "has Multiline=true with an OnSubmit handler; OnSubmit will not fire " +
                            "(Enter inserts newline in multiline mode). Use OnChange to observe edits.");
                    bufs.AddOp(Op.CreateTextEntry(
                        index,
                        text:         frame.Path,
                        isControlled: frame.IsControlled,
                        placeholder:  frame.Placeholder,
                        maxLength:    frame.MaxLength,
                        disabled:     frame.Disabled,
                        numeric:      frame.Numeric,
                        minValue:     frame.MinValue,
                        maxValue:     frame.MaxValue,
                        numberFormat: frame.NumberFormat,
                        multiline:    frame.Multiline,
                        onChange:     frame.OnChange,
                        onSubmit:     frame.OnSubmit,
                        onFocus:      frame.OnFocus,
                        onBlur:       frame.OnBlur,
                        onCancel:     frame.OnCancel,
                        hostPath:     Snapshot(path)));
                    if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
                    {
                        path.Add(index);
                        bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
                        UpdateFiberStyle(fiber, frame.Style);
                        path.RemoveAt(path.Count - 1);
                    }
                    fiber.Path = frame.Path;
                    fiber.Placeholder = frame.Placeholder;
                    fiber.MaxLength = frame.MaxLength;
                    fiber.Disabled = frame.Disabled;
                    fiber.Numeric = frame.Numeric;
                    fiber.MinValue = frame.MinValue;
                    fiber.MaxValue = frame.MaxValue;
                    fiber.NumberFormat = frame.NumberFormat;
                    fiber.Multiline = frame.Multiline;
                    fiber.OnChange = frame.OnChange;
                    fiber.OnSubmit = frame.OnSubmit;
                    fiber.OnFocus = frame.OnFocus;
                    fiber.OnBlur = frame.OnBlur;
                    fiber.OnCancel = frame.OnCancel;
                    fiber.IsControlled = frame.IsControlled;
                    path.Add(index);
                    EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
                    path.RemoveAt(path.Count - 1);
                }
                break;

            default:
                throw new InvalidOperationException($"Unknown BlobKind {frame.Kind}.");
        }
    }

    static Fiber AllocateFiberFor(in Frame frame)
    {
        return new Fiber
        {
            Kind = frame.Kind,
            Key = frame.Key,
            Content = frame.Content,
            Style = StyleList.Empty,
            Children = frame.Kind == BlobKind.Container ? new List<Fiber>() : null,
            Texture = frame.Texture,
            Path = frame.Path,
            Scene = frame.Scene,
            RenderOnce = frame.RenderOnce,
            Paused = frame.Paused,
            Color = frame.Color,
            Shape = frame.Shape,
            Points = frame.Points,
            Material = frame.Material,
            Uniforms = frame.Uniforms.IsDefault ? ImmutableArray<UniformValue>.Empty : frame.Uniforms,
            Draw     = frame.Draw,
            Placeholder = frame.Placeholder,
            MaxLength = frame.MaxLength,
            Disabled = frame.Disabled,
            Numeric = frame.Numeric,
            MinValue = frame.MinValue,
            MaxValue = frame.MaxValue,
            NumberFormat = frame.NumberFormat,
            Multiline = frame.Multiline,
            OnChange = frame.OnChange,
            OnSubmit = frame.OnSubmit,
            OnFocus = frame.OnFocus,
            OnBlur = frame.OnBlur,
            OnCancel = frame.OnCancel,
            IsControlled = frame.IsControlled,
        };
    }

    static void UpdateFiberStyle(Fiber fiber, StyleList incoming)
    {
        if (incoming == StyleList.Empty || incoming.Count == 0)
        {
            fiber.Style = StyleList.Empty;
            return;
        }
        if (fiber.Style == StyleList.Empty)
            fiber.Style = new StyleList();
        fiber.Style.CopyFrom(incoming);
    }

    static bool AllUnkeyed(List<Fiber> fibers)
    {
        foreach (var f in fibers) if (f.Key is not null) return false;
        return true;
    }

    static bool AllUnkeyed(in Children children)
    {
        for (int i = 0; i < children.Count; i++) if (children[i].Key is not null) return false;
        return true;
    }

    static bool AllKeyed(List<Fiber> fibers)
    {
        foreach (var f in fibers) if (f.Key is null) return false;
        return true;
    }

    static bool AllKeyed(in Children children)
    {
        for (int i = 0; i < children.Count; i++) if (children[i].Key is null) return false;
        return true;
    }

    static int[] Snapshot(List<int> path) => BuildContext.Current.RentHostPath(path);

    // Slow-path diagnostic for the mixed-keyed warning: lists which sibling indices are keyed vs unkeyed (truncated per side) so the developer can spot the offender.
    static string DescribeKeyMix(in Children intentChildren)
    {
        const int MaxListed = 8;
        var unkeyed = new System.Text.StringBuilder();
        var keyed = new System.Text.StringBuilder();
        int unkeyedCount = 0, keyedCount = 0;
        for (int i = 0; i < intentChildren.Count; i++)
        {
            var key = intentChildren[i].Key;
            if (key is null)
            {
                if (unkeyedCount < MaxListed)
                {
                    if (unkeyedCount > 0) unkeyed.Append(',');
                    unkeyed.Append(i);
                }
                unkeyedCount++;
            }
            else
            {
                if (keyedCount < MaxListed)
                {
                    if (keyedCount > 0) keyed.Append(',');
                    keyed.Append(i).Append(":\"").Append(key).Append('"');
                }
                keyedCount++;
            }
        }
        if (unkeyedCount > MaxListed) unkeyed.Append(",...");
        if (keyedCount > MaxListed) keyed.Append(",...");
        return $"Intent ({intentChildren.Count} children): unkeyed at [{unkeyed}]; keyed at [{keyed}].";
    }
}