Emulator/GbaSerialize.cs
using System.IO;

namespace sGBA;

public static class GbaSerialize
{
	private const uint Magic = 0x53474241;
	public const int SlotCount = 4;
	public const int ScreenshotSize = GbaConstants.ScreenWidth * GbaConstants.ScreenHeight * 4;
	private const int HeaderSize = sizeof( uint ) + sizeof( long ); // magic + timestamp

	public static byte[] Save( Gba gba, byte[] screenshot )
	{
		using var ms = new MemoryStream();
		using var w = new BinaryWriter( ms );

		w.Write( Magic );
		w.Write( DateTime.UtcNow.Ticks );

		if ( screenshot != null && screenshot.Length == ScreenshotSize )
			w.Write( screenshot );
		else
			w.Write( new byte[ScreenshotSize] );

		WriteCpu( w, gba.Cpu );
		WriteMemory( w, gba.Memory );
		WriteVideo( w, gba.Video );
		WriteAudio( w, gba.Audio );
		WriteIo( w, gba.Io );
		WriteDma( w, gba.Dma );
		WriteTimers( w, gba.Timers );
		WriteSavedata( w, gba.Savedata );
		WriteHardware( w, gba.Hardware );
		WriteBios( w, gba.Bios );
		WriteSystem( w, gba );

		gba.Cpu.SerializePipeline( w );

		return ms.ToArray();
	}

	public static void Load( Gba gba, byte[] data )
	{
		using var ms = new MemoryStream( data );
		using var r = new BinaryReader( ms );

		ValidateHeader( r );
		r.BaseStream.Position = HeaderSize + ScreenshotSize;

		ReadCpu( r, gba.Cpu );
		ReadMemory( r, gba.Memory );
		ReadVideo( r, gba.Video );
		ReadAudio( r, gba.Audio );
		ReadIo( r, gba.Io );
		ReadDma( r, gba.Dma );
		ReadTimers( r, gba.Timers );
		ReadSavedata( r, gba.Savedata );
		ReadHardware( r, gba.Hardware );
		ReadBios( r, gba.Bios );
		ReadSystem( r, gba );

		gba.Cpu.DeserializePipeline( r );
		gba.Memory.InstallHleBios();

		gba.Audio.SamplesWritten = 0;

		gba.Video._firstAffine = -1;
		gba.Video._lastDrawnY = -1;
		for ( int i = 0; i < 4; i++ )
		{
			bool enabled = (gba.Video.DispCnt & (0x100 << i)) != 0;
			gba.Video._enabledAtY[i] = enabled ? 0 : int.MaxValue;
			gba.Video._wasFullyEnabled[i] = enabled;
		}
		gba.Video._oldCharBase[0] = (uint)((gba.Video.BgCnt[2] >> 2) & 3) * 0x4000u;
		gba.Video._oldCharBase[1] = (uint)((gba.Video.BgCnt[3] >> 2) & 3) * 0x4000u;
		gba.Video._oldCharBaseFirstY[0] = 0;
		gba.Video._oldCharBaseFirstY[1] = 0;
	}

	public static byte[] ReadScreenshot( byte[] data )
	{
		if ( data == null || data.Length < HeaderSize + ScreenshotSize )
			return null;

		using var ms = new MemoryStream( data );
		using var r = new BinaryReader( ms );

		if ( !TryValidateHeader( r ) )
			return null;

		r.BaseStream.Position = HeaderSize;
		return r.ReadBytes( ScreenshotSize );
	}

	public static DateTime? ReadTimestamp( byte[] data )
	{
		if ( data == null || data.Length < HeaderSize )
			return null;

		using var ms = new MemoryStream( data );
		using var r = new BinaryReader( ms );

		if ( !TryValidateHeader( r ) )
			return null;

		return new DateTime( r.ReadInt64(), DateTimeKind.Utc ).ToLocalTime();
	}

	private static void ValidateHeader( BinaryReader r )
	{
		if ( !TryValidateHeader( r ) )
			throw new Exception( "Invalid suspend point." );
	}

	private static bool TryValidateHeader( BinaryReader r )
	{
		return r.ReadUInt32() == Magic;
	}

