using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections.Generic;

/// <summary>
/// Programmatically builds the complete in-game HUD inside the InGame canvas (Client.unity).
///
/// Call BuildNow() from GameManager.OnSceneLoaded BEFORE BindClientScene so GameManager_UI
/// can locate named children.
///
/// Named elements expected by GameManager_UI (Transform.Find / FindTMP):
///   ActionButton         – proxim action button
///   SabotagePanel        – top-of-screen sabotage banner
///     SabotageTimer      – TMP countdown text inside SabotagePanel
///   MeetingPanel              – full-screen voting overlay
///     MeetingHeader           – TMP title (event type)
///     MeetingPhaseLabel       – TMP sub-phase label (DISCUSSION / VOTING / RESULTS / ARRIVAL)
///     MeetingPhaseProgressBar – Image filled, drains with countdown
///     MeetingPhaseCountdown   – TMP "0:24" countdown text
///     MeetingPlayerList       – TMP player list (text fallback)
///     MyVoteIndicator         – TMP "You voted for: X" strip
///     SkipButton              – skip-vote button
///     VoteResultPanel         – sub-panel shown after voting
///       VoteResult            – TMP result text
///   GameEndPanel         – full-screen end-of-game overlay
///     GameEndText        – TMP result text
///   ReconnectOverlay     – full-screen "reconnecting..." overlay
///     ReconnectMessage   – TMP "Reconnecting..." headline
///     ReconnectSubtext   – TMP secondary message
///   KillCooldown         – TMP kill-cooldown label
///   TaskList             – TMP task name list
///   TaskProgress         – TMP global task progress
///   Toast                – TMP toast notification
/// </summary>
public class InGameHUDBuilder : MonoBehaviour
{
    // ── Palette ───────────────────────────────────────────────────────────────
    // Aliases to UITheme so this file's existing local references keep working
    // without a wholesale rename pass. New code should call UITheme.* directly;
    // the C_* names stay as a transitional crutch for in-flight edits.
    static readonly Color C_BG       = UITheme.Bg;
    static readonly Color C_BAR      = UITheme.Surface;
    static readonly Color C_ACCENT   = UITheme.Accent;
    static readonly Color C_GREEN    = UITheme.Success;
    static readonly Color C_RED      = UITheme.Danger;
    static readonly Color C_ORANGE   = UITheme.Warning;
    static readonly Color C_YELLOW   = UITheme.Caution;
    static readonly Color C_MUTED    = UITheme.TextLo;
    static readonly Color C_ROW_A    = UITheme.RowA;
    static readonly Color C_ROW_B    = UITheme.RowB;

    private bool _built;

    // Reference resolution constants kept here as public surface so external
    // callers (other UI scripts that grew to use these) don't break. Forward
    // to UITheme so there's still one source of truth.
    public const float kReferenceWidth   = UITheme.ReferenceWidth;
    public const float kReferenceHeight  = UITheme.ReferenceHeight;
    public const float kMatchWidthHeight = UITheme.MatchWidthOrHeight;

    public void BuildNow() { if (!_built) { _built = true; Build(); } }
    void Start()           { if (!_built) Build(); }

    void Build()
    {
        var rt = GetComponent<RectTransform>();
        if (rt == null) return;

        // Wire up emoji fallback before any TMP component renders. Idempotent
        // and cheap on subsequent calls.
        UITheme.EnsureEmojiFontFallback();

        // Make sure the host canvas has a CanvasScaler with our reference
        // resolution. Without this, RectTransform offsets are interpreted as
        // raw pixels and the layout looks correct only on whichever device
        // the project was last opened against.
        ConfigureCanvasScaler(GetComponentInParent<Canvas>());

        // Apply Screen.safeArea so iOS notches and Android punch-hole cameras
        // don't eat the top/bottom bar. We anchor the host RectTransform to
        // the safe rectangle so all child anchors inherit the inset.
        rt.anchorMin = Vector2.zero;
        rt.anchorMax = Vector2.one;
        rt.offsetMin = Vector2.zero;
        rt.offsetMax = Vector2.zero;
        ApplySafeArea(rt);

        BuildTopBar(rt);
        BuildTaskPanel(rt);
        BuildTaskProgress(rt);
        BuildBottomBar(rt);
        BuildActionButton(rt);
        BuildSabotagePanel(rt);
        BuildMeetingPanel(rt);
        BuildGameEndPanel(rt);
        BuildReconnectOverlay(rt);
        BuildSpectatePanel(rt);
        BuildToast(rt);
    }

