using System.Text.Json.Serialization;
namespace Flyshot.Core.Domain;
///
/// Represents an uploaded flyshot program built from teach points and shot metadata.
///
public sealed class FlyshotProgram
{
///
/// Initializes a validated flyshot program.
///
public FlyshotProgram(
string name,
IEnumerable waypoints,
IEnumerable shotFlags,
IEnumerable offsetValues,
IEnumerable addressGroups)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Program name is required.", nameof(name));
}
ArgumentNullException.ThrowIfNull(waypoints);
ArgumentNullException.ThrowIfNull(shotFlags);
ArgumentNullException.ThrowIfNull(offsetValues);
ArgumentNullException.ThrowIfNull(addressGroups);
// Freeze the uploaded data so later orchestration cannot silently drift from the request payload.
var copiedWaypoints = waypoints.ToArray();
var copiedShotFlags = shotFlags.ToArray();
var copiedOffsetValues = offsetValues.ToArray();
var copiedAddressGroups = addressGroups.ToArray();
if (copiedWaypoints.Length == 0)
{
throw new ArgumentException("At least one waypoint is required.", nameof(waypoints));
}
if (copiedShotFlags.Length != copiedWaypoints.Length)
{
throw new ArgumentException("Shot flag count must match waypoint count.", nameof(shotFlags));
}
if (copiedOffsetValues.Length != copiedWaypoints.Length)
{
throw new ArgumentException("Offset value count must match waypoint count.", nameof(offsetValues));
}
if (copiedAddressGroups.Length != copiedWaypoints.Length)
{
throw new ArgumentException("Address group count must match waypoint count.", nameof(addressGroups));
}
var jointDimension = copiedWaypoints[0].Positions.Count;
if (copiedWaypoints.Any(waypoint => waypoint.Positions.Count != jointDimension))
{
throw new ArgumentException("All waypoints must share the same joint dimension.", nameof(waypoints));
}
Name = name;
Waypoints = copiedWaypoints;
ShotFlags = copiedShotFlags;
OffsetValues = copiedOffsetValues;
AddressGroups = copiedAddressGroups;
JointDimension = jointDimension;
ShotWaypointCount = copiedShotFlags.Count(flag => flag);
}
///
/// Gets the program name used by cache and gateway lookups.
///
[JsonPropertyName("name")]
public string Name { get; }
///
/// Gets the immutable teach waypoint list.
///
[JsonPropertyName("waypoints")]
public IReadOnlyList Waypoints { get; }
///
/// Gets the per-waypoint shot trigger flags.
///
[JsonPropertyName("shotFlags")]
public IReadOnlyList ShotFlags { get; }
///
/// Gets the per-waypoint trigger offset values in servo cycles.
///
[JsonPropertyName("offsetValues")]
public IReadOnlyList OffsetValues { get; }
///
/// Gets the per-waypoint IO address groups.
///
[JsonPropertyName("addressGroups")]
public IReadOnlyList AddressGroups { get; }
///
/// Gets the joint dimension shared by all waypoints.
///
[JsonPropertyName("jointDimension")]
public int JointDimension { get; }
///
/// Gets the number of waypoints that request a shot trigger.
///
[JsonPropertyName("shotWaypointCount")]
public int ShotWaypointCount { get; }
}
///
/// Represents a single teach waypoint in joint space.
///
public sealed class JointWaypoint
{
///
/// Initializes a validated joint waypoint.
///
public JointWaypoint(IEnumerable positions)
{
ArgumentNullException.ThrowIfNull(positions);
// Copy the input once so the waypoint remains immutable after upload.
var copiedPositions = positions.ToArray();
if (copiedPositions.Length == 0)
{
throw new ArgumentException("Joint waypoint must contain at least one joint value.", nameof(positions));
}
Positions = copiedPositions;
}
///
/// Gets the immutable joint value vector.
///
[JsonPropertyName("positions")]
public IReadOnlyList Positions { get; }
}
///
/// Represents the list of IO addresses that should fire at one logical trigger point.
///
public sealed class IoAddressGroup
{
///
/// Initializes a validated IO address group.
///
public IoAddressGroup(IEnumerable addresses)
{
ArgumentNullException.ThrowIfNull(addresses);
// Preserve address order because some upper-layer tooling expects exported order stability.
var copiedAddresses = addresses.ToArray();
if (copiedAddresses.Length > 8)
{
throw new ArgumentException("A single trigger point can contain at most eight IO addresses.", nameof(addresses));
}
if (copiedAddresses.Any(address => address < 0))
{
throw new ArgumentException("IO addresses must be zero or positive.", nameof(addresses));
}
Addresses = copiedAddresses;
}
///
/// Gets the immutable ordered IO address list.
///
[JsonPropertyName("addresses")]
public IReadOnlyList Addresses { get; }
}