using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using ytLive.Models;
using ytLive.Services.Compositor;
namespace ytLive.Services;
///
/// The scene graph (TASK 31): single owner of the scene collection and the
/// element inventory / mutation surface, plus the static/dynamic classification
/// queries the compositor optimization depends on.
///
/// Scenes are referenced (not owned): the same instances live
/// on the ViewModel's bindable surface; SceneGraph adds/removes/reorders their
/// and answers queries (background / webcam / chat /
/// split point) that used to be scattered LINQ across MainViewModel.
///
/// Classification: (images, background art) is
/// baked once; (webcam, live capture, chat, web)
/// is composited per frame. The split point is the index of the first
/// dynamic element — everything below it forms the baked base.
///
public sealed class SceneGraph
{
private readonly Dictionary _regionCache = new(StringComparer.Ordinal);
private readonly object _gate = new();
/// The shared scene collection (same instances the ViewModel binds).
public ObservableCollection Scenes { get; } = new();
/// The one Background row for a scene — null when the scene has none.
public Source? GetBackground(Scene scene)
=> scene.Elements.OfType().FirstOrDefault(s => s.IsBackground);
/// This scene's webcam usage, if any.
public WebcamSceneConfig? GetWebcam(Scene scene)
=> scene.Elements.OfType().FirstOrDefault();
/// All chat-box elements across every scene (for ChatOverlayLayer).
public IEnumerable GetChatBoxes()
=> Scenes.SelectMany(s => s.Elements.OfType().Where(x => x.Type == SourceType.ChatBox));
/// Index of the first dynamic element (the split point). If no dynamic
/// element exists this equals count — fully baked.
public int GetSplitPoint(Scene scene)
{
var elements = scene.Elements;
for (var i = 0; i < elements.Count; i++)
{
if (elements[i].Kind == ElementKind.Dynamic)
return i;
}
return elements.Count;
}
/// True when the whole scene is static (no dynamic element) — the
/// compositor can bake it once and skip per-frame compositing.
public bool IsStatic(Scene scene)
=> scene.Elements.All(e => e.Kind == ElementKind.Static);
/// Invalidate the cached bake region for a scene after any mutation
/// (element add/remove/reorder, static resize/opacity/visibility, asset change).
public void InvalidateBake(Scene scene)
{
lock (_gate) _regionCache.Remove(scene.Id);
}
// ─── Mutation surface (single owner of element operations) ───
/// Append an element to a scene and (re)bake accordingly.
public void AddElement(Scene scene, SceneElement element)
{
scene.Elements.Add(element);
InvalidateBake(scene);
}
/// Insert an element at a specific z-index and (re)bake accordingly.
public void InsertElement(Scene scene, int index, SceneElement element)
{
scene.Elements.Insert(index, element);
InvalidateBake(scene);
}
/// Remove an element and (re)bake accordingly.
public bool RemoveElement(Scene scene, SceneElement element)
{
var removed = scene.Elements.Remove(element);
if (removed) InvalidateBake(scene);
return removed;
}
/// Reorder an element in z-space and (re)bake accordingly.
public void MoveElement(Scene scene, SceneElement element, int newIndex)
{
var current = scene.Elements.IndexOf(element);
if (current < 0) return;
scene.Elements.Move(current, newIndex);
InvalidateBake(scene);
}
/// Add a scene to the collection (wiring is handled by the ViewModel).
public void AddScene(Scene scene) => Scenes.Add(scene);
/// Get (or bake) the cached static base for a scene. Null means "no static
/// base" (first layer is dynamic or the scene is empty) — caller falls back to full render.
public VideoFrame? GetBakedBase(
Scene scene,
Func frameFor,
Func options,
VideoFrame? socialBarFrame = null,
int socialBarTop = 0,
VideoFrame? flashFrame = null)
{
var split = GetSplitPoint(scene);
if (split == 0) return null;
lock (_gate)
{
if (_regionCache.TryGetValue(scene.Id, out var cached)
&& cached.Matches(scene, split))
return cached.Frame;
}
var baked = SceneCompositor.BakeStaticBase(
scene, split, frameFor, options, socialBarFrame, socialBarTop, flashFrame);
if (baked == null) return null;
lock (_gate)
{
_regionCache[scene.Id] = new SceneRegion(baked, scene.Elements.Take(split).ToList());
}
return baked;
}
/// The baked frame + the element identity it was baked from (for invalidation).
private sealed class SceneRegion
{
public VideoFrame Frame { get; }
private readonly string[] _staticElementIds;
public SceneRegion(VideoFrame frame, List staticBelow)
{
Frame = frame;
_staticElementIds = staticBelow.Select(e => e.Id).ToArray();
}
/// Only static layers below the split are part of the base. Two bakes
/// match iff the same static element identities, in the same order, are below
/// the split — dynamic-only pixel/layout changes never invalidate.
public bool Matches(Scene scene, int split)
{
var below = scene.Elements.Take(split).Select(e => e.Id).ToArray();
return below.Length == _staticElementIds.Length
&& below.SequenceEqual(_staticElementIds);
}
}
}