UI/GameMenu/GameMenu.razor.cs
using Sandbox.Diagnostics;
using Sandbox.UI;
using System;
using System.IO;
using System.IO.Compression;

namespace GuessIt;

public enum LobbyState
{
	WaitingForPlayers,
	ChoosingWord,
	Playing,
	Results
}

public partial class GameMenu : PanelComponent, Component.INetworkListener
{
	[Sync] public LobbyState LobbyState { get; set; } = LobbyState.WaitingForPlayers;
	[Sync] public Player CurrentlyDrawing { get; set; }
	[Sync] public List<Player> RemainingPlayers { get; set; } = new();
	public List<Player> AllPlayers => Scene.GetAllComponents<Player>().ToList();
	public List<Player> CorrectPlayers => AllPlayers.Where( x => x.HasGuessed ).ToList();

	[Sync] public string CurrentWord { get; set; } = "";
	[Sync] public float CurrentDifficulty { get; set; } = 1f;
	[Sync] public int CurrentRound { get; set; } = 0;
	[Sync] public int TotalRounds { get; set; } = 2;
	[Sync] public TimeUntil GameTimer { get; set; } = 0f;
	[Sync] public bool HasDrawn { get; set; } = false;
	[Sync] public bool RoundEnded { get; set; } = false;
	[Sync] public TimeSince TimeSinceLetterReveal { get; set; } = 0f;

	public Texture Canvas { get; set; }
	public float RoundLength => 90f;
	public float ResultsLength => 15f;
	public float PickWordLength => 15f;
	public int BrushSize = 2;
	public Color BrushColor = Color.Black;

	GameHeader Header { get; set; }
	Panel PlayerList { get; set; }
	Panel CanvasContainer { get; set; }
	GameCanvas CanvasPanel { get; set; }
	Toolbar CanvasToolbar { get; set; }
	Panel ChatBox { get; set; }
	TextEntry ChatEntry { get; set; }
	GameResults ResultsPanel { get; set; }

	int _clockIndex = 0;
	float _previousTime = 0f;

	public static GameMenu Instance { get; set; }

	protected override void OnStart()
	{
		base.OnStart();

		Instance = this;
		CurrentRound = 0;
		TotalRounds = 2;
	}

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

		if ( LobbyState == LobbyState.Playing || LobbyState == LobbyState.ChoosingWord )
		{
			float seconds = MathF.Floor( MathF.Max( GameTimer, 0 ) );
			if ( seconds != _previousTime )
			{
				Sound.Play( "ui.clock.tick" + (_clockIndex + 1) );
				_clockIndex = (_clockIndex + 1) % 2;
				_previousTime = seconds;
			}
		}

		if ( LobbyState == LobbyState.Playing )
		{
			if ( GameTimer <= 0 )
			{
				if ( RoundEnded ) StartNextRound();
				else EndRound();
			}
			else if ( !HasDrawn && CorrectPlayers.Count == 0 && GameTimer <= (RoundLength - 15) )
			{
				EndRound();
			}
			else if ( TimeSinceLetterReveal > 25f )
			{
				int index = Random.Shared.Int( 0, CurrentWord.Length - 1 );
				BroadcastRevealLetter( index );
				TimeSinceLetterReveal = 0f;
			}
		}

