using Sandbox.Network;
using Sandbox.Utility;
using System.IO;
using System.Text.Json.Nodes;

namespace Sandbox;

/// <summary>
/// This is created and referenced by the network system, as a way to route.
/// </summary>
[Expose]
public partial class SceneNetworkSystem : GameNetworkSystem
{
	internal static SceneNetworkSystem Instance { get; set; }
	internal DeltaSnapshotSystem DeltaSnapshots { get; private set; }

	private List<NetworkObject> BatchSpawnList { get; set; } = [];
	private static bool IsSupressingSpawnMessages { get; set; }
	private bool IsBatchNetworkSpawning { get; set; }
	private int BatchNetworkSpawnCount { get; set; }

	internal override bool IsHostBusy => !Game.ActiveScene?.IsLoading ?? true;

	internal bool IsDisconnecting { get; set; }

	internal SceneNetworkSystem( Internal.TypeLibrary typeLibrary, NetworkSystem system )
	{
		Instance = this;
		DeltaSnapshots = new( this );

		Library = typeLibrary;
		NetworkSystem = system;

		AddHandler<ObjectCreateBatchMsg>( OnObjectCreateBatch );
		AddHandler<ObjectCreateMsg>( OnObjectCreate );
		AddHandler<ObjectRefreshMsg>( OnObjectRefresh );
		AddHandler<ObjectDestroyComponentMsg>( OnObjectDestroyComponent );
		AddHandler<ObjectDestroyDescendantMsg>( OnObjectDestroyDescendant );
		AddHandler<ObjectRefreshComponentMsg>( OnObjectRefreshComponent );
		AddHandler<ObjectRefreshDescendantMsg>( OnObjectRefreshDescendant );
		AddHandler<ObjectRefreshMsgAck>( OnObjectRefreshAck );
		AddHandler<ObjectDestroyMsg>( OnObjectDestroy );
		AddHandler<ObjectRpcMsg>( OnObjectMessage );
		AddHandler<ObjectNetworkTableMsg>( OnNetworkTableChanges );
		AddHandler<SceneNetworkTableMsg>( OnNetworkTableChanges );
		AddHandler<SceneRpcMsg>( OnSceneRpc );
		AddHandler<StaticRpcMsg>( OnStaticRpc );
		AddHandler<LoadSceneBeginMsg>( OnLoadSceneMsg );
		AddHandler<LoadSceneSnapshotMsg>( OnLoadSceneSnapshotMsg );
		AddHandler<LoadSceneRequestSnapshotMsg>( OnLoadSceneRequestSnapshotMsg );
		AddHandler<SceneLoadedMsg>( OnSceneLoadedMsg );
	}

	internal void OnHotload()
	{
		DeltaSnapshots.Reset();
	}

	/// <summary>
	/// Any <see cref="GameObject">GameObjects</see> created within this scope will not send spawn messages to other clients.
	/// </summary>
	internal static IDisposable SuppressSpawnMessages()
	{
		IsSupressingSpawnMessages = true;

		return new DisposeAction( () =>
		{
			IsSupressingSpawnMessages = false;
		} );
	}

	private readonly Dictionary<Guid, Guid> PendingSceneLoads = new();

	/// <summary>
	/// Load a scene for all other clients. This can only be called by the host.
	/// </summary>
	internal void LoadSceneBroadcast( SceneLoadOptions options )
	{
		if ( !Networking.IsActive || !Networking.IsHost )
			return;

		var loadMsg = new LoadSceneBeginMsg
		{
			ShowLoadingScreen = options.ShowLoadingScreen,
			MountedVPKs = Game.ActiveScene.GetAllComponents<MapInstance>().Select( x => x.MapName ).ToList(),
			Id = Guid.NewGuid()
		};

		var msg = ByteStream.Create( 256 );
		msg.Write( InternalMessageType.Packed );

		Networking.System.Serialize( loadMsg, ref msg );

		foreach ( var c in Connection.All )
		{
			if ( c.State < Connection.ChannelState.Snapshot )
				continue;

			PendingSceneLoads[c.Id] = loadMsg.Id;
			c.SendRawMessage( msg );
		}

		msg.Dispose();
	}

	/// <summary>
	/// Start a network spawn batch. Any networked objects created within this scope
	/// will be sent with one spawn message. This makes sure that any references are
	/// kept to child networked objects when the objects are spawned on the other side.
	/// </summary>
	/// <returns></returns>
	internal IDisposable NetworkSpawnBatch()
	{
		IsBatchNetworkSpawning = true;
		BatchNetworkSpawnCount++;

		return new DisposeAction( () =>
		{
			BatchNetworkSpawnCount--;

			if ( BatchNetworkSpawnCount > 0 )
				return;

			IsBatchNetworkSpawning = false;
			SendNetworkSpawnBatch();
		} );
	}

