using FontStashSharp;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Theriapolis.Game.CodexUI.Core;
using Theriapolis.Game.CodexUI.Drag;
using Theriapolis.Core.Rules.Stats;

namespace Theriapolis.Game.CodexUI.Widgets;

/// <summary>
/// One row of the stat-assignment grid. Composite layout:
///   [ Ability name + bonus pill ]  [ Slot ]  [ formula ]  [ Final + mod ]  [ progress bar ]
/// The slot is both a drop target (for pool dice) and a drag source (when
/// filled — drag the value back to the pool, or onto another slot to swap).
/// Click on a filled slot also returns to pool, mirroring the React design.
/// </summary>
public sealed class CodexAbilityRow : CodexWidget
{
    public AbilityId Ability { get; }
    public string LongName { get; set; } = "";
    public int? Assigned { get; set; }
    public int Bonus { get; set; }       // total clade + species mod
    public string BonusSourceText { get; set; } = "";
    public bool IsPrimary { get; set; }

    public System.Action? OnSlotClick { get; set; }
    public System.Action<StatPoolPayload>? OnDragStart { get; set; }

    private readonly CodexAtlas _atlas;
    private readonly DragDropController _drag;

    private readonly SpriteFontBase _nameFont = CodexFonts.DisplaySmall;
    private readonly SpriteFontBase _tagFont  = CodexFonts.MonoTagSmall;
    private readonly SpriteFontBase _slotFont = CodexFonts.DisplayMedium;
    private readonly SpriteFontBase _modFont  = CodexFonts.MonoTag;

    private CodexBonusPill? _bonusPill;

    public CodexAbilityRow(AbilityId ability, CodexAtlas atlas, DragDropController drag)
    {
        Ability = ability;
        _atlas = atlas;
        _drag = drag;
    }

    public CodexBonusPill? BonusPillRef => _bonusPill;

    protected override Point MeasureCore(Point available) => new(available.X, 56);

    protected override void ArrangeCore(Rectangle bounds)
    {
        // Lay out the bonus pill within the name column so we can hover-test it.
        if (Bonus != 0)
        {
            _bonusPill ??= new CodexBonusPill(Bonus, _atlas);
            var pillSize = _bonusPill.Measure(new Point(60, bounds.Height));
            _bonusPill.Arrange(new Rectangle(
                bounds.X + 60,
                bounds.Y + (bounds.Height - pillSize.Y) / 2,
                pillSize.X, pillSize.Y));
        }
        else _bonusPill = null;
    }

    public override void Update(GameTime gt, CodexInput input)
    {
        var slotRect = SlotRect();

        // Dragging a value out of a filled slot.
        if (Assigned is int v && slotRect.Contains(input.MousePosition) && input.LeftJustPressed && !_drag.IsDragging)
        {
            OnDragStart?.Invoke(new StatPoolPayload { Source = "slot", Value = v, Ability = Ability });
            return;
        }
        // Click-to-return (no drag) when a slot is filled and the pool is not currently active.
        if (Assigned is not null && slotRect.Contains(input.MousePosition) && input.LeftJustReleased && !_drag.IsDragging)
        {
            OnSlotClick?.Invoke();
        }
        // Empty slot click — also fires OnSlotClick so the screen can use click-to-place semantics if drag isn't active.
        else if (Assigned is null && slotRect.Contains(input.MousePosition) && input.LeftJustReleased && !_drag.IsDragging)
        {
            OnSlotClick?.Invoke();
        }

        // Register the slot as a drop target.
        if (_drag.IsDragging)
            _drag.RegisterTarget("ability:" + Ability, slotRect);

        _bonusPill?.Update(gt, input);
    }