    /// <summary>
    /// Apply the project's standard CanvasScaler config to a Canvas. Idempotent -
    /// adds the component if missing, otherwise updates settings in-place.
    /// </summary>
    public static void ConfigureCanvasScaler(Canvas canvas)
    {
        if (canvas == null) return;
        var scaler = canvas.GetComponent<CanvasScaler>()
                  ?? canvas.gameObject.AddComponent<CanvasScaler>();
        scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
        scaler.referenceResolution = new Vector2(kReferenceWidth, kReferenceHeight);
        scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
        scaler.matchWidthOrHeight = kMatchWidthHeight;
        scaler.referencePixelsPerUnit = 100f;
    }

    /// <summary>
    /// Anchors the given RectTransform to the screen's safe rectangle, so all
    /// children inherit the inset. Called once at build time; the safe area
    /// rarely changes after the app launches and a full HUD rebuild on rotation
    /// would be the simpler way to handle that case.
    /// </summary>
    public static void ApplySafeArea(RectTransform rt)
    {
        if (rt == null) return;
        var safe = Screen.safeArea;
        var screenSize = new Vector2(Screen.width, Screen.height);
        if (screenSize.x <= 0 || screenSize.y <= 0) return;

        Vector2 aMin = safe.position;
        Vector2 aMax = safe.position + safe.size;
        aMin.x /= screenSize.x; aMin.y /= screenSize.y;
        aMax.x /= screenSize.x; aMax.y /= screenSize.y;

        rt.anchorMin = aMin;
        rt.anchorMax = aMax;
        rt.offsetMin = Vector2.zero;
        rt.offsetMax = Vector2.zero;
    }

    // ── Top bar ───────────────────────────────────────────────────────────────
    void BuildTopBar(RectTransform parent)
    {
        var bar = Child("_TopBar", parent);
        Anchor(bar, new Vector2(0,1), new Vector2(1,1), new Vector2(0,-90f), Vector2.zero);
        bar.pivot = new Vector2(0.5f,1f);
        Img(bar, C_BAR);

        // Kill cooldown – right half, hidden by default
        var cd = Child("KillCooldown", bar);
        cd.anchorMin = new Vector2(0.5f, 0); cd.anchorMax = Vector2.one;
        cd.offsetMin = new Vector2(0, 6); cd.offsetMax = new Vector2(-12, -6);
        var cdTmp = cd.gameObject.AddComponent<TextMeshProUGUI>();
        cdTmp.text = ""; cdTmp.fontSize = 32; cdTmp.color = C_ORANGE;
        cdTmp.fontStyle = FontStyles.Bold; cdTmp.alignment = TextAlignmentOptions.MidlineRight;
        cd.gameObject.SetActive(false);
    }

    // ── Task panel (right side) ───────────────────────────────────────────────
    void BuildTaskPanel(RectTransform parent)
    {
        var panel = Child("_TaskPanel", parent);
        panel.anchorMin = new Vector2(1,0.35f); panel.anchorMax = new Vector2(1,0.88f);
        panel.pivot = new Vector2(1,0.5f); panel.sizeDelta = new Vector2(280,0);
        Img(panel, new Color(0.05f,0.06f,0.12f,0.85f));

        var hdr = Child("_Hdr", panel);
        Anchor(hdr, new Vector2(0,1), new Vector2(1,1), new Vector2(0,-44), Vector2.zero);
        hdr.pivot = new Vector2(0.5f,1f);
        Img(hdr, new Color(0.2f,0.6f,1f,0.5f));
        TxtChild(hdr,"MY TASKS",26,Color.white,TextAlignmentOptions.Center,bold:true);

        var body = Child("TaskList", panel);
        body.anchorMin = Vector2.zero; body.anchorMax = Vector2.one;
        body.offsetMin = new Vector2(8,8); body.offsetMax = new Vector2(-8,-48);
        var t = body.gameObject.AddComponent<TextMeshProUGUI>();
        t.text = ""; t.fontSize = 22; t.color = Color.white; t.alignment = TextAlignmentOptions.TopLeft;
        t.enableWordWrapping = true;
    }