	private static void WriteCpu( BinaryWriter w, ArmCore cpu )
	{
		for ( int i = 0; i < 16; i++ ) w.Write( cpu.Gprs[i] );
		w.Write( cpu.FlagN ); w.Write( cpu.FlagZ ); w.Write( cpu.FlagC ); w.Write( cpu.FlagV );
		w.Write( cpu.IrqDisable ); w.Write( cpu.FiqDisable );
		w.Write( cpu.ThumbMode );
		w.Write( (int)cpu.PrivilegeMode );

		for ( int i = 0; i < 6; i++ )
		{
			w.Write( cpu.BankedSPSRs[i] );
		}

		WriteBankedRegs( w, cpu );

		w.Write( cpu.Cycles );
		w.Write( cpu.Halted ); w.Write( cpu.IrqPending );
		w.Write( cpu.OpenBusPrefetch );
	}

	private static void WriteBankedRegs( BinaryWriter w, ArmCore cpu )
	{
		var origMode = cpu.PrivilegeMode;
		var origR13 = cpu.Gprs[13];
		var origR14 = cpu.Gprs[14];

		PrivilegeMode[] modes = [PrivilegeMode.User, PrivilegeMode.FIQ, PrivilegeMode.IRQ, PrivilegeMode.Supervisor, PrivilegeMode.Abort, PrivilegeMode.Undefined];

		foreach ( var mode in modes )
		{
			cpu.SetPrivilegeMode( mode );
			w.Write( cpu.Gprs[13] );
			w.Write( cpu.Gprs[14] );
		}

		cpu.SetPrivilegeMode( PrivilegeMode.FIQ );
		for ( int i = 8; i <= 12; i++ ) w.Write( cpu.Gprs[i] );

		cpu.SetPrivilegeMode( PrivilegeMode.System );
		for ( int i = 8; i <= 12; i++ ) w.Write( cpu.Gprs[i] );

		cpu.SetPrivilegeMode( origMode );
		cpu.Gprs[13] = origR13;
		cpu.Gprs[14] = origR14;
	}

	private static void ReadCpu( BinaryReader r, ArmCore cpu )
	{
		for ( int i = 0; i < 16; i++ ) cpu.Gprs[i] = r.ReadUInt32();
		cpu.FlagN = r.ReadBoolean(); cpu.FlagZ = r.ReadBoolean();
		cpu.FlagC = r.ReadBoolean(); cpu.FlagV = r.ReadBoolean();
		cpu.IrqDisable = r.ReadBoolean(); cpu.FiqDisable = r.ReadBoolean();
		cpu.ThumbMode = r.ReadBoolean();
		cpu.PrivilegeMode = (PrivilegeMode)r.ReadInt32();

		for ( int i = 0; i < 6; i++ )
			cpu.BankedSPSRs[i] = r.ReadUInt32();

		ReadBankedRegs( r, cpu );

		cpu.Cycles = r.ReadInt64();
		cpu.Halted = r.ReadBoolean(); cpu.IrqPending = r.ReadBoolean();
		cpu.OpenBusPrefetch = r.ReadUInt32();
	}

	private static void ReadBankedRegs( BinaryReader r, ArmCore cpu )
	{
		PrivilegeMode[] modes = [PrivilegeMode.User, PrivilegeMode.FIQ, PrivilegeMode.IRQ, PrivilegeMode.Supervisor, PrivilegeMode.Abort, PrivilegeMode.Undefined];
		var targetMode = cpu.PrivilegeMode;
		var savedR13 = cpu.Gprs[13];
		var savedR14 = cpu.Gprs[14];

		foreach ( var mode in modes )
		{
			cpu.SetPrivilegeMode( mode );
			cpu.Gprs[13] = r.ReadUInt32();
			cpu.Gprs[14] = r.ReadUInt32();
		}

		cpu.SetPrivilegeMode( PrivilegeMode.FIQ );
		for ( int i = 8; i <= 12; i++ ) cpu.Gprs[i] = r.ReadUInt32();

		cpu.SetPrivilegeMode( PrivilegeMode.System );
		for ( int i = 8; i <= 12; i++ ) cpu.Gprs[i] = r.ReadUInt32();

		cpu.SetPrivilegeMode( targetMode );
		cpu.Gprs[13] = savedR13;
		cpu.Gprs[14] = savedR14;
	}

	private static void WriteMemory( BinaryWriter w, GbaMemory memory )
	{
		w.Write( memory.Wram );
		w.Write( memory.Iwram );
		w.Write( memory.PaletteRam );
		w.Write( memory.Vram );
		w.Write( memory.Oam );

		for ( int i = 0; i < memory.Io.Length; i++ )
			w.Write( memory.Io[i] );

		w.Write( memory.BiosPrefetch );
		w.Write( memory.Prefetch );
		w.Write( memory.LastPrefetchedPc );

		for ( int i = 0; i < 16; i++ ) w.Write( memory.WaitstatesNonseq16[i] );
		for ( int i = 0; i < 16; i++ ) w.Write( memory.WaitstatesNonseq32[i] );
		for ( int i = 0; i < 16; i++ ) w.Write( memory.WaitstatesSeq16[i] );
		for ( int i = 0; i < 16; i++ ) w.Write( memory.WaitstatesSeq32[i] );

		w.Write( memory.Debug );
		w.Write( memory.DebugString );
		w.Write( memory.DebugFlags );
	}

