using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using IFSEngine.Model;
using IFSRenderer.AvaloniaShell;
using IFSRenderer.AvaloniaShell.Rendering;
namespace IFSRenderer.AvaloniaShell.Tests.Fakes;
/// <summary>
/// An <see cref="IRenderQueue"/> with no GPU: it parses the real plugin library from disk and
/// records engine calls, but draws nothing.
/// </summary>
/// <remarks>
/// <para>
/// <b>Actions are DEFERRED, exactly as the real service defers them.</b> This is the whole
/// point of the fake and it must not be "simplified" into running them inline. The bug this
/// harness exists to pin — node and post-effect edits appearing one action late — is an
/// ORDERING bug: the UI read the fractal before the queued mutation had run. A fake that
/// executed <see cref="Post"/> synchronously would make the broken code pass, because the
/// mutation would already have happened by the time the UI looked.
/// </para>
/// <para>
/// So a test drives the queue the way the render thread does: perform the gesture, then
/// <see cref="Drain"/>, then pump the dispatcher for anything the action posted back.
/// </para>
/// </remarks>
public sealed class FakeRenderQueue : IRenderQueue
{
private readonly Queue<Action<IRenderer>> _pending = new();
/// <summary>The engine stand-in handed to every queued action.</summary>
public FakeRenderer Renderer { get; } = new();
/// <summary>Actions queued but not yet run — non-zero means the UI is ahead of the engine.</summary>
public int PendingCount => _pending.Count;
/// <summary>Total actions ever queued, for asserting a gesture did any engine work at all.</summary>
public int PostCount { get; private set; }
public IReadOnlyList<TransformPlugin> Transforms { get; private set; } = [];
public IReadOnlyList<EffectPlugin> Effects { get; private set; } = [];
public IFS Current { get; private set; }
public object FrameLock { get; } = new();
public byte[] FrameBuffer { get; } = [];
public int FrameWidth => 0;
public int FrameHeight => 0;
public event Action FrameReady;
public event Action<Exception> Failed;
public string GpuName => "(fake)";
public Exception LastError { get; private set; }
public ulong TotalIterations => 0;
public int IterationLevel => 0;
public double Fps => 0;
public double IterationSpeed => 0;
public int InvocationIters => 0;
public int WorkgroupCount => Renderer.WorkgroupCount;
/// <summary>Mirrors RenderService: resuming clears the recorded failure.</summary>
public bool Paused
{
get => _paused;
set
{
if (!value)
LastError = null;
_paused = value;
}
}
private bool _paused;
/// <summary>Loads the real plugin files. No GL context is created.</summary>
public async Task StartAsync(string libraryDir, string transformsDir)
{
string includesDir = Path.Combine(libraryDir, "Includes");
string postFxDir = Path.Combine(libraryDir, "PostEffects");
var includeFiles = Directory.Exists(includesDir) ? Directory.GetFiles(includesDir) : [];
var includeNames = includeFiles.Select(Path.GetFileName).ToList();
var allTransforms = (await Task.WhenAll(Directory
.GetFiles(transformsDir, "*.ifstf", SearchOption.AllDirectories)
.Select(TransformPlugin.FromFile))).ToList();
Transforms = allTransforms.Where(t => t.IncludeUses.All(u => includeNames.Contains(u))).ToList();
var allEffects = Directory.Exists(postFxDir)
? (await Task.WhenAll(Directory.GetFiles(postFxDir, "*.ifsfx", SearchOption.AllDirectories)
.Select(EffectPlugin.FromFile))).ToList()
: [];
Effects = allEffects.Where(t => t.IncludeUses.All(u => includeNames.Contains(u))).ToList();
}
public Task ReloadPluginsAsync(string libraryDir, string transformsDir) =>
StartAsync(libraryDir, transformsDir);
public void LoadFractal(IFS fractal, int viewportWidth, int viewportHeight)
{
Current = fractal;
Post(r => r.LoadParams(fractal));
}
public void Post(Action<IRenderer> action)
{
PostCount++;
_pending.Enqueue(action);
}
public Task PostAsync(Action<IRenderer> action)
{
Post(action);
return Task.CompletedTask;
}
/// <summary>Runs every queued action, as the render thread's loop would.</summary>
/// <returns>How many actions ran.</returns>
public int Drain()
{
int ran = 0;
// Actions may queue further actions; keep going until the queue is empty.
while (_pending.Count > 0)
{
Action<IRenderer> action = _pending.Dequeue();
try
{
action(Renderer);
}
catch (Exception ex)
{
LastError = ex;
Paused = true;
Failed?.Invoke(ex);
}
ran++;
}
return ran;
}
/// <summary>Discards queued actions without running them.</summary>
public void Clear() => _pending.Clear();
public void SetViewport(int width, int height, bool fillViewport = true) { }
public Task SetWorkgroupCountAsync(int count) => PostAsync(r => r.SetWorkgroupCount(count));
public Task ExportAsync(string path, int width, int height, int targetLevel,
Action<int> onLevel = null, ExportMetadata metadata = null) => Task.CompletedTask;
public Task ExportFramesAsync(string directory, int width, int height, int targetLevel,
Action<int, int> onProgress, Func<bool> isCancelled, bool rawExr = false,
Action<byte[], int, int> frameSink = null) => Task.CompletedTask;
public Task<byte[]> RenderThumbnailAsync(IFS candidate, int width, int height, int level) =>
Task.FromResult(Array.Empty<byte>());
public void Dispose() { }
/// <summary>Raises <see cref="Failed"/> as a dispatch failure would, for the stall test.</summary>
public void SimulateRenderFailure(Exception ex)
{
LastError = ex;
Paused = true;
Failed?.Invoke(ex);
}
private void Unused() => FrameReady?.Invoke();
}