    // ── Task progress (above bottom bar) ─────────────────────────────────────
    void BuildTaskProgress(RectTransform parent)
    {
        var prog = Child("TaskProgress", parent);
        Anchor(prog, new Vector2(0,0), new Vector2(1,0), new Vector2(-20,120), new Vector2(20,160));
        var t = prog.gameObject.AddComponent<TextMeshProUGUI>();
        t.text = ""; t.fontSize = 28; t.color = Color.white;
        t.fontStyle = FontStyles.Bold; t.alignment = TextAlignmentOptions.Center;
    }

    // ── Bottom bar ────────────────────────────────────────────────────────────
    void BuildBottomBar(RectTransform parent)
    {
        var bar = Child("_BottomBar", parent);
        Anchor(bar, Vector2.zero, new Vector2(1,0), Vector2.zero, new Vector2(0,110));
        bar.pivot = new Vector2(0.5f,0);
        Img(bar, C_BAR);

        var recBtn = Child("_RecenterBtn", bar);
        recBtn.anchorMin = new Vector2(0.82f,0.08f); recBtn.anchorMax = new Vector2(0.98f,0.92f);
        var recBg = Img(recBtn, C_ACCENT);
        var recButton = recBtn.gameObject.AddComponent<Button>();
        recButton.targetGraphic = recBg;
        recButton.onClick.AddListener(() => MapCameraController.Instance?.Recenter());
        TxtChild(recBtn,"⊙",42,Color.white,TextAlignmentOptions.Center,bold:true);
    }

    // ── Action button (DIRECT child so Transform.Find works) ─────────────────
    void BuildActionButton(RectTransform parent)
    {
        var btn = Child("ActionButton", parent);
        Anchor(btn, new Vector2(0.15f,0), new Vector2(0.80f,0), new Vector2(0,12), new Vector2(0,102));
        btn.pivot = new Vector2(0.5f,0);
        var bg = Img(btn, C_GREEN);
        var button = btn.gameObject.AddComponent<Button>();
        button.targetGraphic = bg;

        var txtRt = Child("Text", btn);
        Stretch(txtRt);
        var tmp = txtRt.gameObject.AddComponent<TextMeshProUGUI>();
        tmp.text = "ACTION"; tmp.fontSize = 44; tmp.fontStyle = FontStyles.Bold;
        tmp.color = Color.white; tmp.alignment = TextAlignmentOptions.Center;

        btn.gameObject.SetActive(false);
    }

    // ── Sabotage panel (top strip) ────────────────────────────────────────────
    void BuildSabotagePanel(RectTransform parent)
    {
        var panel = Child("SabotagePanel", parent);
        panel.anchorMin = new Vector2(0,1); panel.anchorMax = new Vector2(1,1);
        panel.pivot = new Vector2(0.5f,1f); panel.sizeDelta = new Vector2(0,80);
        Img(panel, new Color(0.76f,0.19f,0.19f,0.92f));

        var timer = Child("SabotageTimer", panel);
        Stretch(timer);
        var t = timer.gameObject.AddComponent<TextMeshProUGUI>();
        t.text = "SABOTAGE!"; t.fontSize = 48; t.fontStyle = FontStyles.Bold;
        t.color = Color.white; t.alignment = TextAlignmentOptions.Center;

        panel.gameObject.SetActive(false);
    }