	private static void ReadMemory( BinaryReader r, GbaMemory memory )
	{
		r.Read( memory.Wram );
		r.Read( memory.Iwram );
		r.Read( memory.PaletteRam );
		r.Read( memory.Vram );
		r.Read( memory.Oam );

		for ( int i = 0; i < memory.Io.Length; i++ )
			memory.Io[i] = r.ReadUInt16();

		memory.BiosPrefetch = r.ReadUInt32();
		memory.Prefetch = r.ReadBoolean();
		memory.LastPrefetchedPc = r.ReadUInt32();

		for ( int i = 0; i < 16; i++ ) memory.WaitstatesNonseq16[i] = r.ReadInt32();
		for ( int i = 0; i < 16; i++ ) memory.WaitstatesNonseq32[i] = r.ReadInt32();
		for ( int i = 0; i < 16; i++ ) memory.WaitstatesSeq16[i] = r.ReadInt32();
		for ( int i = 0; i < 16; i++ ) memory.WaitstatesSeq32[i] = r.ReadInt32();

		memory.Debug = r.ReadBoolean();
		r.Read( memory.DebugString );
		memory.DebugFlags = r.ReadUInt16();
		memory.ClearAgbPrint();
	}

	private static void WriteVideo( BinaryWriter w, GbaVideo video )
	{
		w.Write( video.VCount ); w.Write( video.Dot );
		w.Write( video.DispCnt ); w.Write( video.DispStat );

		for ( int i = 0; i < 4; i++ ) w.Write( video.BgCnt[i] );
		for ( int i = 0; i < 4; i++ ) w.Write( video.BgHOfs[i] );
		for ( int i = 0; i < 4; i++ ) w.Write( video.BgVOfs[i] );
		for ( int i = 0; i < 2; i++ ) w.Write( video.BgPA[i] );
		for ( int i = 0; i < 2; i++ ) w.Write( video.BgPB[i] );
		for ( int i = 0; i < 2; i++ ) w.Write( video.BgPC[i] );
		for ( int i = 0; i < 2; i++ ) w.Write( video.BgPD[i] );
		for ( int i = 0; i < 2; i++ ) w.Write( video.BgX[i] );
		for ( int i = 0; i < 2; i++ ) w.Write( video.BgY[i] );
		for ( int i = 0; i < 2; i++ ) w.Write( video.BgRefX[i] );
		for ( int i = 0; i < 2; i++ ) w.Write( video.BgRefY[i] );

		w.Write( video.BldCnt ); w.Write( video.BldAlpha ); w.Write( video.BldY );
		w.Write( video.Win0H ); w.Write( video.Win0V );
		w.Write( video.Win1H ); w.Write( video.Win1V );
		w.Write( video.WinIn ); w.Write( video.WinOut );
		w.Write( video.Mosaic );
	}

	private static void ReadVideo( BinaryReader r, GbaVideo video )
	{
		video.VCount = r.ReadInt32(); video.Dot = r.ReadInt32();
		video.DispCnt = r.ReadUInt16(); video.DispStat = r.ReadUInt16();

		for ( int i = 0; i < 4; i++ ) video.BgCnt[i] = r.ReadUInt16();
		for ( int i = 0; i < 4; i++ ) video.BgHOfs[i] = r.ReadInt16();
		for ( int i = 0; i < 4; i++ ) video.BgVOfs[i] = r.ReadInt16();
		for ( int i = 0; i < 2; i++ ) video.BgPA[i] = r.ReadInt16();
		for ( int i = 0; i < 2; i++ ) video.BgPB[i] = r.ReadInt16();
		for ( int i = 0; i < 2; i++ ) video.BgPC[i] = r.ReadInt16();
		for ( int i = 0; i < 2; i++ ) video.BgPD[i] = r.ReadInt16();
		for ( int i = 0; i < 2; i++ ) video.BgX[i] = r.ReadInt32();
		for ( int i = 0; i < 2; i++ ) video.BgY[i] = r.ReadInt32();
		for ( int i = 0; i < 2; i++ ) video.BgRefX[i] = r.ReadInt32();
		for ( int i = 0; i < 2; i++ ) video.BgRefY[i] = r.ReadInt32();

		video.BldCnt = r.ReadUInt16(); video.BldAlpha = r.ReadUInt16(); video.BldY = r.ReadUInt16();
		video.Win0H = r.ReadUInt16(); video.Win0V = r.ReadUInt16();
		video.Win1H = r.ReadUInt16(); video.Win1V = r.ReadUInt16();
		video.WinIn = r.ReadUInt16(); video.WinOut = r.ReadUInt16();
		video.Mosaic = r.ReadUInt16();

		video.ScheduleEventForLoad();
	}

