using System;
using System.Collections.Generic;
using System.Linq;
using Cthangover.Core.Characters;
using Cthangover.Core.Scenes;
using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming;
using Godot;
namespace Cthangover.FFBattle.UI
{
///
/// Panel that arranges enemy character widgets in a grid layout and handles their
/// lifecycle. Calculates a uniform scale for all enemy widgets based on the panel
/// dimensions and enemy count, centering rows horizontally. Listens to each
/// widget's health change and triggers
/// when HP reaches zero, then removes the dead widget and re-grids the survivors.
/// Raises so can check win conditions.
///
public partial class FFEnemyPanel : ModWidget
{
private const float BASE_W = 180f;
private const float BASE_H = 260f;
private const float MIN_SCALE = 0.25f;
private const float MAX_SCALE = 1.0f;
private const float CELL_PADDING = 10f;
/// All enemy character widgets currently in the panel, including dead ones until removed.
public List Widgets { get; } = new();
/// Raised when a widget's death animation completes and the widget is removed.
public event System.Action OnEnemyDead;
/// Raised when an enemy widget is clicked (used for target selection).
public event System.Action OnEnemyClicked;
protected override void Construct()
{
CthangoverTheme.Apply(this);
}
/// Number of enemy widget instances currently managed (including dead).
public int EnemyCount => Widgets.Count;
///
/// Creates instances for each enemy, scales
/// them uniformly, wires click and health-change handlers, and calls
/// to arrange them.
///
public void Init(Character[] enemies, float scale)
{
foreach (var child in Widgets)
child.QueueFree();
Widgets.Clear();
for (int i = 0; i < enemies.Length; i++)
{
var widget = (FFCharacterWidget)TscnScenes.LoadAndBuild("scenes/ff_character_widget.tscn");
widget.EnsureConstructed();
widget.Scale = new Vector2(scale, scale);
widget.Init(enemies[i]);
widget.IsPlayer = false;
widget.MouseFilter = MouseFilterEnum.Stop;
widget.GuiInput += (evt) =>
{
if (evt is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left)
OnEnemyClicked?.Invoke(widget);
};
AddChild(widget);
Widgets.Add(widget);
enemies[i].Attributes.Health.OnChange += (value, baseValue) =>
{
if (value <= 0 && !widget.IsDead)
HandleDeath(widget);
};
}
GridLayout();
}
///
/// Computes a uniform scale factor so all enemies fit within
/// with padding. Uses
/// to determine layout, then chooses
/// the tighter of width-based and height-based scales, clamped to
/// [MIN_SCALE, MAX_SCALE].
///
public static float CalculateScale(int enemyCount, Vector2 panelSize)
{
var (cols, rows) = GetGridDimensions(enemyCount);
float availW = panelSize.X - CELL_PADDING * 2;
float availH = panelSize.Y - CELL_PADDING * 2;
float scaleByW = (availW - (cols - 1) * CELL_PADDING) / (cols * BASE_W);
float scaleByH = (availH - (rows - 1) * CELL_PADDING) / (rows * BASE_H);
float scale = Mathf.Min(scaleByW, scaleByH);
scale = Mathf.Clamp(scale, MIN_SCALE, MAX_SCALE);
return scale;
}
///
/// Determines grid dimensions for a given count: 1–4 → single row,
/// 5–10 → 2 rows, 11–16 → 3 rows, 17+ → 4 rows.
///
public static (int cols, int rows) GetGridDimensions(int count)
{
if (count <= 4) return (count, 1);
if (count <= 10) return ((count + 1) / 2, 2);
if (count <= 16) return ((count + 2) / 3, 3);
return ((count + 3) / 4, 4);
}
/// Repositions all widgets into the calculated grid, with optional 0.3s cubic tween animation.
public void GridLayout(bool animate = true)
{
var (cols, rows) = GetGridDimensions(Widgets.Count);
for (int i = 0; i < Widgets.Count; i++)
{
var widget = Widgets[i];
if (widget == null)
continue;
int row = i / cols;
int col = i % cols;
var totalColsInRow = row < rows - 1 ? cols : (Widgets.Count - row * cols);
if (totalColsInRow <= 0)
totalColsInRow = 1;
var effW = widget.Size.X * widget.Scale.X;
var effH = widget.Size.Y * widget.Scale.Y;
float startX = (Size.X - (totalColsInRow * effW + (totalColsInRow - 1) * CELL_PADDING)) / 2f;
float startY = CELL_PADDING;
var targetPos = new Vector2(
startX + col * (effW + CELL_PADDING),
startY + row * (effH + CELL_PADDING)
);
if (animate)
{
var tween = CreateTween();
tween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
tween.TweenProperty(widget, "position", targetPos, 0.3f);
}
else
{
widget.Position = targetPos;
}
}
}
private void HandleDeath(FFCharacterWidget widget)
{
if (widget.IsDead)
return;
widget.PlayDeathAnimation(() =>
{
RemoveWidget(widget);
GridLayout(true);
OnEnemyDead?.Invoke(widget);
});
}
/// Removes a widget from the panel, frees it, and re-grids the remaining widgets.
public void RemoveWidget(FFCharacterWidget widget)
{
Widgets.Remove(widget);
widget.QueueFree();
GridLayout();
}
/// Returns true if any widget in the panel is not marked dead.
public bool HasAlive()
{
foreach (var w in Widgets)
if (!w.IsDead)
return true;
return false;
}
/// Frees all widget instances and clears the widget list. Called on battle cleanup.
public void ClearAll()
{
foreach (var widget in Widgets.ToList())
{
if (GodotObject.IsInstanceValid(widget))
widget.QueueFree();
}
Widgets.Clear();
GridLayout(false);
}
}
}