	private void SendNetworkSpawnBatch()
	{
		// If we only have one, just send a normal message.
		if ( BatchSpawnList.Count == 1 )
		{
			var networkObject = BatchSpawnList.FirstOrDefault();

			if ( !(networkObject.GameObject?.IsDestroyed ?? true) )
				Broadcast( networkObject.GetCreateMessage() );

			BatchSpawnList.Clear();
			return;
		}

		var msg = new ObjectCreateBatchMsg();
		var list = new List<ObjectCreateMsg>();

		foreach ( var networkObject in BatchSpawnList )
		{
			if ( networkObject.GameObject?.IsDestroyed ?? true )
				continue;

			list.Add( networkObject.GetCreateMessage() );
		}

		msg.CreateMsgs = list.ToArray();
		BatchSpawnList.Clear();

		Broadcast( msg );
	}

	/// <summary>
	/// Broadcast the spawning of a networked object. This will add the networked object
	/// to batch list if we're spawning as part of a batch, and will ignore the spawn message
	/// entirely if we're supposed to be suppressing spawn messages.
	/// </summary>
	/// <param name="networkObject"></param>
	internal void NetworkSpawnBroadcast( NetworkObject networkObject )
	{
		// We're not supposed to send spawn messages right now.
		if ( IsSupressingSpawnMessages )
			return;

		if ( IsBatchNetworkSpawning )
		{
			BatchSpawnList.Add( networkObject );
			return;
		}

		Broadcast( networkObject.GetCreateMessage() );
	}

	/// <summary>
	/// Called when the host has provided us with a snapshot for a newly loaded scene.
	/// </summary>
	private async Task OnLoadSceneSnapshotMsg( LoadSceneSnapshotMsg msg, Connection connection, Guid msgId )
	{
		NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.Snapshot, msg );

		await SetSnapshotAsync( msg.Snapshot );

		// Let them know we have now loaded this scene.
		var loadedMsg = new SceneLoadedMsg { Id = msg.Id };
		connection.SendMessage( loadedMsg, NetFlags.Reliable );