	private static void WriteAudio( BinaryWriter w, GbaAudio audio )
	{
		w.Write( audio.Enable );
		w.Write( audio.SoundBias );

		w.Write( audio.Sound1CntL ); w.Write( audio.Sound1CntH ); w.Write( audio.Sound1CntX );
		w.Write( audio.Sound2CntL ); w.Write( audio.Sound2CntH );
		w.Write( audio.Sound3CntL ); w.Write( audio.Sound3CntH ); w.Write( audio.Sound3CntX );
		w.Write( audio.Sound4CntL ); w.Write( audio.Sound4CntH );
		w.Write( audio.SoundCntL ); w.Write( audio.SoundCntH ); w.Write( audio.SoundCntX );

		w.Write( audio.WaveRam );

		audio.Serialize( w );
	}

	private static void ReadAudio( BinaryReader r, GbaAudio audio )
	{
		audio.Enable = r.ReadBoolean();
		audio.SoundBias = r.ReadUInt16();

		audio.Sound1CntL = r.ReadUInt16(); audio.Sound1CntH = r.ReadUInt16(); audio.Sound1CntX = r.ReadUInt16();
		audio.Sound2CntL = r.ReadUInt16(); audio.Sound2CntH = r.ReadUInt16();
		audio.Sound3CntL = r.ReadUInt16(); audio.Sound3CntH = r.ReadUInt16(); audio.Sound3CntX = r.ReadUInt16();
		audio.Sound4CntL = r.ReadUInt16(); audio.Sound4CntH = r.ReadUInt16();
		audio.SoundCntL = r.ReadUInt16(); audio.SoundCntH = r.ReadUInt16(); audio.SoundCntX = r.ReadUInt16();

		r.Read( audio.WaveRam );

		audio.Deserialize( r );
		audio.ScheduleAudioEvents();
	}

	private static void WriteIo( BinaryWriter w, GbaIo io )
	{
		w.Write( io.IE ); w.Write( io.IF ); w.Write( io.IME );
		w.Write( io.WaitCnt );
		w.Write( io.Read16( 0x130 ) ); w.Write( io.KeyCnt );
		w.Write( io.Rcnt );
		w.Write( io.PostFlg );
		w.Write( io.Gba.HaltPending );
		io.Serialize( w );
	}

	private static void ReadIo( BinaryReader r, GbaIo io )
	{
		io.IE = r.ReadUInt16(); io.IF = r.ReadUInt16(); io.IME = r.ReadUInt16();
		io.WaitCnt = r.ReadUInt16();
		ushort keyInput = r.ReadUInt16(); io.KeyCnt = r.ReadUInt16();
		io.Gba.KeysActive = (ushort)(0x03FF ^ (keyInput & 0x03FF));
		io.Gba.Memory.Io[0x130 >> 1] = keyInput;
		io.Rcnt = r.ReadUInt16();
		io.PostFlg = r.ReadByte();
		io.Gba.HaltPending = r.ReadBoolean();
		io.Deserialize( r );
	}

	private static void WriteDma( BinaryWriter w, GbaDmaController dma )
	{
		w.Write( dma.ActiveDma );
		w.Write( dma.CpuBlocked );
		w.Write( dma.PerformingDma );

		for ( int i = 0; i < 4; i++ )
		{
			var c = dma.Channels[i];
			w.Write( c.SrcLow ); w.Write( c.SrcHigh );
			w.Write( c.DstLow ); w.Write( c.DstHigh );
			w.Write( c.CntLo ); w.Write( c.Reg );
			w.Write( c.NextSource ); w.Write( c.NextDest );
			w.Write( c.NextCount ); w.Write( c.Count ); w.Write( c.Latch );
			w.Write( c.When ); w.Write( c.Cycles );
			w.Write( c.IsFirstUnit );
			w.Write( c.SourceOffset ); w.Write( c.DestOffset );
			w.Write( c.DestInvalid );
		}
	}

