UI/LobbyPanel.cs
using System;
using System.Collections.Generic;
using Sandbox.UI;

/// <summary>
/// 游戏房间/大厅(M5 二级界面):HOST / VS BOTS 点开后进入——host 可调比赛参数、看玩家列表、
/// 点 START 开局;客户端 JOIN 连上后进同一界面,参数与玩家列表由 host 经 LobbyState RPC 推送,
/// 显示"等待房主开始",房主开局后经 MatchStarted RPC 自动进游戏。
/// 主菜单的设置区已移到这里(房间才是设置参数的地方)。
/// 纯 C# Panel + 同目录 LobbyPanel.cs.scss。
/// </summary>
public sealed class LobbyPanel : PanelComponent
{
	Label _status;
	Label[] _settings;       // 0=模式 1=时长 2=人数(host 可点击循环,client 只读展示)
	Label _settingHint;      // "CLICK A SETTING TO CYCLE IT"(仅 host,v0.7.8.0)
	Label[] _playerRows;     // 玩家列表:单列(滚动容器内,容量 = MaxPlayerRows)
	Panel _playersScroll;    // 玩家列表原生滚动容器(overflow-y + 滚轮)
	Label _botsNote;
	Label _start;
	Label _leave;
	Label _join;             // 客户端"JOIN GAME"按钮:对局进行中时显示,点击后 3-2-1 进入(v0.6.4.1)
	Label _countdown;        // 加入倒计时(客户端收到 host 开局指令后 3-2-1,v0.6.4.0)
	bool _matchRunning;      // 客户端视角:host 的对局进行中(JOIN GAME 按钮亮起)
	bool _modeGlowActive;    // 模式行呼吸色激活中(切回客户端身份时恢复 scss 基色一次)

	bool _built;
	bool _retryOpen;         // Panel 未就绪时的开门重试(EnsureWorld 会先于 OnStart 调用 ShowClient/ShowHost)
	bool _isHost;
	bool _shown;
	bool _connectionLost;    // 掉线重连中:状态行锁定提示(否则每帧 RefreshStatus 会覆盖掉)
	TimeSince _sinceListRefresh;

	// 客户端:房主推送的房间信息(LobbyState RPC)
	byte _clientMode;
	int _clientDuration;
	int _clientPlayers;
	List<string> _clientNames = new List<string>();

	const int MaxPlayerRows = 64;   // = MaxPlayers(sbproj),滚动列表装得下满房

	// 排版预览(2026-09-09 用户调样式用):列表显示一批假玩家名。已调完关闭;下次预览再改 true
	// (static readonly 不用 const:const 的 true/false 都会把分支判成不可达代码,报 CS0162)
	static readonly bool ShowSamplePlayers = false;
	static readonly string[] SamplePlayers =
	{
		"FLYBIRD", "KITTEN_92", "小���鸟", "ProEater", "SNACKLORD", "汤圆", "Wobble", "XIAO_LONG",
		"Mochi", "大球王", "pixel_cat", "Doodle", "布丁", "GULP", "泥鳅", "Bubbles",
		"阿翔", "Tofu", "MUNCHER", "云朵",
	};

	protected override void OnStart()
	{
		base.OnStart();
		BuildUi();
		Hide();
	}