		LoadingScreen.IsVisible = false;
	}

	/// <summary>
	/// Called when a client has requested the snapshot for a newly loaded scene. This is usually
	/// once they've done any preloading that they need to do.
	/// </summary>
	private void OnLoadSceneRequestSnapshotMsg( LoadSceneRequestSnapshotMsg msg, Connection connection, Guid msgId )
	{
		// If this connection doesn't have a pending scene load with this id then bail.
		if ( !PendingSceneLoads.TryGetValue( connection.Id, out var id ) || id != msg.Id )
			return;

		var output = new LoadSceneSnapshotMsg { Id = msg.Id };
		var snapshot = new SnapshotMsg
		{
			GameObjectSystems = [],
			NetworkObjects = new( 64 )
		};

		GetSnapshot( default, ref snapshot );
		output.Snapshot = snapshot;

		var bs = ByteStream.Create( 256 );
		bs.Write( InternalMessageType.Packed );

		Networking.System.Serialize( output, ref bs );
		connection.SendRawMessage( bs );

		bs.Dispose();
	}

	/// <summary>
	/// Called when the host has told us to load a new scene.
	/// </summary>
	private async Task OnLoadSceneMsg( LoadSceneBeginMsg msg, Connection connection, Guid msgId )
	{
		if ( !Game.IsEditor && msg.ShowLoadingScreen )
		{
			LoadingScreen.IsVisible = true;
			LoadingScreen.Title = "Loading Scene";
		}

		// Go ahead and destroy the scene
		if ( Game.ActiveScene is not null )
		{
			Game.ActiveScene?.Destroy();
			Game.ActiveScene = null;
		}

		MountedVPKs?.Dispose();
		MountedVPKs = await MountMaps( msg.MountedVPKs );

		// Let them know we would like a snapshot now.
		var loadedMsg = new LoadSceneRequestSnapshotMsg { Id = msg.Id };
		connection.SendMessage( loadedMsg, NetFlags.Reliable );
	}

	/// <summary>
	/// Called by clients to confirm they have finished loading the new scene.
	/// </summary>
	private void OnSceneLoadedMsg( SceneLoadedMsg msg, Connection connection, Guid msgId )
	{
		// If this connection doesn't have a pending scene load with this id then bail.
		if ( !PendingSceneLoads.TryGetValue( connection.Id, out var id ) || id != msg.Id )
			return;

		PendingSceneLoads.Remove( connection.Id );
		Instance?.OnJoined( connection );
	}

	/// <summary>
	/// A client has joined and wants to know what VPKs to preload.
	/// </summary>
	public override void GetMountedVPKs( Connection source, ref MountedVPKsResponse msg )
	{
		msg.MountedVPKs = Game.ActiveScene.GetAllComponents<MapInstance>().Select( x => x.MapName ).ToList();
	}

	/// <summary>
	/// Asynchronously load and mount any VPKs from the provided server response.
	/// </summary>
	public override async Task MountVPKs( Connection source, MountedVPKsResponse msg )
	{
		// Mount any vpks early because snapshotted or networked objects can use resources within
		// This removes it's refcount at the end because the MapInstance should take over
		MountedVPKs?.Dispose();
		MountedVPKs = await MountMaps( msg.MountedVPKs );
	}

	private static readonly GameObject.SerializeOptions _snapshotSerializeOptions = new() { SceneForNetwork = true };

	/// <summary>
	/// A client has joined and wants a snapshot of the world.
	/// </summary>
	public override void GetSnapshot( Connection source, ref SnapshotMsg msg )
	{
		ThreadSafe.AssertIsMainThread();
		using var _ = PerformanceStats.Timings.Network.Scope();

		msg.Time = Time.Now;

		var analytic = new Api.Events.EventRecord( "SceneNetworkSystem.GetSnapshot" );

		using ( analytic.ScopeTimer( "SceneTime" ) )
		{
			msg.SceneData = Game.ActiveScene.Serialize( _snapshotSerializeOptions ).ToJsonString();
		}

		using ( analytic.ScopeTimer( "NetworkObjectTime" ) )
		{
			Game.ActiveScene.SerializeNetworkObjects( msg.NetworkObjects );
		}

		var systems = Game.ActiveScene.GetSystems();

		foreach ( var system in systems )
		{
			var type = new SnapshotMsg.GameObjectSystemData
			{
				TableData = system.WriteDataTable( true ),
				Type = Game.TypeLibrary.GetType( system.GetType() ).Identity,
				Id = system.Id
			};

			msg.GameObjectSystems.Add( type );
		}

		analytic.SetValue( "SceneDataLength", msg.SceneData?.Length ?? 0 );
		analytic.SetValue( "NetworkObjectCount", msg.NetworkObjects?.Count ?? 0 );
		analytic.SetValue( "GameObjectCount", Game.ActiveScene.Directory.GameObjectCount );
		analytic.SetValue( "ComponentCount", Game.ActiveScene.Directory.ComponentCount );
		analytic.SetValue( "Machine", Environment.MachineName );

		analytic.Submit();
	}

	public override void Dispose()
	{
		base.Dispose();

		MountedVPKs?.Dispose();
		MountedVPKs = null;

		if ( Instance == this )
			Instance = null;
	}

	protected string WorkoutMapName()
	{
		if ( Game.ActiveScene is null ) return "<empty>";

		foreach ( var map in Game.ActiveScene.GetAllComponents<MapInstance>() )
		{
			if ( !map.Active ) continue;
			if ( !map.IsLoaded ) continue;

			return map.MapName;
		}

		return Game.ActiveScene.Name;
	}

	protected override void Tick()
	{
		if ( !Networking.IsHost )
			return;

		Networking.MapName = WorkoutMapName();
	}

	private IDisposable MountedVPKs { get; set; }
	private async Task<IDisposable> MountMaps( List<string> maps )
	{
		// Lets see if any are cloud maps and mount those first
		List<string> vpks = new();
		foreach ( var map in maps )
		{
			if ( map.EndsWith( ".vpk" ) )
			{
				vpks.Add( map );
				continue;
			}

			if ( !Package.TryParseIdent( map, out var parts ) )
				continue;

			var package = await Package.Fetch( map, false );
			if ( package is null )
				continue;

			var fs = await package.MountAsync();
			if ( fs is null ) continue;

			var mapFileName = package.PrimaryAsset;
			vpks.Add( mapFileName );
		}

		foreach ( var vpk in vpks )
			g_pWorldRendererMgr.MountWorldVPK( Path.GetFileNameWithoutExtension( vpk ), Path.ChangeExtension( vpk, ".vpk" ) );

		return new DisposeAction( () =>
		{
			foreach ( var vpk in vpks )
				g_pWorldRendererMgr.UnmountWorldVPK( Path.GetFileNameWithoutExtension( vpk ) );
		} );
	}

	/// <summary>
	/// We have recieved a snapshot of the world.
	/// </summary>
	public override async Task SetSnapshotAsync( SnapshotMsg msg )
	{
		ThreadSafe.AssertIsMainThread();

		if ( Game.ActiveScene is not null )
		{
			Game.ActiveScene?.Destroy();
			Game.ActiveScene = null;
		}

		Game.ActiveScene = new();
		Game.ActiveScene.StartLoading();

		Time.Now = (float)msg.Time;
		Game.ActiveScene.UpdateTimeFromHost( msg.Time );

		{
			using var batchGroup = CallbackBatch.Batch();

			if ( !string.IsNullOrWhiteSpace( msg.SceneData ) )
			{
				var sceneData = JsonNode.Parse( msg.SceneData ).AsObject();
				Game.ActiveScene.Deserialize( sceneData );
			}

			var createdNetworkObjects = new List<Tuple<GameObject, ObjectCreateMsg>>();

			foreach ( var nwo in msg.NetworkObjects )
			{
				if ( nwo is not ObjectCreateMsg oc )
					continue;

				var go = new GameObject();
				go.Deserialize( JsonNode.Parse( oc.JsonData ).AsObject() );
				createdNetworkObjects.Add( new( go, oc ) );
			}

			foreach ( var (go, oc) in createdNetworkObjects )
			{
				go.NetworkSpawnRemote( oc );
			}
		}

		foreach ( var s in msg.GameObjectSystems )
		{
			var type = Game.TypeLibrary.GetTypeByIdent( s.Type );
			var system = Game.ActiveScene.GetSystemByType( type );

			if ( system is null )
				continue;

			system.Id = s.Id;
			system.ReadDataTable( s.TableData );
		}

		MountedVPKs?.Dispose();
		MountedVPKs = null;

		// Wait for loading to finish
		if ( Game.ActiveScene is not null )
		{
			await Game.ActiveScene.WaitForLoading();
		}

		if ( Game.ActiveScene.IsValid() )
		{
			Game.ActiveScene.Signal( GameObjectSystem.Stage.SceneLoaded );

			Game.ActiveScene.RunEvent<ISceneStartup>( x => x.OnClientInitialize() );
		}

		Game.IsPlaying = true;
	}

	/// <summary>
	/// Called on the host to decide whether to accept a <see cref="Connection"/>. If any <see cref="Component"/>
	/// that implements this returns false, the connection will be denied.
	/// </summary>
	/// <param name="channel"></param>
	/// <param name="reason">The reason to display to the client.</param>
	public override bool AcceptConnection( Connection channel, ref string reason )
	{
		foreach ( var c in Game.ActiveScene.GetAll<Component.INetworkListener>() )
		{
			if ( !c.AcceptConnection( channel, ref reason ) )
				return false;
		}

		return true;
	}

	public override void OnConnected( Connection client )
	{
		Action queue = default;

		foreach ( var c in Game.ActiveScene.GetAll<Component.INetworkListener>() )
		{
			queue += () => c.OnConnected( client );
		}

		try
		{
			queue?.Invoke();
		}
		catch ( Exception e )
		{
			Log.Error( e, "Exception when calling INetworkListener.OnConnected" );
		}
	}

	public override void OnInitialize()
	{
		if ( !Networking.IsHost )
			return;

		var scene = Game.ActiveScene;

		if ( !scene.IsValid() || scene.IsLoading )
			return;

		var sceneInformation = scene.Components.Get<SceneInformation>();
		OnLoadedScene( sceneInformation?.Title );
	}

	public override void OnJoined( Connection client )
	{
		Action queue = default;

		foreach ( var c in Game.ActiveScene.GetAll<Component.INetworkListener>() )
		{
			queue += () => c.OnActive( client );
		}

		try
		{
			queue?.Invoke();
		}
		catch ( Exception e )
		{
			Log.Error( e, "Exception when calling INetworkListener.OnActive" );
		}
	}

	public override void OnLeave( Connection client )
	{
		DeltaSnapshots.RemoveConnection( client );

		if ( Game.ActiveScene is not null )
		{
			foreach ( var no in Game.ActiveScene.networkedObjects )
			{
				no.RemoveConnection( client.Id );
			}

			foreach ( var system in Game.ActiveScene.GetSystems() )
			{
				system.LocalSnapshotState.RemoveConnection( client.Id );
			}

			if ( Networking.IsHost )
			{
				Action queue = default;

				foreach ( var c in Game.ActiveScene.GetAll<Component.INetworkListener>() )
				{
					queue += () => c.OnDisconnected( client );
				}

				try
				{
					queue?.Invoke();
				}
				catch ( Exception e )
				{
					Log.Error( e, "Exception when calling INetworkListener.OnDisconnected" );
				}
			}
		}

		if ( client.Id == Guid.Empty )
			return;

		DoOrphanedActions( client );
	}

	public override void OnHostChanged( Connection previousHost, Connection newHost )
	{
		var scene = Game.ActiveScene;

		if ( scene.IsValid() )
		{
			foreach ( var system in scene.GetSystems() )
			{
				system.LocalSnapshotState.ClearConnections();
			}

			foreach ( var no in scene.networkedObjects )
			{
				no.ClearConnections();
			}
		}

		foreach ( var connection in Connection.All )
		{
			connection.Input.Clear();
		}

		DeltaSnapshots?.Reset();
		UserCommand.Reset();
	}

	public override void OnBecameHost( Connection previousHost )
	{
		// Was the host at startup, so this call isn't needed
		if ( previousHost is null || previousHost.Id == Guid.Empty )
			return;

		Log.Info( $"Became the host (previous host was {previousHost})" );
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() ) return;

		Action queue = default;
		foreach ( var c in scene.GetAll<Component.INetworkListener>() )
		{
			queue += () => c.OnBecameHost( previousHost );
		}

		try
		{
			queue?.Invoke();
		}
		catch ( Exception e )
		{
			Log.Error( e, "Exception when calling INetworkListener.OnBecameHost" );
		}

		// Don't run orphaned actions if the previous host is still connected.
		if ( previousHost.IsActive )
			return;

		DoOrphanedActions( previousHost );
	}

	internal void DoOrphanedActions( Connection connection )
	{
		Game.ActiveScene?.DoOrphanedActions( connection );
	}

	public override IDisposable Push()
	{
		return Game.ActiveScene is null ? null : Game.ActiveScene.Push();
	}

	private void OnObjectDestroyDescendant( ObjectDestroyDescendantMsg message, Connection source )
	{
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() )
			return;

		var go = scene.Directory.FindByGuid( message.Guid );
		if ( !go.IsValid() ) return;

		var root = go.Network.RootGameObject;
		if ( !root.IsValid() ) return;

		if ( root._net is null )
		{
			Log.Warning( $"ObjectDestroyDescendant: Object {root} is not networked" );
			return;
		}

		// Only the owner or the host can do this.
		if ( !root._net.HasControl( source ) && !source.IsHost )
			return;

		go.Destroy();
	}

	private void OnObjectDestroyComponent( ObjectDestroyComponentMsg message, Connection source )
	{
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() )
			return;

		var component = scene.Directory.FindComponentByGuid( message.Guid );
		if ( component == null ) return;

		var root = component.Network.RootGameObject;
		if ( !root.IsValid() ) return;

		if ( root._net is null )
		{
			Log.Warning( $"ObjectDestroyComponent: Object {root} is not networked" );
			return;
		}

		// Only the owner or the host can do this.
		if ( !root._net.HasControl( source ) && !source.IsHost )
			return;

		component.Destroy();
	}

	private void OnObjectRefreshAck( ObjectRefreshMsgAck message, Connection source )
	{
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() )
			return;

		var obj = scene.Directory.FindByGuid( message.Guid );
		if ( obj is null ) return;

		if ( obj._net is null )
		{
			Log.Warning( $"ObjectRefreshAck: Object {obj} is not networked" );
			return;
		}

		DeltaSnapshots.ClearNetworkObject( obj._net );
	}

	private void OnObjectRefreshDescendant( ObjectRefreshDescendantMsg message, Connection source )
	{
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() )
			return;

		var parentObject = scene.Directory.FindByGuid( message.ParentId );
		if ( !parentObject.IsValid() )
			return;

		var root = parentObject.Network.RootGameObject;
		if ( !root.IsValid() ) return;

		if ( root._net is null )
		{
			Log.Warning( $"ObjectRefreshDescendant: Object {root} is not networked" );
			return;
		}

		// Only the owner or the host can do this.
		if ( !root._net.HasControl( source ) && !source.IsHost )
			return;

		var gameObjectJson = JsonNode.Parse( message.JsonData ).AsObject();

		if ( !gameObjectJson.TryGetPropertyValue( GameObject.JsonKeys.Id, out var childId ) )
			return;

		var gameObject = scene.Directory.FindByGuid( childId.GetValue<Guid>() );

		if ( !gameObject.IsValid() )
		{
			gameObject = new GameObject( parentObject, false );
		}
		else if ( gameObject != parentObject )
		{
			gameObject.SetParentFromNetwork( parentObject );
		}

		using ( var _ = CallbackBatch.Batch() )
		{
			gameObject?.Deserialize( gameObjectJson, new GameObject.DeserializeOptions
			{
				IsNetworkRefresh = true,
				IsRefreshing = true
			} );
		}

		root._net.UpdateFromRefresh( source, message.TableData, message.Snapshot );
	}

	private void OnObjectRefreshComponent( ObjectRefreshComponentMsg message, Connection source )
	{
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() )
			return;

		var gameObject = scene.Directory.FindByGuid( message.GameObjectId );

		if ( !gameObject.IsValid() )
			return;

		var root = gameObject.Network.RootGameObject;
		if ( !root.IsValid() ) return;

		if ( root._net is null )
		{
			Log.Warning( $"ObjectRefreshComponent: Object {root} is not networked" );
			return;
		}

		// Only the owner or the host can do this.
		if ( !root._net.HasControl( source ) && !source.IsHost )
			return;

		var componentJson = JsonNode.Parse( message.JsonData ).AsObject();

		if ( !componentJson.TryGetPropertyValue( Component.JsonKeys.Id, out var componentId ) )
			return;

		var component = scene.Directory.FindComponentByGuid( componentId.GetValue<Guid>() );

		if ( !component.IsValid() )
		{
			var componentTypeName = componentJson.GetPropertyValue( Component.JsonKeys.Type, "" );
			var componentType = Game.TypeLibrary.GetType<Component>( componentTypeName, true );

			if ( componentType is null || componentType.TargetType.IsAbstract )
			{
				Log.Warning( $"TypeLibrary couldn't find {nameof( Component )} type {componentTypeName}" );
				return;
			}

			try
			{
				component = gameObject.Components.Create( componentType, false );
			}
			catch ( Exception e )
			{
				Log.Error( e );
			}
		}
		else if ( component.GameObject != gameObject )
		{
			return;
		}

		using ( var _ = CallbackBatch.Batch() )
		{
			component?.Deserialize( componentJson );
		}

		root._net.UpdateFromRefresh( source, message.TableData, message.Snapshot );
	}


	private void OnObjectRefresh( ObjectRefreshMsg message, Connection source )
	{
		NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.Refresh, message );
		NetworkDebugSystem.Current?.Track( "OnObjectRefresh", message );

		// Is this a request from someone?
		if ( source is not null && !source.CanRefreshObjects )
			return;

		var scene = Game.ActiveScene;
		if ( !scene.IsValid() ) return;

		var obj = scene.Directory.FindByGuid( message.Guid );
		if ( obj is null ) return;

		if ( obj._net is null )
		{
			Log.Warning( $"ObjectRefresh: Object {obj} is not networked" );
			return;
		}

		if ( obj._net.IsUnowned )
		{
			// If we're unowned and the source is not the host, we can't refresh.
			if ( !source.IsHost )
				return;
		}
		else
		{
			// If the source is not the owner and not the host, we can't refresh.
			if ( !source.IsHost && obj._net.Owner != source.Id )
				return;
		}

		obj._net.OnRefreshMessage( source, message );
	}

	private void OnObjectCreateBatch( ObjectCreateBatchMsg message, Connection source, Guid msgId )
	{
		NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.Spawn, message );
		NetworkDebugSystem.Current?.Track( "OnObjectCreateBatch", message );

		// If we haven't even loaded a scene yet, this message was not sent in order (we don't even have the snapshot yet.)
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() )
			return;

		// Is this a request from someone?
		if ( source is not null && !source.CanSpawnObjects )
			return;

		using ( CallbackBatch.Batch() )
		{
			foreach ( var msg in message.CreateMsgs )
			{
				var go = new GameObject();
				go.Deserialize( JsonNode.Parse( msg.JsonData ).AsObject() );
				go.NetworkSpawnRemote( msg );
			}
		}
	}

	private void OnObjectCreate( ObjectCreateMsg message, Connection source, Guid msgId )
	{
		NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.Spawn, message );
		NetworkDebugSystem.Current?.Track( "OnObjectCreate", message );

		// If we haven't even loaded a scene yet, this message was not sent in order (we don't even have the snapshot yet.)
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() )
			return;

		// Is this a request from someone?
		if ( source is not null && !source.CanSpawnObjects )
			return;

		var go = new GameObject();

		using ( CallbackBatch.Batch() )
		{
			go.Deserialize( JsonNode.Parse( message.JsonData ).AsObject() );
			go.NetworkSpawnRemote( message );
		}
	}

	private void OnNetworkTableChanges( SceneNetworkTableMsg message, Connection source, Guid msgId )
	{
		NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.SyncVars, message );

		var scene = Game.ActiveScene;
		if ( !scene.IsValid() )
			return;

		var system = scene.Directory.FindSystemByGuid( message.Guid );
		if ( system is null )
			return;

		// Can we receive network table changes from this source?
		if ( !source.IsHost )
			return;

		system.ReadDataTable( message.TableData );
	}

	private void OnNetworkTableChanges( ObjectNetworkTableMsg message, Connection source, Guid msgId )
	{
		NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.SyncVars, message );

		var scene = Game.ActiveScene;
		if ( !scene.IsValid() )
			return;

		var obj = scene.Directory.FindByGuid( message.Guid );
		if ( obj is null )
			return;

		if ( obj._net is null )
		{
			Log.Warning( $"ObjectNetworkTable: Object {obj} is not networked" );
			return;
		}

		// Can we receive network table changes from this source?
		if ( !source.IsHost && source.Id != obj._net.Owner )
			return;

		obj._net.OnNetworkTableMessage( message );
	}

	private void OnObjectDestroy( ObjectDestroyMsg message, Connection source, Guid msgId )
	{
		NetworkDebugSystem.Current?.Track( "OnObjectDestroy", message );

		var scene = Game.ActiveScene;
		if ( !scene.IsValid() )
			return;

		var obj = scene.Directory.FindByGuid( message.Guid );
		if ( obj is null )
			return;

		if ( obj._net is null )
		{
			// We can't just destroy arbitrary game objects.
			Log.Warning( $"ObjectDestroy: Object {obj} is not networked" );
			return;
		}

		if ( obj._net.IsUnowned )
		{
			// If we're unowned and the source is not the host, we can't destroy.
			if ( !source.IsHost )
				return;
		}
		else
		{
			// If the source is not the owner and not the host, we can't destroy.
			if ( !source.IsHost && obj._net.Owner != source.Id )
				return;
		}

		obj._net.OnNetworkDestroy();
	}

	private void OnObjectMessage( ObjectRpcMsg rpc, Connection source, Guid msgId )
	{
		Rpc.IncomingInstanceRpcMsg( rpc, source );
	}

	private void OnSceneRpc( SceneRpcMsg message, Connection source, Guid msgId )
	{
		Rpc.IncomingInstanceRpcMsg( message, source );
	}

	private void OnStaticRpc( StaticRpcMsg message, Connection source, Guid msgId )
	{
		Rpc.IncomingStaticRpcMsg( message, source );
	}

	/// <summary>
	/// A heartbeat has been received from the host. We should make sure our times are in sync.
	/// </summary>
	internal override void OnHeartbeat( float serverGameTime )
	{
		Game.ActiveScene?.UpdateTimeFromHost( serverGameTime );
	}

	/// <summary>
	/// We've received a cull state change for a networked object.
	/// </summary>
	internal override void OnCullStateChangeMessage( ByteStream bs, Connection source )
	{
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() ) return;

		NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.Culling, bs.Length );

		var objectId = bs.Read<Guid>();
		var isCulled = bs.Read<bool>();

		var go = scene.Directory.FindByGuid( objectId );
		if ( !go.IsValid() ) return;

		if ( go.IsNetworkCulled == isCulled )
			return;

		var ownerId = go.Network.OwnerId;
		var isOwner = (source.Id == ownerId) || (ownerId == Guid.Empty && source.IsHost);

		if ( !isOwner )
			return;

		go.IsNetworkCulled = isCulled;
		go.UpdateNetworkCulledState();
	}

	/// <summary>
	/// A delta snapshot message has been received from another connection.
	/// </summary>
	internal override void OnDeltaSnapshotMessage( InternalMessageType type, ByteStream bs, Connection source )
	{
		if ( type == InternalMessageType.DeltaSnapshot )
		{
			NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.SyncVars, bs.Length );
			DeltaSnapshots.OnDeltaSnapshot( source, bs );
		}
		else if ( type == InternalMessageType.DeltaSnapshotAck )
		{
			DeltaSnapshots.OnDeltaSnapshotAck( source, bs );
		}
		else if ( type == InternalMessageType.DeltaSnapshotCluster )
		{
			NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.SyncVars, bs.Length );
			DeltaSnapshots.OnDeltaSnapshotCluster( source, bs );
		}
		else if ( type == InternalMessageType.DeltaSnapshotClusterAck )
		{
			DeltaSnapshots.OnDeltaSnapshotClusterAck( source, bs );
		}
	}
}

