Game/RunRecorder.cs
using System;
using System.Collections.Generic;

namespace BlockParty;

/// <summary>
/// Records the player's per-step input over a single run so the exact run can be replayed later
/// (see <see cref="RunPlayback"/>). The simulation is fully deterministic at a fixed
/// <see cref="Arena.STEP"/> with all randomness routed through the seeded <see cref="Rng"/>, so a
/// run is reproduced from (seed + the input consumed each step) alone.
///
/// <para>The input that each fixed sim step consumed is captured as one byte
/// (<see cref="InputState.PackByte"/>) and stored DELTA-encoded — only the steps where the byte
/// changes — which keeps a typical run to a few hundred bytes. The edge ("just pressed") bits are
/// recorded explicitly rather than re-derived on replay: a sub-frame tap can latch UpJust while Up
/// reads false at the consumed step, so the held bits alone don't reconstruct the edge.</para>
///
/// Static (one run at a time, like <see cref="InputState"/>); <see cref="GameManager"/> drives it
/// from the fixed-step loop and the leaderboard submit reads the encoded result.
/// </summary>
public static class RunRecorder
{
	/// <summary>Hard cap on recorded length: 10 minutes at the fixed tick rate. A run that exceeds it
	/// is flagged <see cref="Truncated"/> and is not offered for replay (rather than storing a partial
	/// run that wouldn't reach the same end state).</summary>
	public const int MAX_STEPS = Arena.TICK_RATE * 600;

	private static readonly List<(int Step, byte Input)> _deltas = new();
	private static byte _last;
	private static int _step;

	/// <summary>True while a fresh run is being recorded.</summary>
	public static bool IsRecording { get; private set; }

	/// <summary>True if the run hit <see cref="MAX_STEPS"/> — it can't be replayed.</summary>
	public static bool Truncated { get; private set; }

	/// <summary>The seed the recorded run was simulated with (reused verbatim on replay).</summary>
	public static int Seed { get; private set; }

	/// <summary>Number of fixed steps recorded so far (the replay length).</summary>
	public static int StepCount => _step;

	/// <summary>True once any recorded step contains gameplay input.</summary>
	public static bool HasInput { get; private set; }

	/// <summary>Begin recording a new run with the given simulation seed.</summary>
	public static void Begin( int seed )
	{
		_deltas.Clear();
		_last = 0;
		_step = 0;
		HasInput = false;
		Seed = seed;
		Truncated = false;
		IsRecording = true;
	}

	/// <summary>Stop recording (e.g. when entering a replay, which must not record).</summary>
	public static void Stop() => IsRecording = false;

	/// <summary>Record the input one fixed sim step consumed. Call once immediately before each
	/// <c>Stage.Tick</c> while a gameplay run is active.</summary>
	public static void RecordStep( byte input )
	{
		if ( !IsRecording )
			return;

		if ( _step >= MAX_STEPS )
		{
			// Past the cap: mark the run non-replayable and stop growing the buffer.
			Truncated = true;
			IsRecording = false;
			return;
		}

		// Delta-encode: only store a transition when the consumed input changes.
		if ( input != _last )
		{
			_deltas.Add( (_step, input) );
			_last = input;
		}
		HasInput |= input != 0;

		_step++;
	}

	/// <summary>Encode the recorded deltas into the compact base64 string stored in the run payload.</summary>
	public static string Encode() => RunInputCodec.Encode( _deltas );
}

/// <summary>
/// Compact, allocation-light codec for a recorded run's per-step input. The transition list
/// (step → new input byte) is written as (varint step-gap, input byte) pairs and base64-encoded for
/// JSON transport; <see cref="Decode"/> expands it back into a dense per-step frame buffer for
/// playback. Kept off System.IO streams so it stays within the engine's sandbox whitelist.
/// </summary>
public static class RunInputCodec
{
	public static string Encode( List<(int Step, byte Input)> deltas )
	{
		if ( deltas == null || deltas.Count == 0 )
			return "";

		var buf = new List<byte>( deltas.Count * 2 );
		int prev = 0;
		foreach ( var (step, input) in deltas )
		{
			WriteVarint( buf, (uint)(step - prev) ); // gaps are smaller than absolute indices
			buf.Add( input );
			prev = step;
		}

		return Convert.ToBase64String( buf.ToArray() );
	}