	void BuildUi()
	{
		var root = Panel;
		if ( root is null ) return;
		if ( _built && _status.IsValid() && root.ChildrenCount > 0 ) return;

		root.DeleteChildren( true );

		root.Style.Position = PositionMode.Absolute;
		root.Style.Left = 0f;
		root.Style.Top = 0f;
		root.Style.Right = 0f;
		root.Style.Bottom = 0f;
		root.Style.BackgroundColor = new Color( 0.918f, 0.957f, 0.988f, 0.82f );   // 浅蓝磨砂(像素风定稿;旧深灰遮罩在浅色世界上一坨脏灰,用户嫌丑)
		root.Style.PointerEvents = PointerEvents.All;

		UiKit.AttachStyles( root );   // 共享部件样式(.cr-pill 药丸按钮)

		var title = new Label() { Classes = "lp-title" };
		title.Text = "GAME LOBBY";
		root.AddChild( title );

		_status = new Label() { Classes = "lp-status" };
		root.AddChild( _status );

		var settingsHead = new Label() { Classes = "lp-head settings" };
		settingsHead.Text = "MATCH SETTINGS";
		root.AddChild( settingsHead );

		// 设置牌纵排(参考图:右栏三张横牌从上到下),host 可点击循环,client 只读展示
		_settings = new Label[3];
		for ( int i = 0; i < _settings.Length; i++ )
		{
			var l = new Label() { Classes = "lp-setting" };
			l.Style.Top = 330f + i * 88f;   // ★ 设置牌起点 + 行距(精修改这里)
			int index = i;
			l.AddEventListener( "onclick", () => CycleSetting( index ) );
			root.AddChild( l );
			_settings[i] = l;
		}

		// 设置可点提示(v0.7.8.0):仅 host 可见——配合模式行呼吸金光,让玩家知道能改模式
		_settingHint = new Label() { Classes = "lp-setting-hint" };
		_settingHint.Text = "CLICK A SETTING TO CYCLE IT";
		root.AddChild( _settingHint );

		// 道具图例已按用户要求整个移除(2026-09-09)——Q/E 用法开局后 HUD 里还有提示

		var playersHead = new Label() { Classes = "lp-head players" };
		playersHead.Text = "PLAYERS";
		root.AddChild( playersHead );

		// 玩家列表容器底(dobou InventoryHotbar 九宫格,用户指定 2026-09-09):
		// 先于行牌创建压在底层;空列表时也是一个有框的整洁面板,不再是裸留白
		var playersBg = new Panel() { Classes = "lp-players-bg" };
		root.AddChild( playersBg );

		// 玩家列表:单列流内排列装进原生滚动容器(overflow-y + 滚轮,网页式滚动条),
		// 容量提到 64(= MaxPlayers);hotbar 底框只是装饰,滚动发生在它上面的透明容器里
		_playersScroll = new ScrollSoundPanel() { Classes = "lp-players-scroll" };
		root.AddChild( _playersScroll );

		_playerRows = new Label[MaxPlayerRows];
		for ( int i = 0; i < _playerRows.Length; i++ )
		{
			var row = new Label() { Classes = "lp-player" };
			_playersScroll.AddChild( row );
			_playerRows[i] = row;
		}

		_botsNote = new Label() { Classes = "lp-note" };
		_botsNote.Text = "BOTS FILL REMAINING SLOTS AT START";
		root.AddChild( _botsNote );

		// 加入倒计时(客户端):收到 host 开局指令后显示 3-2-1
		_countdown = new Label() { Classes = "lp-countdown" };
		_countdown.Style.Display = DisplayMode.None;
		root.AddChild( _countdown );

		// 三个按钮全走 UiKit 药丸(外观在 Assets/ui/cruikit.scss 的 .cr-pill),这里只管摆放类
		_join = UiKit.PillButton( root, "JOIN GAME", "lp-join", () => OnJoinClicked() );

		_start = UiKit.PillButton( root, "START GAME", "lp-start", () => OnStartClicked() );

		_leave = UiKit.PillButton( root, "LEAVE", "lp-leave", () => OnLeaveClicked() );

		_built = true;
	}

	/// <summary> host 进入房间(HOST / VS BOTS 开房后调用) </summary>
	public void ShowHost()
	{
		_isHost = true;
		Open();
	}

	/// <summary> 客户端连入后进入房间(等待房主开局;列表等 LobbyState 推送) </summary>
	public void ShowClient()
	{
		_isHost = false;
		Open();
	}