    // ── Meeting panel (full screen overlay) ───────────────────────────────────
    //
    // Layout (vertical, top to bottom):
    //   0.90 - 1.00  MeetingHeader     - "EMERGENCY MEETING" / "BODY REPORTED"
    //   0.83 - 0.90  MeetingPhaseLabel - "ARRIVAL" / "DISCUSSION" / "VOTING" / "RESULTS"
    //   0.79 - 0.83  MeetingPhaseProgressBar - thin fill bar that drains as countdown runs
    //   0.74 - 0.79  MeetingPhaseCountdown - "0:24" countdown text
    //   0.18 - 0.74  _MeetingScroll    - vote rows (or VoteResultPanel when resolved)
    //   0.14 - 0.18  MyVoteIndicator   - "You voted for: X" or "Voting hasn't started"
    //   0.04 - 0.14  SkipButton        - skip vote (will move into the row list in P2.7)
    void BuildMeetingPanel(RectTransform parent)
    {
        var panel = Child("MeetingPanel", parent);
        Stretch(panel);
        Img(panel, new Color(0.04f,0.05f,0.14f,0.97f));

        // Title
        var hdr = Child("MeetingHeader", panel);
        Anchor(hdr, new Vector2(0,0.90f), new Vector2(1,1), Vector2.zero, Vector2.zero);
        var hdrTmp = hdr.gameObject.AddComponent<TextMeshProUGUI>();
        hdrTmp.text = "EMERGENCY MEETING"; hdrTmp.fontSize = 52;
        hdrTmp.fontStyle = FontStyles.Bold; hdrTmp.color = C_ORANGE;
        hdrTmp.alignment = TextAlignmentOptions.Center;

        // Sub-phase label (DISCUSSION / VOTING / RESULTS / ARRIVAL)
        var phaseLbl = Child("MeetingPhaseLabel", panel);
        Anchor(phaseLbl, new Vector2(0,0.83f), new Vector2(1,0.90f), Vector2.zero, Vector2.zero);
        var phaseLblTmp = phaseLbl.gameObject.AddComponent<TextMeshProUGUI>();
        phaseLblTmp.text = ""; phaseLblTmp.fontSize = 32;
        phaseLblTmp.fontStyle = FontStyles.Bold; phaseLblTmp.color = C_ACCENT;
        phaseLblTmp.alignment = TextAlignmentOptions.Center;

        // Phase progress bar (drains as countdown elapses)
        var progBg = Child("MeetingPhaseProgressBg", panel);
        Anchor(progBg, new Vector2(0.10f,0.79f), new Vector2(0.90f,0.83f), Vector2.zero, Vector2.zero);
        Img(progBg, new Color(0.10f,0.13f,0.22f,1f));
        var progFill = Child("MeetingPhaseProgressBar", progBg);
        progFill.anchorMin = new Vector2(0,0); progFill.anchorMax = new Vector2(1,1);
        progFill.offsetMin = Vector2.zero; progFill.offsetMax = Vector2.zero;
        var fillImg = progFill.gameObject.AddComponent<Image>();
        fillImg.color = C_ACCENT;
        fillImg.type = Image.Type.Filled;
        fillImg.fillMethod = Image.FillMethod.Horizontal;
        fillImg.fillAmount = 0f;

        // Countdown text under the bar
        var cd = Child("MeetingPhaseCountdown", panel);
        Anchor(cd, new Vector2(0,0.74f), new Vector2(1,0.79f), Vector2.zero, Vector2.zero);
        var cdTmp = cd.gameObject.AddComponent<TextMeshProUGUI>();
        cdTmp.text = ""; cdTmp.fontSize = 28;
        cdTmp.color = new Color(0.8f,0.85f,0.95f);
        cdTmp.alignment = TextAlignmentOptions.Center;

        // Scrollable player vote list
        var scrollArea = Child("_MeetingScroll", panel);
        Anchor(scrollArea, new Vector2(0,0.18f), new Vector2(1,0.74f), Vector2.zero, Vector2.zero);
        BuildMeetingScroll(scrollArea);

        // Text fallback (hidden by default; shown if scroll build fails)
        var fallback = Child("MeetingPlayerList", panel);
        Anchor(fallback, new Vector2(0,0.18f), new Vector2(1,0.74f), new Vector2(8,0), new Vector2(-8,0));
        var fallbackTmp = fallback.gameObject.AddComponent<TextMeshProUGUI>();
        fallbackTmp.text = ""; fallbackTmp.fontSize = 28; fallbackTmp.color = Color.white;
        fallbackTmp.alignment = TextAlignmentOptions.TopLeft;
        fallback.gameObject.SetActive(false); // hidden - scroll list used instead

        // "Your vote: X" indicator strip
        var myVote = Child("MyVoteIndicator", panel);
        Anchor(myVote, new Vector2(0.05f,0.14f), new Vector2(0.95f,0.18f), Vector2.zero, Vector2.zero);
        var myVoteTmp = myVote.gameObject.AddComponent<TextMeshProUGUI>();
        myVoteTmp.text = ""; myVoteTmp.fontSize = 26;
        myVoteTmp.color = new Color(0.73f,0.8f,0.88f);
        myVoteTmp.alignment = TextAlignmentOptions.Center;

        // Skip button (will be merged into the vote list as a row in P2.7)
        var skip = Child("SkipButton", panel);
        Anchor(skip, new Vector2(0.05f,0.04f), new Vector2(0.95f,0.14f), Vector2.zero, Vector2.zero);
        var skipBg = Img(skip, C_MUTED);
        var skipBtn = skip.gameObject.AddComponent<Button>();
        skipBtn.targetGraphic = skipBg;
        skipBtn.onClick.AddListener(() => GameManager.Instance?.CastVote(null));
        TxtChild(skip, "⏭  SKIP", 36, Color.white, TextAlignmentOptions.Center, bold: true);

        // Vote-result sub-panel - now sized to *replace* the scroll area when
        // results arrive, instead of squeezing into the bottom strip alongside
        // skip/my-vote (which caused the previous overlap).
        var resultPanel = Child("VoteResultPanel", panel);
        Anchor(resultPanel, new Vector2(0,0.18f), new Vector2(1,0.74f), Vector2.zero, Vector2.zero);
        Img(resultPanel, new Color(0.05f,0.05f,0.15f,0.95f));
        var resultText = Child("VoteResult", resultPanel);
        Stretch(resultText);
        var rtTmp = resultText.gameObject.AddComponent<TextMeshProUGUI>();
        rtTmp.text = ""; rtTmp.fontSize = 34; rtTmp.color = C_YELLOW;
        rtTmp.fontStyle = FontStyles.Bold; rtTmp.alignment = TextAlignmentOptions.Center;
        resultPanel.gameObject.SetActive(false);

        panel.gameObject.SetActive(false);
    }

