using System;
using System.Collections.Generic;
using System.Numerics;
namespace AcDream.Core.Lighting;
///
/// Manages the registered dynamic lights in the world and picks the 8
/// most relevant ones each frame for the shader to consume. Matches
/// retail's fixed-function-era "8 hardware lights" constraint (r13
/// §12.2).
///
///
/// Active-light selection algorithm (r13 §12.2), as implemented by
/// :
///
/// -
/// Reserve slot 0 for the sun (directional, infinite range) when present.
///
/// -
/// For every registered lit point/spot light, recompute DistSq
/// from the viewer and keep the nearest (MaxActiveLights − sunSlot)
/// directly in the active window via an allocation-free insertion
/// partial-select (no per-frame list/sort).
///
///
/// There is deliberately NO viewer-range candidacy filter: each light's
/// own range cutoff is applied PER SURFACE in the shader
/// (mesh_modern.frag: d < range), so a torch the viewer
/// stands outside the range of must still light the wall it sits on. The
/// earlier Range² × 1.1 slack filter wrongly dropped exactly those
/// lights (the #133 "lighting off" report).
///
///
///
/// Not thread-safe — the render thread owns the light list.
///
///
public sealed class LightManager
{
public const int MaxActiveLights = 8; // D3D parity
private readonly List _all = new();
private readonly LightSource?[] _active = new LightSource?[MaxActiveLights];
private int _activeCount;
/// Current cell ambient state applied to everything.
public CellAmbientState CurrentAmbient { get; set; }
///
/// The sun (or "global directional") — always slot 0 of the active
/// list. Set this from the
/// each frame.
///
public LightSource? Sun { get; set; }
/// Snapshot of the currently-active lights (up to 8).
public ReadOnlySpan Active => _active.AsSpan(0, _activeCount);
public int ActiveCount => _activeCount;
public int RegisteredCount => _all.Count;
/// Add a light. Idempotent — adding the same instance twice is a no-op.
public void Register(LightSource light)
{
ArgumentNullException.ThrowIfNull(light);
foreach (var existing in _all)
if (ReferenceEquals(existing, light)) return;
_all.Add(light);
}
/// Remove by reference.
public void Unregister(LightSource light)
{
_all.Remove(light);
}
/// Remove every light attached to a specific entity.
public void UnregisterByOwner(uint ownerId)
{
_all.RemoveAll(l => l.OwnerId == ownerId);
}
public void Clear()
{
_all.Clear();
Array.Clear(_active);
_activeCount = 0;
}
///
/// Refresh the active-light list for the current viewer position.
/// Called once per render frame from the render thread; the shader
/// reads and uploads to the light UBO.
///
public void Tick(Vector3 viewerWorldPos)
{
// Retail D3D-style fixed-pipeline lighting takes the nearest (MaxActiveLights-1)
// point lights (slot 0 is the sun) and applies each light's hard range cutoff
// PER SURFACE in the shader (mesh_modern.frag: `if (d < range && range > 1e-3)`),
// NOT a viewer-range candidacy filter — a torch the viewer stands outside the
// range of must still light the wall it sits on.
//
// Allocation-free partial selection: the old path built `new List<>(N)` and
// ran an O(N log N) Sort EVERY FRAME; in a dungeon N is thousands of torches,
// so that allocated a large list per frame (GC pressure → FPS). Instead keep
// the nearest maxPoint directly in the _active window, maintained sorted by
// insertion. O(N · maxPoint), maxPoint ≤ 8, zero allocation.
Array.Clear(_active);
_activeCount = 0;
// Slot 0 = sun when present (directional; never ranked by distance).
int baseSlot = 0;
if (Sun is not null)
{
_active[0] = Sun;
baseSlot = 1;
}
int maxPoint = MaxActiveLights - baseSlot;
int filled = 0;
if (maxPoint > 0)
{
foreach (var light in _all)
{
if (!light.IsLit || light.Kind == LightKind.Directional) continue;
Vector3 delta = light.WorldPosition - viewerWorldPos;
light.DistSq = delta.LengthSquared();
// Maintain _active[baseSlot .. baseSlot+filled) sorted ascending by
// DistSq. Insert if there's room or this light is nearer than the
// current farthest (then the farthest falls off the end).
if (filled < maxPoint)
{
int j = baseSlot + filled;
while (j > baseSlot && _active[j - 1]!.DistSq > light.DistSq)
{
_active[j] = _active[j - 1];
j--;
}
_active[j] = light;
filled++;
}
else if (light.DistSq < _active[baseSlot + maxPoint - 1]!.DistSq)
{
int j = baseSlot + maxPoint - 1;
while (j > baseSlot && _active[j - 1]!.DistSq > light.DistSq)
{
_active[j] = _active[j - 1];
j--;
}
_active[j] = light;
}
}
}
_activeCount = baseSlot + filled;
}
}