	void Open()
	{
		BuildUi();
		if ( !_built )
		{
			_retryOpen = true;   // Panel 未就绪(EnsureWorld 立即调 ShowClient/ShowHost 时),OnUpdate 重试
			return;
		}

		// 面板被销毁但 _built 还残留 true(世界重建/组件禁用后)——重建,别碰 null Panel:
		// 每帧异常会漏引擎"每帧内存栈",几分钟后崩编辑器(实测最贵的一课)
		if ( Panel is null || !Panel.IsValid() )
		{
			_built = false;
			_retryOpen = true;
			return;
		}

		_retryOpen = false;
		Panel.Style.Display = DisplayMode.Flex;
		if ( _shown )
		{
			// 已显示:不重置状态(ReconcileUi 每帧调,JOIN 按钮/倒计时不能被清掉)。
			// 但状态行要恢复——掉线重连成功回到房间时,"CONNECTION LOST" 不能一直挂着
			if ( !_isHost ) RefreshStatus();
			return;
		}
		_shown = true;
		_matchRunning = false;   // 每次进房间重置(MatchStarted 到达后再亮 JOIN 按钮)
		HideJoinCountdown();
		_sinceListRefresh = 1f;   // 首帧立即刷
		RefreshAll();
	}

	/// <summary> 收起(开局 / 回菜单) </summary>
	public void Hide()
	{
		_shown = false;
		_retryOpen = false;
		if ( Panel.IsValid() ) Panel.Style.Display = DisplayMode.None;
	}

	/// <summary> 客户端应用房主推送的房间信息(CircleroyaleGame.OnLobbyStateRemote 转发) </summary>
	public void UpdateFromHost( byte mode, int duration, int players, string[] names )
	{
		_connectionLost = false;   // host 有消息 = 连接活着,解除掉线提示
		_clientMode = mode;
		_clientDuration = duration;
		_clientPlayers = players;
		_clientNames = names is not null ? new List<string>( names ) : new List<string>();

		if ( _shown && !_isHost )
		{
			RefreshSettings();
			RefreshPlayers();
			RefreshButtons();   // 掉线恢复后把常驻按钮摆回来(WAITING/JOIN 文案也靠它刷)
		}
	}

	/// <summary> 显示加入倒计时(CircleroyaleGame 每帧调用) </summary>
	public void ShowJoinCountdown( float secondsLeft )
	{
		if ( _countdown is null || !_countdown.IsValid() ) return;

		_countdown.Style.Display = DisplayMode.Flex;
		_countdown.Text = $"JOINING MATCH IN {MathF.Ceiling( MathF.Max( 0f, secondsLeft ) ):0}";

		// 倒计时期间收起 JOIN 按钮
		if ( _join.IsValid() ) _join.Style.Display = DisplayMode.None;
	}

	/// <summary> 收起加入倒计时 </summary>
	public void HideJoinCountdown()
	{
		if ( _countdown.IsValid() ) _countdown.Style.Display = DisplayMode.None;
	}

	/// <summary> 客户端视角:host 的对局进行中(房间页亮起 JOIN GAME 按钮) </summary>
	public void SetMatchRunning()
	{
		_matchRunning = true;
		RefreshButtons();

		SetText( _status, "MATCH IN PROGRESS — JOIN NOW" );
	}

	/// <summary> 文本防抖(v0.7.1.2):值没变就不赋——Label.Text 每帧重设(哪怕同值)会触发布局
	/// 重算;ReconcileUi 每帧调 ShowClient→RefreshStatus 把状态行反复重刷 = UI 一闪一闪(实测) </summary>
	static void SetText( Label l, string text )
	{
		if ( !l.IsValid() || l.Text == text ) return;
		l.Text = text;
	}

	/// <summary> 掉线自动重连中(CircleroyaleGame.OnDisconnectRecoveryStarted):状态行提示 + 收掉 JOIN 按钮 </summary>
	public void SetConnectionLost()
	{
		_connectionLost = true;
		SetText( _status, "CONNECTION LOST — RECONNECTING ..." );
		if ( _join.IsValid() ) _join.Style.Display = DisplayMode.None;
	}

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

		BuildUi();

		// EnsureWorld 先于 OnStart 调过 ShowClient/ShowHost 且 Panel 未就绪 → 此刻补开
		if ( _retryOpen && !_shown )
		{
			Open();
			return;
		}

		if ( !_shown ) return;

		if ( Panel.IsValid() && Panel.Style.Display != DisplayMode.Flex ) Panel.Style.Display = DisplayMode.Flex;   // 同值守卫(v0.7.8.87)
		Mouse.Visibility = MouseVisibility.Visible;   // 按钮/设置项要能点