    void BuildMeetingScroll(RectTransform rt)
    {
        var sr = rt.gameObject.AddComponent<ScrollRect>();

        var vp = Child("Viewport", rt);
        Stretch(vp);
        vp.gameObject.AddComponent<RectMask2D>();

        var content = Child("MeetingContent", vp);
        content.anchorMin = new Vector2(0,1); content.anchorMax = new Vector2(1,1);
        content.pivot = new Vector2(0.5f,1);
        var vlg = content.gameObject.AddComponent<VerticalLayoutGroup>();
        vlg.childControlWidth = true; vlg.childControlHeight = false;
        vlg.childForceExpandWidth = true; vlg.spacing = 4;
        var csf = content.gameObject.AddComponent<ContentSizeFitter>();
        csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;

        sr.viewport = vp; sr.content = content;
        sr.horizontal = false; sr.vertical = true; sr.scrollSensitivity = 60;
    }

    // ── Game-end panel ────────────────────────────────────────────────────────
    void BuildGameEndPanel(RectTransform parent)
    {
        var panel = Child("GameEndPanel", parent);
        Stretch(panel);
        Img(panel, new Color(0,0,0,0.90f));

        // Result text (upper half)
        var txt = Child("GameEndText", panel);
        Anchor(txt, new Vector2(0,0.4f), new Vector2(1,0.9f), Vector2.zero, Vector2.zero);
        var tmp = txt.gameObject.AddComponent<TextMeshProUGUI>();
        tmp.text = ""; tmp.fontSize = 72; tmp.fontStyle = FontStyles.Bold;
        tmp.color = Color.white; tmp.alignment = TextAlignmentOptions.Center;

        // "Return to lobby" button – owner only (GameManager_UI shows/hides it)
        var retBtn = Child("ReturnToLobbyButton", panel);
        Anchor(retBtn, new Vector2(0.15f,0.22f), new Vector2(0.85f,0.36f), Vector2.zero, Vector2.zero);
        var retBg = Img(retBtn, C_GREEN);
        var retButton = retBtn.gameObject.AddComponent<Button>();
        retButton.targetGraphic = retBg;
        retButton.onClick.AddListener(() => GameManager.Instance?.gameClient?.ReturnToLobby());
        TxtChild(retBtn, "▶  RETURN TO LOBBY", 38, Color.white, TextAlignmentOptions.Center, bold: true);
        retBtn.gameObject.SetActive(false); // shown only for host

        // "Leave" button
        var leaveBtn = Child("LeaveGameButton", panel);
        Anchor(leaveBtn, new Vector2(0.3f,0.08f), new Vector2(0.7f,0.20f), Vector2.zero, Vector2.zero);
        var leaveBg = Img(leaveBtn, C_RED);
        var leaveButton = leaveBtn.gameObject.AddComponent<Button>();
        leaveButton.targetGraphic = leaveBg;
        leaveButton.onClick.AddListener(() => GameManager.Instance?.LeaveLobbyButton());
        TxtChild(leaveBtn, "✕  LEAVE", 34, Color.white, TextAlignmentOptions.Center, bold: true);

        panel.gameObject.SetActive(false);
    }