    public override void Draw(SpriteBatch sb, GameTime gt)
    {
        // Bottom rule
        sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Bottom - 1, Bounds.Width, 1),
                new Color(CodexColors.Rule.R, CodexColors.Rule.G, CodexColors.Rule.B, (byte)100));

        // Name column
        _nameFont.DrawText(sb, Ability.ToString(), new Vector2(Bounds.X, Bounds.Y + 8), CodexColors.Ink);
        string sub = LongName + (IsPrimary ? " · primary" : "");
        _tagFont.DrawText(sb, sub.ToUpperInvariant(), new Vector2(Bounds.X, Bounds.Y + 8 + _nameFont.LineHeight), CodexColors.InkMute);
        _bonusPill?.Draw(sb, gt);

        // Slot
        var slotRect = SlotRect();
        bool dragHover = _drag.IsDragging && slotRect.Contains(_drag.CursorPosition);
        bool filled = Assigned is not null;
        Color slotBorder = dragHover ? CodexColors.Seal : (filled ? CodexColors.InkSoft : CodexColors.InkMute);
        Color slotFill = dragHover
            ? new Color(CodexColors.Seal.R, CodexColors.Seal.G, CodexColors.Seal.B, (byte)28)
            : (filled ? new Color(180, 138, 60, 22) : CodexColors.Bg);
        sb.Draw(_atlas.Pixel, slotRect, slotFill);
        DrawBorder(sb, slotRect, slotBorder, filled ? 1 : 1, dashed: !filled);

        if (filled)
        {
            string label = Assigned!.Value.ToString();
            var s = _slotFont.MeasureString(label);
            _slotFont.DrawText(sb, label, new Vector2(slotRect.X + (slotRect.Width - s.X) / 2f,
                                                      slotRect.Y + (slotRect.Height - _slotFont.LineHeight) / 2f),
                              CodexColors.Ink);
        }
        else
        {
            string dash = "—";
            var s = _slotFont.MeasureString(dash);
            _slotFont.DrawText(sb, dash, new Vector2(slotRect.X + (slotRect.Width - s.X) / 2f,
                                                     slotRect.Y + (slotRect.Height - _slotFont.LineHeight) / 2f),
                              CodexColors.InkMute);
        }

        // Formula column ("base 13" when bonus is non-zero)
        var formulaRect = new Rectangle(slotRect.Right + 14, Bounds.Y, 80, Bounds.Height);
        if (filled && Bonus != 0)
        {
            string text = "base " + Assigned!.Value;
            _modFont.DrawText(sb, text, new Vector2(formulaRect.X, formulaRect.Y + (formulaRect.Height - _modFont.LineHeight) / 2f), CodexColors.InkMute);
        }

        // Final score + modifier
        var finalRect = new Rectangle(formulaRect.Right + 8, Bounds.Y, 90, Bounds.Height);
        if (filled)
        {
            int final = Assigned!.Value + Bonus;
            int mod = AbilityScores.Mod(final);
            string fLabel = final.ToString();
            string mLabel = (mod >= 0 ? "+" : "") + mod;
            var fSize = _slotFont.MeasureString(fLabel);
            _slotFont.DrawText(sb, fLabel, new Vector2(finalRect.X, finalRect.Y + 4), CodexColors.Ink);
            _modFont.DrawText(sb, mLabel, new Vector2(finalRect.X + fSize.X + 6, finalRect.Y + 14),
                              mod >= 0 ? CodexColors.Seal : CodexColors.InkMute);
        }

        // Progress bar
        var barRect = new Rectangle(finalRect.Right + 12, Bounds.Y + 24, Bounds.Right - finalRect.Right - 16, 6);
        sb.Draw(_atlas.Pixel, barRect, CodexColors.Bg);
        DrawBorder(sb, barRect, CodexColors.Rule, 1, dashed: false);
        if (filled)
        {
            int final = System.Math.Clamp(Assigned!.Value + Bonus, 0, 20);
            int fillW = (int)(barRect.Width * (final / 20f));
            sb.Draw(_atlas.Pixel, new Rectangle(barRect.X, barRect.Y, fillW, barRect.Height), CodexColors.Gild);
        }
    }

    private Rectangle SlotRect() => new(Bounds.X + 160, Bounds.Y + 6, 60, Bounds.Height - 12);

    private void DrawBorder(SpriteBatch sb, Rectangle r, Color c, int t, bool dashed)
    {
        if (!dashed)
        {
            sb.Draw(_atlas.Pixel, new Rectangle(r.X, r.Y, r.Width, t), c);
            sb.Draw(_atlas.Pixel, new Rectangle(r.X, r.Bottom - t, r.Width, t), c);
            sb.Draw(_atlas.Pixel, new Rectangle(r.X, r.Y, t, r.Height), c);
            sb.Draw(_atlas.Pixel, new Rectangle(r.Right - t, r.Y, t, r.Height), c);
            return;
        }
        // Dashed: draw 4-px dashes with 3-px gaps
        for (int x = r.X; x < r.Right; x += 7)
        {
            int w = System.Math.Min(4, r.Right - x);
            sb.Draw(_atlas.Pixel, new Rectangle(x, r.Y, w, t), c);
            sb.Draw(_atlas.Pixel, new Rectangle(x, r.Bottom - t, w, t), c);
        }
        for (int y = r.Y; y < r.Bottom; y += 7)
        {
            int h = System.Math.Min(4, r.Bottom - y);
            sb.Draw(_atlas.Pixel, new Rectangle(r.X, y, t, h), c);
            sb.Draw(_atlas.Pixel, new Rectangle(r.Right - t, y, t, h), c);
        }
    }
}