	/// <summary>True when <paramref name="encoded"/> is a stream <see cref="Encode"/> could actually
	/// have produced for a run of <paramref name="stepCount"/> steps: valid base64, every varint
	/// terminated, every gap followed by its input byte, and transition steps strictly increasing
	/// within the run. The gate the replay entry points share — see RunData.CanReplay — so a corrupt
	/// payload reads as unreplayable up front. This must be STRICTER than <see cref="Decode"/>, which
	/// is deliberately lenient: a structurally truncated stream still "decodes" there, just into the
	/// wrong inputs — a replay that silently desyncs instead of a greyed-out button.</summary>
	public static bool Validate( string encoded, int stepCount )
	{
		if ( stepCount < 0 )
			return false;
		if ( string.IsNullOrEmpty( encoded ) )
			return true; // no transitions — an all-idle stream (CanReplay separately rejects null)

		byte[] raw;
		try
		{
			raw = Convert.FromBase64String( encoded );
		}
		catch
		{
			return false;
		}

		long step = 0;
		bool first = true;
		int i = 0;
		while ( i < raw.Length )
		{
			if ( !TryReadVarint( raw, ref i, out uint gap ) )
				return false; // stream ends mid-varint (or an over-long encoding)
			if ( i >= raw.Length )
				return false; // gap without its input byte — a truncated final pair
			i++;              // the input byte itself: any value is valid

			// First gap is the absolute step (0 is fine); after that Encode's steps strictly
			// increase, so a zero gap is something it could never have written.
			if ( !first && gap == 0 )
				return false;
			step = first ? gap : step + gap;
			first = false;
			if ( step >= stepCount )
				return false; // transition beyond the run's recorded length
		}
		return true;
	}

	/// <summary>Expand the encoded transitions into a dense byte-per-step buffer of length
	/// <paramref name="stepCount"/>. Each frame holds the input that was in effect at that step.</summary>
	public static byte[] Decode( string encoded, int stepCount )
	{
		var frames = new byte[Math.Max( 0, stepCount )];
		if ( string.IsNullOrEmpty( encoded ) || frames.Length == 0 )
			return frames;

		var raw = Convert.FromBase64String( encoded );
		byte cur = 0;
		int writeFrom = 0;
		long acc = 0; // long: a corrupt varint ≥ 2^31 must saturate past the buffer, not wrap negative
		int i = 0;
		while ( i < raw.Length )
		{
			acc += ReadVarint( raw, ref i ); // absolute step where the input changes
			for ( int s = writeFrom; s < acc && s < frames.Length; s++ )
				frames[s] = cur;

			if ( i >= raw.Length )
				break; // malformed tail guard

			cur = raw[i++];
			writeFrom = (int)Math.Min( acc, frames.Length );
		}

		for ( int s = writeFrom; s < frames.Length; s++ )
			frames[s] = cur;

		return frames;
	}

	private static void WriteVarint( List<byte> buf, uint v )
	{
		while ( v >= 0x80 )
		{
			buf.Add( (byte)(v | 0x80) );
			v >>= 7;
		}
		buf.Add( (byte)v );
	}

	// Lenient read for Decode (Validate has already vouched for the stream by the time it runs).
	private static uint ReadVarint( byte[] raw, ref int i )
	{
		TryReadVarint( raw, ref i, out uint v );
		return v;
	}

	/// <summary>Strict varint read: false when the stream ends mid-varint (a continuation bit on the
	/// final byte) or the encoding runs past 5 bytes (more than a uint — Encode never writes it).</summary>
	private static bool TryReadVarint( byte[] raw, ref int i, out uint v )
	{
		v = 0;
		int shift = 0;
		while ( i < raw.Length )
		{
			byte b = raw[i++];
			v |= (uint)(b & 0x7F) << shift;
			if ( (b & 0x80) == 0 )
				return true;
			shift += 7;
			if ( shift > 28 )
				return false; // over-long: a 6th byte can't be part of a canonical uint varint
		}
		return false; // ran out of bytes mid-varint
	}
}

/// <summary>
/// Plays back a recorded run: decodes its input into a per-step frame buffer and hands one frame to
/// the simulation per fixed step. Index-driven (not wall-clock), so playback reproduces the run
/// regardless of the viewer's frame rate or momentary hitches.
/// </summary>
public sealed class RunPlayback
{
	private readonly byte[] _frames;
	private int _i;

	public RunPlayback( RunData data )
	{
		Seed = data.Seed;
		SimVersion = data.SimVersion;
		ExpectedFinalScore = data.FinalScore;
		_frames = RunInputCodec.Decode( data.InputDeltas, data.StepCount );
	}

	/// <summary>The seed the run was recorded with; the sim is reseeded with this before playback.</summary>
	public int Seed { get; }

	/// <summary>The sim version the run was recorded on; the sim's version gates run at this version
	/// during playback (see <see cref="Sim.VERSION"/>).</summary>
	public int SimVersion { get; }

	/// <summary>The integer score the original run submitted; compared against the reproduced score
	/// at the end of playback to detect determinism drift.</summary>
	public int ExpectedFinalScore { get; }

	public int Length => _frames.Length;
	public int Position => _i;
	public bool HasNext => _i < _frames.Length;

	/// <summary>Return the next step's input byte and advance.</summary>
	public byte Next() => _frames[_i++];

	/// <summary>Rewind to the first frame (used to restart a replay from the beginning).</summary>
	public void Reset() => _i = 0;
}