Networking/LinkCablePackets.cs
using System.IO;
using System.IO.Compression;

namespace sGBA;

public enum LinkCablePacketType : byte
{
	Input = 1,
	Av = 2,
	SaveUpload = 3,
	State = 4,
}

internal static class LinkCableCompression
{
	public static byte[] Deflate( byte[] data )
	{
		using var output = new MemoryStream();
		using ( var deflate = new DeflateStream( output, CompressionLevel.Fastest, leaveOpen: true ) )
			deflate.Write( data, 0, data.Length );
		return output.ToArray();
	}

	public static byte[] Inflate( byte[] compressed, int rawLength )
	{
		var result = new byte[rawLength];
		using var input = new MemoryStream( compressed, false );
		using var deflate = new DeflateStream( input, CompressionMode.Decompress );
		int read = 0;
		while ( read < rawLength )
		{
			int n = deflate.Read( result, read, rawLength - read );
			if ( n <= 0 )
				break;
			read += n;
		}
		return result;
	}
}

public sealed class LinkCableStatePacket
{
	public int PlayerId;
	public byte[] State;

	public byte[] Serialize()
	{
		using var stream = new MemoryStream();
		using var writer = new BinaryWriter( stream );
		writer.Write( (byte)LinkCablePacketType.State );
		writer.Write( PlayerId );

		int rawLen = State?.Length ?? 0;
		writer.Write( rawLen );
		if ( rawLen > 0 )
		{
			byte[] compressed = LinkCableCompression.Deflate( State );
			writer.Write( compressed.Length );
			writer.Write( compressed );
		}

		writer.Flush();
		return stream.ToArray();
	}

	public static bool TryParse( byte[] payload, int length, out LinkCableStatePacket packet )
	{
		packet = null;
		if ( payload == null || length < 1 || payload[0] != (byte)LinkCablePacketType.State )
			return false;

		using var stream = new MemoryStream( payload, 0, length, false );
		using var reader = new BinaryReader( stream );
		reader.ReadByte();

		var result = new LinkCableStatePacket { PlayerId = reader.ReadInt32() };

		int rawLen = reader.ReadInt32();
		if ( rawLen < 0 )
			return false;
		if ( rawLen > 0 )
		{
			int compressedLen = reader.ReadInt32();
			if ( compressedLen < 0 )
				return false;
			byte[] compressed = reader.ReadBytes( compressedLen );
			result.State = LinkCableCompression.Inflate( compressed, rawLen );
		}

		packet = result;
		return true;
	}
}

public sealed class LinkCableSaveUploadPacket
{
	public int PlayerId;
	public byte[] SaveData;

	public byte[] Serialize()
	{
		using var stream = new MemoryStream();
		using var writer = new BinaryWriter( stream );
		writer.Write( (byte)LinkCablePacketType.SaveUpload );
		writer.Write( PlayerId );

		int saveLen = SaveData?.Length ?? 0;
		writer.Write( saveLen );
		if ( saveLen > 0 )
			writer.Write( SaveData );

		writer.Flush();
		return stream.ToArray();
	}

	public static bool TryParse( byte[] payload, int length, out LinkCableSaveUploadPacket packet )
	{
		packet = null;
		if ( payload == null || length < 1 )
			return false;
		if ( payload[0] != (byte)LinkCablePacketType.SaveUpload )
			return false;

		using var stream = new MemoryStream( payload, 0, length, false );
		using var reader = new BinaryReader( stream );
		reader.ReadByte();

		var result = new LinkCableSaveUploadPacket
		{
			PlayerId = reader.ReadInt32(),
		};

		int saveLen = reader.ReadInt32();
		if ( saveLen < 0 )
			return false;
		if ( saveLen > 0 )
			result.SaveData = reader.ReadBytes( saveLen );

		packet = result;
		return true;
	}
}