		// host:玩家进出、设置变化本地就能看到,0.5s 轮刷;client 列表靠 LobbyState 推送
		if ( _isHost && _sinceListRefresh > 0.5f )
		{
			_sinceListRefresh = 0;
			RefreshPlayers();
			RefreshStatus();
		}

		// 模式行呼吸金光(v0.7.8.0,仅 host):设置行是可点的,用光效引导玩家去改模式。
		// 颜色动画每帧在变(Sin 连续),不碰 Text 不触发布局重算;客户端恢复静态基色
		if ( _settings is not null && _settings[0].IsValid() )
		{
			if ( _isHost )
			{
				var glow = 0.72f + 0.28f * MathF.Sin( Time.Now * 3.2f );
				_settings[0].Style.FontColor = new Color( 1f * glow, 0.76f * glow, 0.30f * glow );
				_modeGlowActive = true;
			}
			else if ( _modeGlowActive )
			{
				_settings[0].Style.FontColor = new Color( 0.29f, 0.23f, 0.39f );   // scss .lp-setting 基色(梅紫 #4a3b63)
				_modeGlowActive = false;
			}
		}
	}

	void RefreshAll()
	{
		RefreshSettings();
		RefreshPlayers();
		RefreshStatus();
		RefreshButtons();
	}

	void RefreshStatus()
	{
		if ( _isHost )
		{
			var names = CircleroyaleGame.Current?.LobbyPlayerNames();
			int n = names?.Count ?? 1;
			SetText( _status, $"ROOM OPEN — {n} PLAYER{( n > 1 ? "S" : "" )} IN LOBBY" );
		}
		else if ( _connectionLost )
		{
			SetText( _status, "CONNECTION LOST — RECONNECTING ..." );
		}
		else
		{
			SetText( _status, _matchRunning ? "MATCH IN PROGRESS — JOIN NOW" : "WAITING FOR HOST TO START ..." );
		}
	}

	void RefreshSettings()
	{
		if ( _settings is null || _settings[0] is null || !_settings[0].IsValid() ) return;

		byte mode;
		float duration;
		int players;

		if ( _isHost )
		{
			mode = (byte)MatchState.PendingMode;
			duration = MatchState.PendingDuration;
			players = MatchState.PendingPlayerTarget;
		}
		else
		{
			mode = _clientMode;
			duration = _clientDuration;
			players = _clientPlayers;
		}

		SetText( _settings[0], (MatchState.Mode)mode switch
		{
			MatchState.Mode.Ffa => "MODE — FREE FOR ALL",
			MatchState.Mode.Team2 => "MODE — TEAM (2 / TEAM)",
			MatchState.Mode.Team3 => "MODE — TEAM (3 / TEAM)",
			MatchState.Mode.Territory => "MODE — TERRITORY WAR",
			_ => "MODE — TEAM (4 / TEAM)",
		} );

		SetText( _settings[1], duration < 60f
			? $"TIME — {duration:0} SEC"
			: $"TIME — {duration / 60f:0} MIN" );

		// 领土模式人数固定 4 队 × 4 人(host Apply 强制 16),菜单选的 PLAYERS 无效——明示避免困惑
		SetText( _settings[2], (MatchState.Mode)mode == MatchState.Mode.Territory ? "PLAYERS — 16 (4 TEAMS)" : $"PLAYERS — {players}" );
	}

	void RefreshPlayers()
	{
		if ( _playerRows is null || _playerRows[0] is null || !_playerRows[0].IsValid() ) return;

		List<string> names;
		if ( _isHost )
		{
			names = CircleroyaleGame.Current?.LobbyPlayerNames() ?? new List<string>();
		}
		else
		{
			names = _clientNames;
		}

		if ( ShowSamplePlayers )
			names = new List<string>( SamplePlayers );   // 排版预览:假名替代真实列表(看完关掉)

		if ( names.Count == 0 ) names = new List<string> { "..." };

		for ( int i = 0; i < _playerRows.Length; i++ )
		{
			var row = _playerRows[i];
			if ( !row.IsValid() ) continue;

			if ( i >= names.Count )
			{
				SetText( row, "" );
				GameHud.SetDisplay( row, DisplayMode.None );   // 空位不画药片条(满屏空条很难看,实测截图)
				continue;
			}

			GameHud.SetDisplay( row, DisplayMode.Flex );
			var tag = i == 0 ? "  (HOST)" : "";
			SetText( row, $"{i + 1}.  {names[i]}{tag}" );
			// 同值守卫:客户端每收到一次 LobbyState 心跳(3s)就全量重刷本列表,
			// 裸写 Display/FontColor = 每 3 秒 64 行布局重算 = 客户端大厅周期性爆闪(实测)
			var c = i == 0 ? new Color( 0.898f, 0.282f, 0.553f ) : new Color( 0.29f, 0.23f, 0.39f, 0.85f );
			if ( row.Style.FontColor != c ) row.Style.FontColor = c;
		}
	}

	void RefreshButtons()
	{
		// host 有 START;客户端第二颗按钮常驻(用户需求 2026-09-09):未开局显示 WAITING FOR HOST
		// (点了不响应),开局亮成 JOIN GAME。LEAVE 两边都有。Display 同值守卫(随 RefreshAll 每 3s/0.5s 全量重刷)
		if ( _start.IsValid() )
			GameHud.SetDisplay( _start, _isHost ? DisplayMode.Flex : DisplayMode.None );

		if ( _join.IsValid() )
		{
			GameHud.SetDisplay( _join, !_isHost ? DisplayMode.Flex : DisplayMode.None );
			SetText( _join, _matchRunning ? "JOIN GAME" : "WAITING FOR HOST" );
		}

		// 设置可点提示(v0.7.8.0):只有 host 能改设置,提示也只给 host
		if ( _settingHint.IsValid() )
			GameHud.SetDisplay( _settingHint, _isHost ? DisplayMode.Flex : DisplayMode.None );
	}

	/// <summary> JOIN GAME 点击(实例方法:热重载可重映射)。WAITING FOR HOST 状态点了不响应 </summary>
	void OnJoinClicked()
	{
		if ( !_matchRunning ) return;
		CircleroyaleGame.Current?.ClientJoinMatch();
	}

	/// <summary> START 点击 </summary>
	void OnStartClicked() => CircleroyaleGame.Current?.StartMatchFromLobby();

	/// <summary> LEAVE 点击 </summary>
	void OnLeaveClicked() => CircleroyaleGame.Current?.ResetToMenu();

	// ---- host 调参(与主菜单时代的循环逻辑一致)----

	void CycleSetting( int index )
	{
		if ( !_isHost ) return;

		GameSfx.UiClick();   // 点了真会改参数才响(client 只读,点了不响)

		switch ( index )
		{
			case 0:
				// 模式 5 档循环(v0.7.8.34):普通赛 → 2 人队 → 3 人队 → 4 人队 → 领土战争
				MatchState.PendingMode = (MatchState.Mode) ( ( (int)MatchState.PendingMode + 1 ) % 5 );
				break;

			case 1:
				MatchState.PendingDuration = NextChoice( GameConfig.MatchDurationChoices, MatchState.PendingDuration );
				break;

			case 2:
				MatchState.PendingPlayerTarget = NextIntChoice( GameConfig.PlayerTargetChoices, MatchState.PendingPlayerTarget );
				break;
		}

		RefreshSettings();
		CircleroyaleGame.Current?.MarkLobbyDirty();   // 让等待中的客户端看到新参数
	}

	static float NextChoice( float[] choices, float current )
	{
		for ( int i = 0; i < choices.Length; i++ )
		{
			if ( MathF.Abs( choices[i] - current ) < 0.5f )
				return choices[( i + 1 ) % choices.Length];
		}
		return choices[0];
	}

	static int NextIntChoice( int[] choices, int current )
	{
		for ( int i = 0; i < choices.Length; i++ )
		{
			if ( choices[i] == current )
				return choices[( i + 1 ) % choices.Length];
		}
		return choices[0];
	}
}