using System.Threading.Channels;
using LogiBoardToolBox.Core.HidPlusPlus;
using LogiBoardToolBox.Core.Onboard;
namespace LogiBoardToolBox.Test;
/// In-memory HID++ firmware. Test runs never enumerate or write physical devices.
internal sealed class FakeMouse : IHidTransport
{
private readonly Channel replies = Channel.CreateUnbounded();
private readonly byte[] description;
private int writingSector;
private int writingSize;
private readonly List writingData = [];
internal Dictionary Sectors { get; }
internal byte Mode { get; private set; } = 2;
internal ushort Active { get; private set; }
internal List ReadOffsets { get; } = [];
internal int WriteStarts { get; private set; }
internal int? FailOnceAtSector { get; set; }
internal byte EndError { get; set; } = 2;
internal bool InjectUnrelated { get; set; }
internal bool SpySupported { get; set; }
internal bool SpyActive { get; set; }
internal bool FailSpyStop { get; set; }
internal byte[] SpyMap { get; private set; } = [1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0];
internal List Requests { get; } = [];
public int InputReportLength => 20;
public int OutputReportLength => 20;
internal FakeMouse(MemorySnapshot snapshot)
{
description = snapshot.Description; Sectors = snapshot.Sectors.ToDictionary(p => p.Key, p => (byte[])p.Value.Clone());
}
public Task WriteAsync(byte[] request, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Requests.Add(request.ToArray());
var reply = new byte[20]; request.AsSpan(0, 4).CopyTo(reply);
var feature = request[2]; var function = request[3] >> 4;
if (InjectUnrelated)
{
var eventReport = (byte[])reply.Clone(); eventReport[3] = 0; replies.Writer.TryWrite(eventReport);
var otherDevice = (byte[])reply.Clone(); otherDevice[1] = 2; replies.Writer.TryWrite(otherDevice);
}
if (feature == 0)
{
if (function == 0)
{
var id = (ushort)((request[4] << 8) | request[5]);
reply[4] = id == 0x8100 ? (byte)2 : id == 0x8110 && SpySupported ? (byte)3 : (byte)0;
}
if (function == 1) { reply[4] = 4; reply[5] = 2; reply[6] = request[6]; }
}
else if (feature == 3)
{
if (function == 0) reply[4] = 11;
if (function == 1) SpyActive = true;
if (function == 2) { if (FailSpyStop) Error(reply, request, 5); else SpyActive = false; }
if (function == 3) SpyMap.CopyTo(reply, 4);
if (function == 4) SpyMap = request[4..15];
}
else if (feature == 2)
{
switch (function)
{
case 0: description.CopyTo(reply, 4); break;
case 1: Mode = request[4]; if (Mode == 2) Active = 0; else if (Active == 0) Active = 1; break;
case 2: reply[4] = Mode; break;
case 3:
Active = (ushort)((request[4] << 8) | request[5]);
if (Active == 0) throw new InvalidOperationException("Cannot activate sector zero.");
break;
case 4: reply[4] = (byte)(Active >> 8); reply[5] = (byte)Active; break;
case 5:
var sector = (request[4] << 8) | request[5]; var offset = (request[6] << 8) | request[7];
ReadOffsets.Add(offset);
if (!Sectors.TryGetValue(sector, out var data) || offset + 16 > data.Length) Error(reply, request, 2);
else data.AsSpan(offset, 16).CopyTo(reply.AsSpan(4));
break;
case 6:
WriteStarts++; writingSector = (request[4] << 8) | request[5]; writingSize = (request[8] << 8) | request[9]; writingData.Clear(); break;
case 7: writingData.AddRange(request.AsSpan(4, 16).ToArray()); break;
case 8:
Sectors[writingSector] = writingData.Take(writingSize).ToArray();
if (FailOnceAtSector == writingSector) { FailOnceAtSector = null; Error(reply, request, EndError); }
break;
default: Error(reply, request, 2); break;
}
}
replies.Writer.TryWrite(reply);
return Task.CompletedTask;
}
private static void Error(byte[] reply, byte[] request, byte error)
{ reply[2] = 0xFF; reply[3] = request[2]; reply[4] = request[3]; reply[5] = error; }
public async Task ReadAsync(CancellationToken cancellationToken) => await replies.Reader.ReadAsync(cancellationToken);
public void Dispose() => replies.Writer.TryComplete();
}