/// <summary>
/// When a client has sent an acknowledgement that they've received a refresh message for
/// a networked object.
/// </summary>
[Expose]
struct ObjectRefreshMsgAck
{
	public Guid Guid { get; set; }
}

/// <summary>
/// When a <see cref="Component"/> in the hierarchy of a networked object has
/// been destroyed.
/// </summary>
[Expose]
struct ObjectDestroyComponentMsg
{
	public Guid Guid { get; set; }
}

/// <summary>
/// When a <see cref="GameObject"/> in the hierarchy of a networked object has
/// been destroyed.
/// </summary>
[Expose]
struct ObjectDestroyDescendantMsg
{
	public Guid Guid { get; set; }
}

/// <summary>
/// When a <see cref="GameObject"/> in the hierarchy of a networked object has
/// been added or changed.
/// </summary>
[Expose]
struct ObjectRefreshDescendantMsg
{
	public string JsonData { get; set; }
	public byte[] TableData { get; set; }
	public byte[] Snapshot { get; set; }
	public Guid ParentId { get; set; }
	public Guid GameObjectId { get; set; }
}

/// <summary>
/// When a <see cref="Component"/> in the hierarchy of a networked object has
/// been added or changed.
/// </summary>
[Expose]
struct ObjectRefreshComponentMsg
{
	public string JsonData { get; set; }
	public byte[] TableData { get; set; }
	public byte[] Snapshot { get; set; }
	public Guid GameObjectId { get; set; }
}