/// <summary>
/// One draggable value in the stat pool. Renders a small parchment tile
/// with the rolled / standard-array number. Drag begins on left-mouse-down;
/// the drag-drop controller then takes over the visual via its ghost
/// callback.
/// </summary>
public sealed class CodexPoolDie : CodexWidget
{
    public int Value { get; }
    public int IndexInPool { get; set; }
    public bool IsSelected { get; set; }
    public System.Action<StatPoolPayload>? OnDragStart { get; set; }
    public System.Action? OnClick { get; set; }

    private readonly CodexAtlas _atlas;
    private readonly DragDropController _drag;
    private readonly SpriteFontBase _font = CodexFonts.DisplayMedium;

    public CodexPoolDie(int value, int indexInPool, CodexAtlas atlas, DragDropController drag)
    {
        Value = value;
        IndexInPool = indexInPool;
        _atlas = atlas;
        _drag = drag;
    }

    protected override Point MeasureCore(Point available) => new(56, 56);
    protected override void ArrangeCore(Rectangle bounds) { }

    public override void Update(GameTime gt, CodexInput input)
    {
        if (input.LeftJustPressed && ContainsPoint(input.MousePosition) && !_drag.IsDragging)
        {
            // Begin drag.
            OnDragStart?.Invoke(new StatPoolPayload { Source = "pool", Value = Value, PoolIdx = IndexInPool });
            _drag.BeginDrag(
                new StatPoolPayload { Source = "pool", Value = Value, PoolIdx = IndexInPool },
                (sb, p) => DrawGhost(sb, p));
        }
        if (input.LeftJustReleased && ContainsPoint(input.MousePosition) && !_drag.IsDragging)
            OnClick?.Invoke();
    }

    public override void Draw(SpriteBatch sb, GameTime gt)
    {
        Color border = IsSelected ? CodexColors.Gild : CodexColors.Rule;
        sb.Draw(_atlas.Pixel, Bounds, CodexColors.Bg);
        sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Y, Bounds.Width, 1), border);
        sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Bottom - 1, Bounds.Width, 1), border);
        sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Y, 1, Bounds.Height), border);
        sb.Draw(_atlas.Pixel, new Rectangle(Bounds.Right - 1, Bounds.Y, 1, Bounds.Height), border);

        string label = Value.ToString();
        var s = _font.MeasureString(label);
        _font.DrawText(sb, label, new Vector2(Bounds.X + (Bounds.Width - s.X) / 2f,
                                              Bounds.Y + (Bounds.Height - _font.LineHeight) / 2f),
                       CodexColors.Ink);
    }

    private void DrawGhost(SpriteBatch sb, Point cursor)
    {
        var rect = new Rectangle(cursor.X - 28, cursor.Y - 28, 56, 56);
        var c = new Color(180, 138, 60, 200);
        sb.Draw(_atlas.Pixel, rect, new Color(CodexColors.Bg.R, CodexColors.Bg.G, CodexColors.Bg.B, (byte)200));
        sb.Draw(_atlas.Pixel, new Rectangle(rect.X, rect.Y, rect.Width, 1), c);
        sb.Draw(_atlas.Pixel, new Rectangle(rect.X, rect.Bottom - 1, rect.Width, 1), c);
        sb.Draw(_atlas.Pixel, new Rectangle(rect.X, rect.Y, 1, rect.Height), c);
        sb.Draw(_atlas.Pixel, new Rectangle(rect.Right - 1, rect.Y, 1, rect.Height), c);

        string label = Value.ToString();
        var s = _font.MeasureString(label);
        _font.DrawText(sb, label, new Vector2(rect.X + (rect.Width - s.X) / 2f,
                                              rect.Y + (rect.Height - _font.LineHeight) / 2f),
                       CodexColors.Ink);
    }
}