public readonly struct LinkCableInputPacket
{
	public readonly long Frame;
	public readonly ushort Keys;

	public LinkCableInputPacket( long frame, ushort keys )
	{
		Frame = frame;
		Keys = keys;
	}

	public byte[] Serialize()
	{
		var buffer = new byte[12];
		buffer[0] = (byte)LinkCablePacketType.Input;
		WriteInt64( buffer, 1, Frame );
		buffer[9] = (byte)(Keys & 0xFF);
		buffer[10] = (byte)(Keys >> 8);
		buffer[11] = 0;
		return buffer;
	}

	public static bool TryParse( byte[] payload, int length, out LinkCableInputPacket packet )
	{
		packet = default;
		if ( payload == null || length < 12 )
			return false;
		if ( payload[0] != (byte)LinkCablePacketType.Input )
			return false;
		long frame = ReadInt64( payload, 1 );
		ushort keys = (ushort)(payload[9] | (payload[10] << 8));
		packet = new LinkCableInputPacket( frame, keys );
		return true;
	}

	internal static void WriteInt64( byte[] buffer, int offset, long value )
	{
		for ( int i = 0; i < 8; i++ )
			buffer[offset + i] = (byte)(value >> (i * 8));
	}

	internal static long ReadInt64( byte[] buffer, int offset )
	{
		long value = 0;
		for ( int i = 0; i < 8; i++ )
			value |= (long)buffer[offset + i] << (i * 8);
		return value;
	}
}

public sealed class LinkCableAvPacket
{
	public int PlayerId;
	public long Frame;
	public short[] Audio;
	public int AudioSamples;
	public byte[] SaveData;
	public byte[] Video;

	public byte[] Serialize()
	{
		using var stream = new MemoryStream();
		using var writer = new BinaryWriter( stream );
		writer.Write( (byte)LinkCablePacketType.Av );
		writer.Write( PlayerId );
		writer.Write( Frame );

		int samples = AudioSamples < 0 ? 0 : AudioSamples;
		int shorts = samples * 2;
		if ( Audio == null || shorts > Audio.Length )
			shorts = Audio == null ? 0 : Audio.Length;
		writer.Write( shorts );
		for ( int i = 0; i < shorts; i++ )
			writer.Write( Audio[i] );

		int saveLen = SaveData?.Length ?? 0;
		writer.Write( saveLen );
		if ( saveLen > 0 )
			writer.Write( SaveData );

		int videoLen = Video?.Length ?? 0;
		writer.Write( videoLen );
		if ( videoLen > 0 )
		{
			byte[] compressed = LinkCableCompression.Deflate( Video );
			writer.Write( compressed.Length );
			writer.Write( compressed );
		}

		writer.Flush();
		return stream.ToArray();
	}

	public static bool TryParse( byte[] payload, int length, out LinkCableAvPacket packet )
	{
		packet = null;
		if ( payload == null || length < 1 )
			return false;
		if ( payload[0] != (byte)LinkCablePacketType.Av )
			return false;

		using var stream = new MemoryStream( payload, 0, length, false );
		using var reader = new BinaryReader( stream );
		reader.ReadByte();

		var result = new LinkCableAvPacket
		{
			PlayerId = reader.ReadInt32(),
			Frame = reader.ReadInt64(),
		};

		int shorts = reader.ReadInt32();
		if ( shorts < 0 )
			return false;
		result.AudioSamples = shorts / 2;
		result.Audio = new short[shorts];
		for ( int i = 0; i < shorts; i++ )
			result.Audio[i] = reader.ReadInt16();

		int saveLen = reader.ReadInt32();
		if ( saveLen > 0 )
			result.SaveData = reader.ReadBytes( saveLen );

		int videoLen = reader.ReadInt32();
		if ( videoLen > 0 )
		{
			int compressedLen = reader.ReadInt32();
			if ( compressedLen < 0 )
				return false;
			byte[] compressed = reader.ReadBytes( compressedLen );
			result.Video = LinkCableCompression.Inflate( compressed, videoLen );
		}

		packet = result;
		return true;
	}
}