    // ── Reconnect overlay (visible while the socket is dropping/reconnecting) ─
    void BuildReconnectOverlay(RectTransform parent)
    {
        var panel = Child("ReconnectOverlay", parent);
        Stretch(panel);
        Img(panel, new Color(0.04f,0.05f,0.14f,0.90f));

        var msg = Child("ReconnectMessage", panel);
        Anchor(msg, new Vector2(0,0.45f), new Vector2(1,0.65f), Vector2.zero, Vector2.zero);
        var msgTmp = msg.gameObject.AddComponent<TextMeshProUGUI>();
        msgTmp.text = "Reconnecting..."; msgTmp.fontSize = 56;
        msgTmp.fontStyle = FontStyles.Bold; msgTmp.color = C_YELLOW;
        msgTmp.alignment = TextAlignmentOptions.Center;

        var sub = Child("ReconnectSubtext", panel);
        Anchor(sub, new Vector2(0,0.35f), new Vector2(1,0.45f), Vector2.zero, Vector2.zero);
        var subTmp = sub.gameObject.AddComponent<TextMeshProUGUI>();
        subTmp.text = "Server keeps your slot for up to 60 seconds.";
        subTmp.fontSize = 28; subTmp.color = new Color(0.73f,0.8f,0.88f);
        subTmp.alignment = TextAlignmentOptions.Center;

        panel.gameObject.SetActive(false);
    }

    // ── Spectate panel (visible after death; dim banner so the player still
    // sees the live map but understands they're spectating) ────────────────
    void BuildSpectatePanel(RectTransform parent)
    {
        var panel = Child("SpectatePanel", parent);
        // Top strip only - we don't want to occlude the map. Just enough to
        // communicate "you're dead, watching live."
        panel.anchorMin = new Vector2(0, 1); panel.anchorMax = new Vector2(1, 1);
        panel.pivot = new Vector2(0.5f, 1f); panel.sizeDelta = new Vector2(0, 96);
        Img(panel, new Color(0f, 0f, 0f, 0.65f));

        var label = Child("SpectateLabel", panel);
        Stretch(label);
        var t = label.gameObject.AddComponent<TextMeshProUGUI>();
        UITheme.StyleText(t, UITheme.FontTitle, UITheme.TextHi,
            TextAlignmentOptions.Center, bold: true);
        t.text = "👻  YOU ARE DEAD - SPECTATING";

        var sub = Child("SpectateSub", panel);
        Anchor(sub, new Vector2(0, 0), new Vector2(1, 0.45f), Vector2.zero, Vector2.zero);
        var st = sub.gameObject.AddComponent<TextMeshProUGUI>();
        UITheme.StyleText(st, UITheme.FontSmall, UITheme.TextMid,
            TextAlignmentOptions.Center);
        st.text = "Crew can finish tasks as ghosts. Impostors cannot kill you.";

        panel.gameObject.SetActive(false);
    }

    // ── Toast notification ────────────────────────────────────────────────────
    void BuildToast(RectTransform parent)
    {
        var toast = Child("Toast", parent);
        Anchor(toast, new Vector2(0.05f,0.88f), new Vector2(0.95f,0.94f), Vector2.zero, Vector2.zero);
        Img(toast, new Color(0.1f,0.1f,0.2f,0.92f));
        TxtChild(toast, "", 30, C_YELLOW, TextAlignmentOptions.Center, bold: true);
        toast.gameObject.SetActive(false);
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    RectTransform Child(string name, RectTransform parent)
    {
        var go = new GameObject(name);
        var rt = go.AddComponent<RectTransform>();
        rt.SetParent(parent, false);
        rt.localScale = Vector3.one;
        return rt;
    }

    Image Img(RectTransform rt, Color c)
    {
        var img = rt.gameObject.AddComponent<Image>();
        img.color = c;
        return img;
    }

    void Stretch(RectTransform rt)
    {
        rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
        rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
    }

    // min/max by anchor + absolute offset corners
    void Anchor(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 offsetMin, Vector2 offsetMax)
    {
        rt.anchorMin = aMin; rt.anchorMax = aMax;
        rt.offsetMin = offsetMin; rt.offsetMax = offsetMax;
    }

    TextMeshProUGUI TxtChild(RectTransform parent, string text, float size, Color color,
        TextAlignmentOptions align, bool bold = false)
    {
        var rt = Child("Txt", parent);
        Stretch(rt);
        var tmp = rt.gameObject.AddComponent<TextMeshProUGUI>();
        tmp.text = text; tmp.fontSize = size; tmp.color = color; tmp.alignment = align;
        if (bold) tmp.fontStyle = FontStyles.Bold;
        return tmp;
    }
}