	private static void ReadDma( BinaryReader r, GbaDmaController dma )
	{
		dma.ActiveDma = r.ReadInt32();
		dma.CpuBlocked = r.ReadBoolean();
		dma.PerformingDma = r.ReadInt32();

		for ( int i = 0; i < 4; i++ )
		{
			var c = dma.Channels[i];
			c.SrcLow = r.ReadUInt16(); c.SrcHigh = r.ReadUInt16();
			c.DstLow = r.ReadUInt16(); c.DstHigh = r.ReadUInt16();
			c.CntLo = r.ReadUInt16(); c.Reg = r.ReadUInt16();
			c.NextSource = r.ReadUInt32(); c.NextDest = r.ReadUInt32();
			c.NextCount = r.ReadInt32(); c.Count = r.ReadInt32(); c.Latch = r.ReadUInt32();
			c.When = r.ReadInt64(); c.Cycles = r.ReadInt32();
			c.IsFirstUnit = r.ReadBoolean();
			c.SourceOffset = r.ReadInt32(); c.DestOffset = r.ReadInt32();
			c.DestInvalid = r.ReadBoolean();
		}

		dma.Update();
	}

	private static void WriteTimers( BinaryWriter w, GbaTimerController timers )
	{
		w.Write( timers.NextGlobalEvent );

		for ( int i = 0; i < 4; i++ )
		{
			var c = timers.Channels[i];
			w.Write( c.Reload ); w.Write( c.Counter ); w.Write( c.Control );
			w.Write( c.Enabled ); w.Write( c.CountUp ); w.Write( c.DoIrq );
			w.Write( c.PrescaleBits );
			w.Write( c.LastEvent ); w.Write( c.NextOverflowCycle );
		}
	}

	private static void ReadTimers( BinaryReader r, GbaTimerController timers )
	{
		timers.NextGlobalEvent = r.ReadInt64();

		for ( int i = 0; i < 4; i++ )
		{
			var c = timers.Channels[i];
			c.Reload = r.ReadUInt16(); c.Counter = r.ReadUInt16(); c.Control = r.ReadUInt16();
			c.Enabled = r.ReadBoolean(); c.CountUp = r.ReadBoolean(); c.DoIrq = r.ReadBoolean();
			c.PrescaleBits = r.ReadInt32();
			c.LastEvent = r.ReadInt64(); c.NextOverflowCycle = r.ReadInt64();
		}

		timers.RecalcGlobalEvent();
	}

	private static void WriteSavedata( BinaryWriter w, GbaSavedata savedata )
	{
		w.Write( (int)savedata.Type );
		w.Write( savedata.Data.Length );
		w.Write( savedata.Data );
		savedata.Serialize( w );
	}

	private static void ReadSavedata( BinaryReader r, GbaSavedata savedata )
	{
		var type = (SavedataType)r.ReadInt32();
		int len = r.ReadInt32();
		var data = r.ReadBytes( len );

		if ( type == savedata.Type && data.Length == savedata.Data.Length )
			Array.Copy( data, savedata.Data, data.Length );
		else if ( type == savedata.Type )
			r.BaseStream.Position -= len;

		savedata.Deserialize( r );
	}

	private static void WriteHardware( BinaryWriter w, GbaCartridgeHardware hardware )
	{
		w.Write( hardware.HasRtc );
		hardware.Serialize( w );
	}

	private static void ReadHardware( BinaryReader r, GbaCartridgeHardware hardware )
	{
		hardware.HasRtc = r.ReadBoolean();
		hardware.Deserialize( r );
	}

	private static void WriteBios( BinaryWriter w, GbaBios bios )
	{
		w.Write( bios.HleActive );
		w.Write( bios.BiosStall );
	}

	private static void ReadBios( BinaryReader r, GbaBios bios )
	{
		bios.HleActive = r.ReadBoolean();
		bios.BiosStall = r.ReadInt32();
	}

	private static void WriteSystem( BinaryWriter w, Gba gba )
	{
		w.Write( gba.CyclesThisFrame );
		w.Write( gba.FrameCounter );
		w.Write( gba.TotalCycles );
	}

	private static void ReadSystem( BinaryReader r, Gba gba )
	{
		gba.CyclesThisFrame = r.ReadInt32();
		gba.FrameCounter = r.ReadInt64();
		gba.TotalCycles = r.ReadInt64();
		gba.IsRunning = true;
	}
}