using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Godosa.Core.Input;
public enum ScriptAction { Down, Up, Click, Type }
/// Physical key name (Down/Up), button label (Click) or text (Type).
public readonly record struct ScriptEvent(double Time, ScriptAction Action, string Arg);
/// Timed input for smoke runs (the host replays it as real key events and button
/// clicks). One per line: TIME ACTION ARGS, TIME absolute seconds or +seconds
/// after the previous line; actions down K, up K, press K,
/// hold K SECONDS, click LABEL, type TEXT (into the focused text box); # comments.
public sealed class InputScript
{
public const double PressSeconds = 0.1;
private int _next;
public IReadOnlyList Events { get; }
public bool Finished => _next >= Events.Count;
private InputScript(List events) => Events = events;
public static InputScript Parse(string text)
{
var events = new List();
double? previous = null;
var lines = text.Split('\n');
for (int n = 0; n < lines.Length; n++)
{
string line = lines[n].Trim();
if (line.Length == 0 || line.StartsWith('#')) continue;
string[] parts = line.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries);
FormatException Bad(string why) => new($"input script line {n + 1}: {why}: '{line}'");
if (parts.Length < 3) throw Bad("expected TIME ACTION ARG");
bool relative = parts[0].StartsWith('+');
if (!double.TryParse(parts[0].TrimStart('+'), NumberStyles.Float, CultureInfo.InvariantCulture, out double t))
throw Bad("bad time");
if (relative)
{
if (previous is null) throw Bad("relative time needs a line before it");
t += previous.Value;
}
previous = t;
string arg = parts[2];
switch (parts[1])
{
case "down": events.Add(new(t, ScriptAction.Down, arg)); break;
case "up": events.Add(new(t, ScriptAction.Up, arg)); break;
case "press":
events.Add(new(t, ScriptAction.Down, arg));
events.Add(new(t + PressSeconds, ScriptAction.Up, arg));
break;
case "hold":
string[] hold = arg.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (hold.Length != 2 || !double.TryParse(hold[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double seconds))
throw Bad("hold needs KEY SECONDS");
events.Add(new(t, ScriptAction.Down, hold[0]));
events.Add(new(t + seconds, ScriptAction.Up, hold[0]));
break;
case "click": events.Add(new(t, ScriptAction.Click, arg)); break;
case "type": events.Add(new(t, ScriptAction.Type, arg)); break;
default: throw Bad($"unknown action '{parts[1]}'");
}
}
return new InputScript(events.OrderBy(e => e.Time).ToList()); // stable: same-time lines keep order
}
/// Appends every not-yet-returned event with Time ≤ .
public void Due(double now, List into)
{
while (_next < Events.Count && Events[_next].Time <= now)
into.Add(Events[_next++]);
}
}