		if ( LobbyState == LobbyState.Results )
		{
			if ( GameTimer <= 0 )
			{
				ResetGame();
			}
		}
	}

	protected override void OnDestroy()
	{
		Canvas?.Dispose();
	}

	protected override void OnTreeFirstBuilt()
	{
		base.OnTreeFirstBuilt();

		foreach ( var queuedMessage in ChatQueue )
		{
			CreateChatEntry( queuedMessage.Item1, queuedMessage.Item2, queuedMessage.Item3 );
		}
		ChatQueue.Clear();
	}

	public void ResetGame()
	{
		Assert.True( Networking.IsHost );

		CurrentRound = 0;
		CurrentlyDrawing = null;
		LobbyState = LobbyState.WaitingForPlayers;

		foreach ( var player in AllPlayers )
		{
			player.HasGuessed = false;
			player.Score = 0;
		}

		BroadcastRoundReset();
	}

	public void StartGame()
	{
		Assert.True( Networking.IsHost );
		if ( LobbyState != LobbyState.WaitingForPlayers ) return;
		if ( !string.IsNullOrEmpty( Header?.StartButtonClasses() ) ) return;

		foreach ( var player in AllPlayers )
		{
			player.Score = 0;
		}

		StartNextRound();
	}

	public void EndGame()
	{
		Assert.True( Networking.IsHost );
		if ( LobbyState == LobbyState.WaitingForPlayers || LobbyState == LobbyState.Results ) return;

		LobbyState = LobbyState.Results;
		GameTimer = ResultsLength;
		CurrentlyDrawing = null;
		RoundEnded = true;

		BroadcastShowResults();
	}

	public void StartNextRound()
	{
		Assert.True( Networking.IsHost );
		if ( LobbyState == LobbyState.Results ) return;

		var players = AllPlayers;
		if ( RemainingPlayers.Count == 0 )
		{
			if ( CurrentRound >= TotalRounds )
			{
				EndGame();
				return;
			}

			players.Sort( ( a, b ) => (Random.Shared.Next( 0, 2 ) == 0 ? -1 : 1) );
			RemainingPlayers = players.ToList();
			CurrentRound++;
		}

		foreach ( var player in players )
		{
			player.HasGuessed = false;
			player.BroadcastResetPose();
		}

		CurrentlyDrawing = RemainingPlayers[0];
		if ( !(CurrentlyDrawing?.GameObject?.IsValid() ?? false) )
		{
			RemainingPlayers.RemoveAt( 0 );
			StartNextRound();
			return;
		}

		LobbyState = LobbyState.ChoosingWord;
		RemainingPlayers.RemoveAt( 0 );
		GameTimer = PickWordLength;
		TimeSinceLetterReveal = 0;
		RoundEnded = false;
		HasDrawn = false;

		BroadcastRoundStart( CurrentlyDrawing );
	}

	public void EndRound()
	{
		Assert.True( Networking.IsHost );
		if ( LobbyState == LobbyState.Results || LobbyState == LobbyState.WaitingForPlayers ) return;
		if ( RoundEnded ) return;

		foreach ( var player in AllPlayers )
		{
			if ( (!player.IsDrawing && !player.HasGuessed) || (player.IsDrawing && CorrectPlayers.Count == 0) )
			{
				player.BroadcastAngryMorph();
			}
		}

		RoundEnded = true;
		GameTimer = 5;
		BroadcastRevealAnswer( CurrentWord, CorrectPlayers.Count > 0 );
		CurrentWord = "";
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	private void StartDrawing( string word )
	{
		LobbyState = LobbyState.Playing;

		if ( Player.Local.IsDrawing )
		{
			Header.WordPanel.SetWord( word, "You are drawing:", true );
		}
		else
		{
			Header.WordPanel.SetWord( word );
		}
	}

	public void SubmitGuess()
	{
		if ( string.IsNullOrEmpty( ChatEntry.Text ) ) return;
		BroadcastSubmitGuess( ChatEntry.Text );
		ChatEntry.Text = "";
		ChatEntry.Focus();
	}

	private void CorrectGuess( Player player )
	{
		Assert.True( Networking.IsHost );
		if ( player.HasGuessed ) return;

		int DrawingScore = 200;
		int PlayerScore = (int)MathF.Floor( MathX.Remap( GameTimer, RoundLength, 0, 1000, 800 ) );

		if ( CorrectPlayers.Count == 0 )
		{
			CurrentlyDrawing.BroadcastHappyMorph();
		}

		player.HasGuessed = true;
		player.BroadcastHappyMorph();

		if ( CorrectPlayers.Count == AllPlayers.Count - 1 )
		{
			if ( CorrectPlayers.Count > 1 )
			{
				PlayerScore = (int)MathF.Floor( MathX.Remap( GameTimer, 30, 0, 800, 250 ) );
				DrawingScore = 100;
				if ( CorrectPlayers.Count >= 2 )
				{
					AllCorrect();
				}
			}
			EndRound();
		}
		else if ( CorrectPlayers.Count == 1 )
		{
			if ( GameTimer > 30f ) GameTimer = 30f;
		}
		else
		{
			if ( CorrectPlayers.Count >= MathF.Floor( (AllPlayers.Count - 1) / 2 ) )
			{
				if ( GameTimer > 15f ) GameTimer = 15f;
			}
			PlayerScore = (int)MathF.Floor( MathX.Remap( GameTimer, 30, 0, 1000, 200 ) );
			DrawingScore = 100;
		}

		CurrentlyDrawing.Score += (int)MathF.Floor( DrawingScore * CurrentDifficulty );
		player.Score += (int)MathF.Floor( PlayerScore * CurrentDifficulty );
	}

	private void AllCorrect()
	{
		using ( Rpc.FilterInclude( x => x.Id == CurrentlyDrawing.Network.OwnerId ) )
		{
			BroadcastAllCorrect();
		}
	}

	public void SendCanvasToUser( Connection connection )
	{
		if ( !(Canvas?.IsValid ?? false) ) return;
		var colors = Canvas.GetPixels();
		var bytes = new List<byte>();
		foreach ( var color in colors )
		{
			bytes.Add( color.r );
			bytes.Add( color.g );
			bytes.Add( color.b );
			bytes.Add( color.a );
		}

		using var ms = new MemoryStream();
		using ( var zs = new GZipStream( ms, CompressionMode.Compress ) )
		{
			var data = bytes.ToArray();
			zs.Write( data, 0, data.Length );
		}

		var str = Convert.ToBase64String( ms.ToArray() );

		using ( Rpc.FilterInclude( x => x.Id == connection.Id ) )
		{
			BroadcastCanvas( str );
		}
	}

	[Rpc.Host]
	public void ChooseWord( string word, int difficulty )
	{
		var callingPlayer = AllPlayers.FirstOrDefault( x => x.Network.OwnerId == Rpc.CallerId );
		if ( !callingPlayer.IsDrawing ) return;

		CurrentWord = word;
		CurrentDifficulty = (new float[] { 0.9f, 1.0f, 1.1f })[difficulty];
		GameTimer = RoundLength;

		StartDrawing( CurrentWord );
	}

	public void Draw( Vector2 point, Color color, int size )
	{
		if ( Canvas is null ) { Log.Warning( "[GameMenu] Draw: Canvas is null" ); return; }
		if ( LobbyState != LobbyState.Playing ) { Log.Warning( $"[GameMenu] Draw: wrong state {LobbyState}" ); return; }
		if ( RoundEnded ) { Log.Warning( "[GameMenu] Draw: round ended" ); return; }
		HasDrawn = true;

		// Draw a circle of radius size at point with color color
		Canvas.Update( color.ToColor32(), new Rect( point.x - size, point.y - size, size * 2, size * 2 ) );
		CanvasPanel?.MarkDirty();
	}

	public async void Draw( List<Vector2> points, Color color, int size, bool delay = false )
	{
		if ( Canvas is null ) return;
		if ( LobbyState != LobbyState.Playing ) return;

		float totalTime = 50f; // ms
		float timePerPoint = totalTime / points.Count;

		// Draw lines of radius size between each point with color color
		for ( int i = 0; i < points.Count - 1; i++ )
		{
			Vector2 point1 = points[i];
			Vector2 point2 = points[i + 1];
			Vector2 diff = point2 - point1;
			float length = diff.Length;
			float timePerThing = timePerPoint / length;
			for ( int j = 0; j < length; j++ )
			{
				Vector2 point = point1 + (diff.Normal * j);
				Draw( point, color, size );
				if ( delay ) await GameTask.Delay( (int)MathF.Floor( timePerThing ) );
			}
		}
	}

	public void OnConnected( Connection connection )
	{
		if ( !Networking.IsHost ) return;
		BroadcastChatEntry( "", $"{connection.DisplayName} has joined the game!", "join-message" );
	}

	public void OnDisconnected( Connection connection )
	{
		if ( !Networking.IsHost ) return;
		BroadcastChatEntry( "", $"{connection.DisplayName} has left the game!", "leave-message" );
		if ( !CurrentlyDrawing.IsValid() || CurrentlyDrawing.Network.OwnerId == connection.Id || (CorrectPlayers.Count == AllPlayers.Count - 1) )
		{
			EndRound();
		}
		var player = AllPlayers.FirstOrDefault( x => x.Network.OwnerId == connection.Id );
		if ( player.IsValid() )
		{
			if ( !player.HasGuessed && (AllPlayers.Count - CorrectPlayers.Count) == 2 )
			{
				EndRound();
			}
		}
	}

	private void ResetCanvas()
	{
		byte[] textureData = new byte[320 * 240 * 4];
		for ( int i = 0; i < textureData.Length; i++ )
		{
			textureData[i] = 255;
		}

		// Create the new texture before disposing the old one so the Image
		// panel never briefly references a disposed/error texture.
		var oldCanvas = Canvas;
		Canvas = Texture.Create( 320, 240 )
			.WithDynamicUsage()
			.WithData( textureData )
			.Finish();

		CanvasPanel.SetTexture( Canvas );
		CanvasPanel.MarkDirty();
		oldCanvas?.Dispose();
		StateHasChanged();
	}

	public void ToggleLocalMic()
	{
		var localPlayer = Player.Local;
		if ( !localPlayer.IsValid() ) return;
		localPlayer.Voice.IsListening = !localPlayer.Voice.IsListening;
	}

	[Rpc.Host]
	public void RequestCanvas()
	{
		SendCanvasToUser( Rpc.Caller );
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	public void BroadcastCanvas( string compressedStr )
	{
		ResetCanvas();
		Header.WordPanel.SetWord( CurrentWord );

		byte[] bytes;
		try
		{
			using var ms = new MemoryStream( Convert.FromBase64String( compressedStr ) );
			using var zs = new GZipStream( ms, CompressionMode.Decompress );
			using var outStream = new MemoryStream();
			zs.CopyTo( outStream );
			bytes = outStream.ToArray();
		}
		catch
		{
			Log.Warning( "Failed to decompress canvas data" );
			return;
		}

		var colors = new List<Color32>();
		var size = bytes.Count() / 4;
		for ( int i = 0; i < size; i++ )
		{
			var index = i * 4;
			colors.Add( new Color32( bytes[index], bytes[index + 1], bytes[index + 2], bytes[index + 3] ) );
		}
		Canvas.Update( colors.ToArray() );
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	public void BroadcastRoundReset()
	{
		if ( Networking.IsHost )
		{
			Header.WordPanel.SetOverride( AllPlayers.Count <= 1 ? "Waiting for players..." : "Waiting for host to begin..." );
		}
		else
		{
			Header.WordPanel.SetOverride( "Waiting for host to begin..." );
		}

		ResultsPanel?.Delete();
		ResultsPanel = null;
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	public void BroadcastAllCorrect()
	{
		Sandbox.Services.Stats.Increment( "all-correct-while-drawing", 1 );
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	public void BroadcastRoundStart( Player currentlyDrawing )
	{
		ResetCanvas();

		ResultsPanel?.Delete();
		ResultsPanel = null;

		if ( Player.Local == currentlyDrawing )
		{
			Header.WordPanel.SetOverride( "Choose a word!" );
			CanvasContainer.AddChild<WordSelection>();
		}
		else
		{
			Header.WordPanel.SetOverride( $"{currentlyDrawing?.Network?.Owner?.DisplayName ?? "someone"} is choosing a word..." );
		}
	}

	[Rpc.Broadcast]
	public void BroadcastRevealAnswer( string answer, bool hadGuesses = true )
	{
		if ( (Player.Local?.IsDrawing ?? false) && hadGuesses || (Player.Local?.HasGuessed ?? false) )
		{
			Sound.Play( "ui.reveal.correct" );
		}
		else
		{
			Sound.Play( "ui.reveal.wrong" );
		}

		CreateChatEntry( "", "The word was: " + answer, "reveal-word" );
		Header?.WordPanel?.SetWord( answer, "The word was:", true );
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	public void BroadcastRevealLetter( int index )
	{
		Header?.WordPanel?.RevealLetter( index );
	}

	[Rpc.Broadcast]
	public void BroadcastDraw( Vector2 point, Color color, int size )
	{
		var callingPlayer = AllPlayers.FirstOrDefault( x => x.Network.OwnerId == Rpc.CallerId );
		if ( !callingPlayer.IsDrawing ) return;
		Draw( point, color, size );
	}

	[Rpc.Broadcast]
	public void BroadcastDraw( List<Vector2> points, Color color, int size )
	{
		var callingPlayer = AllPlayers.FirstOrDefault( x => x.Network.OwnerId == Rpc.CallerId );
		if ( !callingPlayer.IsDrawing ) return;
		Draw( points, color, size );
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	public void BroadcastShowResults()
	{
		ResetCanvas();
		Header.WordPanel.SetOverride( "Game Results" );

		Dictionary<Player, int> leaderboard = new();
		foreach ( var player in AllPlayers )
		{
			leaderboard[player] = player.Score;
		}
		leaderboard = leaderboard.OrderByDescending( x => x.Value ).ToDictionary( x => x.Key, x => x.Value );

		GameResults results = CanvasContainer.AddChild<GameResults>();
		results.Winner = AllPlayers.OrderByDescending( x => x.Score ).FirstOrDefault();
		results.Leaderboard = leaderboard;
		ResultsPanel = results;

		if ( results.Winner == Player.Local )
		{
			Sandbox.Services.Stats.Increment( "games-won", 1 );
			if ( AllPlayers.Count >= 4 )
			{
				Sandbox.Services.Achievements.Unlock( "winner" );
			}
		}
	}

	[Rpc.Broadcast]
	public void BroadcastSubmitGuess( string message )
	{
		var player = AllPlayers.FirstOrDefault( x => x.Network.OwnerId == Rpc.CallerId );
		if ( player is null ) return;

		if ( player.IsDrawing && !RoundEnded )
		{
			if ( player == Player.Local )
			{
				CreateChatEntry( "", "You can't chat while you're drawing!", "is-drawing" );
			}
			return;
		}

		var localPlayer = Player.Local;
		bool shouldSendMessage = true;

		if ( LobbyState != LobbyState.WaitingForPlayers && LobbyState != LobbyState.Results )
		{
			// Strip trailing punctuation so guesses like "dog!" or "cat?" still match
			var trimmedMessage = message.TrimEnd( '.', '!', '?', ',', ';', ':' );
			var parts = trimmedMessage.Split( ' ' );
			var hasPart = parts.Any( x => string.Equals( x.TrimEnd( '.', '!', '?', ',', ';', ':' ), CurrentWord, StringComparison.OrdinalIgnoreCase ) );
			var hasString = trimmedMessage.Contains( CurrentWord, StringComparison.OrdinalIgnoreCase );
			if ( !player.HasGuessed && ((CurrentWord.Contains( ' ' ) && hasString) || hasPart) && !string.IsNullOrEmpty( CurrentWord ) )
			{
				// Capture the word now, before CorrectGuess -> EndRound can clear CurrentWord
				var correctWord = CurrentWord;
				if ( Networking.IsHost && !RoundEnded ) CorrectGuess( player );
				if ( player == localPlayer )
				{
					Sandbox.Services.Stats.Increment( "correct-guesses", 1 );
					Header.WordPanel.SetWord( correctWord, "The word was:", true );
				}
				else if ( localPlayer.IsDrawing ) Sandbox.Services.Stats.Increment( "correct-while-drawing", 1 );
				CreateChatEntry( player.Network.Owner.DisplayName, "guessed correctly!", "guess-correct" );
				Sound.Play( "ui.guess.correct" );
				shouldSendMessage = false;
			}

			if ( player.HasGuessed && !localPlayer.HasGuessed )
			{
				shouldSendMessage = false;
			}
		}

		if ( shouldSendMessage )
		{
			CreateChatEntry( player.Network.Owner.DisplayName, message, player.HasGuessed ? "post-game" : "" );
		}
	}

	[Rpc.Broadcast]
	public void BroadcastChatEntry( string name, string message, string classes = "" )
	{
		CreateChatEntry( name, message, classes );
	}

	int ChatIndex = 0;
	List<(string, string, string)> ChatQueue = new();
	public void CreateChatEntry( string name, string message, string classes = "" )
	{
		if ( !ChatBox.IsValid() )
		{
			ChatQueue.Add( (name, message, classes) );
			return;
		}

		var entry = ChatBox.AddChild<ChatEntry>();
		entry.SetMessage( name, message );
		entry.AddClass( classes );

		if ( ChatBox.ChildrenCount > 250 )
		{
			ChatBox.GetChild( 0 ).Delete();
		}

		Sound.Play( "ui.chat.message" + (ChatIndex + 1) );
		ChatIndex = (ChatIndex + 1) % 2;
	}

	string LogoClass()
	{
		return "";
	}
}