using System;
using System.IO;
using System.Threading.Tasks;
using System.Text.Json.Nodes;
using Godot;
using System.Linq;
using Chessistics.Engine.Model;
using Chessistics.Scripts.Input;
namespace Chessistics.Scripts.Automation;
///
/// Maps command strings → handlers. Each handler returns the "result" payload;
/// the harness wraps it in the envelope.
///
internal class CommandDispatcher
{
private readonly AutomationHarness _harness;
private readonly AutomationFacade _facade;
public CommandDispatcher(AutomationHarness harness, AutomationFacade facade)
{
_harness = harness;
_facade = facade;
}
public async Task Dispatch(string cmd, JsonObject args)
{
return cmd switch
{
"screenshot" => await Screenshot(args),
"get_state" => GetState(),
"select_piece" => SelectPiece(args),
"place" => Place(args),
"click_cell" => ClickCell(args),
"key" => Key(args),
"play" => Play(),
"pause" => Pause(),
"step" => await Step(),
"wait_idle" => await WaitIdle(args),
"set_speed" => SetSpeed(args),
"load_mission" => LoadMission(args),
"back_to_menu" => BackToMenu(),
"quit" => Quit(),
_ => throw new InvalidOperationException($"Unknown command: {cmd}"),
};
}
// --- handlers ---
private async Task Screenshot(JsonObject args)
{
var name = args["name"]?.GetValue() ?? "screenshot";
await _harness.ToSignalAsync(RenderingServer.Singleton, "frame_post_draw");
var image = _harness.GetViewport().GetTexture().GetImage();
var path = IpcFiles.ScreenshotPath(_harness.Root, name);
var err = image.SavePng(path);
if (err != Error.Ok)
throw new InvalidOperationException($"SavePng failed: {err}");
var result = new JsonObject
{
["path"] = Path.GetRelativePath(_harness.Root, path).Replace('\\', '/'),
["abs_path"] = path,
["width"] = image.GetWidth(),
["height"] = image.GetHeight(),
};
return result;
}
private JsonNode? GetState()
{
var sim = _facade.Sim();
if (sim == null) return new JsonObject { ["loaded"] = false };
var snap = sim.GetSnapshot();
var obj = SnapshotSerializer.Serialize(snap);
obj["loaded"] = true;
obj["animating"] = _facade.Animator.IsAnimating;
return obj;
}
private JsonNode? SelectPiece(JsonObject args)
{
var kind = ParsePieceKind(args["kind"]);
_facade.Stock.SimulateSelect(kind);
return new JsonObject
{
["phase"] = _facade.Input.CurrentPhase.ToString(),
["kind"] = kind.ToString(),
};
}
private JsonNode? Place(JsonObject args)
{
var sim = _facade.Sim();
if (sim == null) throw new InvalidOperationException("No simulation loaded.");
var kind = ParsePieceKind(args["kind"]);
var start = ParseCoords(args["start"]);
var end = ParseCoords(args["end"]);
// Snapshot ids before so we can identify the new piece after.
var before = sim.GetSnapshot();
var idsBefore = new HashSet(before.Pieces.Select(p => p.Id));
// Route through the UI signal — this mutates the sim exactly once and
// runs the same path a human click would: HandleEditEvents, stock refresh,
// SetSnapshot on InputMapper.
_facade.Input.EmitSignal(
InputMapper.SignalName.PlacementRequested,
(int)kind, start.Col, start.Row, end.Col, end.Row);
var after = sim.GetSnapshot();
var newPiece = after.Pieces.FirstOrDefault(p => !idsBefore.Contains(p.Id));
var placed = newPiece != null;
return new JsonObject
{
["placed"] = placed,
["pieceId"] = newPiece?.Id,
["reason"] = placed ? null : "Placement rejected (check Godot console log for PlacementRejectedEvent).",
};
}
private JsonNode? ClickCell(JsonObject args)
{
var col = args["col"]!.GetValue();
var row = args["row"]!.GetValue();
var button = args["button"]?.GetValue()?.ToLowerInvariant() ?? "left";
var mouseBtn = button switch
{
"right" => MouseButton.Right,
_ => MouseButton.Left,
};
_facade.Input.SimulateClick(new Coords(col, row), mouseBtn);
return new JsonObject
{
["phase"] = _facade.Input.CurrentPhase.ToString(),
};
}
private JsonNode? Key(JsonObject args)
{
var key = args["key"]!.GetValue();
switch (key.ToLowerInvariant())
{
case "space":
_facade.TogglePlayPause();
break;
case "escape":
case "esc":
_facade.Input.Cancel();
break;
default:
throw new InvalidOperationException($"Unsupported key: {key}");
}
return new JsonObject();
}
private JsonNode? Play()
{
_facade.Play();
return PhaseInfo();
}
private JsonNode? Pause()
{
_facade.Pause();
return PhaseInfo();
}
private async Task Step()
{
_facade.Step();
// Dispatcher auto-waits for idle before writing the result.
await WaitIdleInternal(timeoutMs: 10000);
return PhaseInfo();
}
private async Task WaitIdle(JsonObject args)
{
var timeout = args["timeoutMs"]?.GetValue() ?? 10000;
var reached = await WaitIdleInternal(timeout);
var info = PhaseInfo()!.AsObject();
info["idle"] = reached;
return info;
}
internal async Task WaitIdleInternal(int timeoutMs)
{
var elapsed = 0.0;
while (_facade.Animator.IsAnimating)
{
await _harness.ToSignalAsync(_harness.GetTree(), "process_frame");
elapsed += 1.0 / 60.0 * 1000.0;
if (elapsed > timeoutMs) return false;
}
return true;
}
private JsonNode? SetSpeed(JsonObject args)
{
var interval = (float)args["interval"]!.GetValue();
_facade.SetSpeed(interval);
return new JsonObject { ["interval"] = interval };
}
private JsonNode? LoadMission(JsonObject args)
{
var campaign = args["campaign"]?.GetValue() ?? "campaign_01";
var index = args["missionIndex"]?.GetValue() ?? 0;
_facade.LoadMission(campaign, index);
var sim = _facade.Sim();
if (sim == null) return new JsonObject { ["loaded"] = false };
return SnapshotSerializer.Serialize(sim.GetSnapshot());
}
private JsonNode? BackToMenu()
{
_facade.BackToMenu();
return new JsonObject();
}
private JsonNode? Quit()
{
// Defer so we can write the result first.
_harness.CallDeferred(nameof(AutomationHarness.RequestQuit));
return new JsonObject { ["quitting"] = true };
}
// --- helpers ---
private JsonNode PhaseInfo()
{
var sim = _facade.Sim();
var obj = new JsonObject();
if (sim != null)
{
var snap = sim.GetSnapshot();
obj["phase"] = snap.Phase.ToString();
obj["turn"] = snap.TurnNumber;
obj["missionIndex"] = snap.Campaign?.CurrentMissionIndex;
}
obj["animating"] = _facade.Animator.IsAnimating;
return obj;
}
private static PieceKind ParsePieceKind(JsonNode? node)
{
if (node == null) throw new InvalidOperationException("Missing 'kind'.");
var val = node.AsValue();
if (val.TryGetValue(out var i)) return (PieceKind)i;
var s = val.GetValue();
if (Enum.TryParse(s, ignoreCase: true, out var k)) return k;
throw new InvalidOperationException($"Unknown piece kind: {s}");
}
private static Coords ParseCoords(JsonNode? node)
{
if (node is JsonArray arr && arr.Count == 2)
return new Coords(arr[0]!.GetValue(), arr[1]!.GetValue());
if (node is JsonObject obj && obj.ContainsKey("col") && obj.ContainsKey("row"))
return new Coords(obj["col"]!.GetValue(), obj["row"]!.GetValue());
throw new InvalidOperationException("Coords must be [col,row] or {col,row}.");
}
}