/// <summary>
/// When a networked object has been refreshed. This is a full update message for that
/// networked object. Any new GameObjects or Components in the hierarchy will be
/// created and existing ones will be updated.
/// </summary>
[Expose]
struct ObjectRefreshMsg
{
	public string JsonData { get; set; }
	public byte[] TableData { get; set; }
	public byte[] Snapshot { get; set; }
	public Guid Parent { get; set; }
	public Guid Guid { get; set; }
}

[Expose]
struct LoadSceneBeginMsg
{
	public List<string> MountedVPKs { get; set; }
	public bool ShowLoadingScreen { get; set; }
	public Guid Id { get; set; }
}

[Expose]
struct LoadSceneRequestSnapshotMsg
{
	public Guid Id { get; set; }
}

[Expose]
struct LoadSceneSnapshotMsg
{
	public SnapshotMsg Snapshot { get; set; }
	public Guid Id { get; set; }
}

[Expose]
struct SceneLoadedMsg
{
	public Guid Id { get; set; }
}

[Expose]
struct ObjectCreateBatchMsg
{
	public ObjectCreateMsg[] CreateMsgs { get; set; }
}

[Expose]
struct ObjectCreateMsg
{
	public ushort SnapshotVersion { get; set; }
	public string JsonData { get; set; }
	public Transform Transform { get; set; }
	public Guid Guid { get; set; }
	public Guid Creator { get; set; }
	public Guid Parent { get; set; }
	public Guid Owner { get; set; }
	public byte[] TableData { get; set; }
	public bool Enabled { get; set; }
}

[Expose]
struct ObjectNetworkTableMsg
{
	public Guid Guid { get; set; }
	public byte[] TableData { get; set; }
}

[Expose]
struct SceneNetworkTableMsg
{
	public Guid Guid { get; set; }
	public byte[] TableData { get; set; }
}

[Expose]
struct ObjectDestroyMsg
{
	public Guid Guid { get; set; }
}

[Expose]
struct SceneRpcMsg
{
	public Guid Guid { get; set; }
	public int MethodIdentity { get; set; }
	public object[] Arguments { get; set; }
}

[Expose]
struct ObjectRpcMsg
{
	public Guid Guid { get; set; }
	public Guid ComponentId { get; set; }
	public int MethodIdentity { get; set; }
	public object[] Arguments { get; set; }
}

[Expose]
struct StaticRpcMsg
{
	public int MethodIdentity { get; set; }
	public object[] Arguments { get; set; }
}
