// Copyright (c) 2026 EzAdmin. All rights reserved.
// Licensed under the EzAdmin Client License. See LICENSE-CLIENT.txt.

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace EzAdmin.Client
{

    [DisallowMultipleComponent]
    [ExecuteAlways]
    public sealed class EzAdminLinkScreen : MonoBehaviour
    {
        public enum ScreenMode { Automatic, Flat, WorldSpaceVR }
        public enum Corner { BottomLeft, BottomRight, TopLeft, TopRight }

        [Serializable] public class StringEvent : UnityEvent<string> { }

        [EzAdminGroup("Where it shows")]
        [Tooltip("Automatic: a board in the world when a VR headset is active, a button on screen otherwise.")]
        [SerializeField] private ScreenMode _mode = ScreenMode.Automatic;
        [Tooltip("Off: build no UI. Call OpenLinkPage / OpenStaffPanel from your own buttons.")]
        [SerializeField] private bool _buildUI = true;
        [SerializeField] private Corner _corner = Corner.BottomLeft;
        [Tooltip("Hide the button once this account is linked.")]
        [SerializeField] private bool _hideWhenLinked = true;
        [Tooltip("Show a Staff Panel button to staff: players whose linked Discord account holds a moderation " +
                 "permission. Everyone else never sees it.")]
        [SerializeField] private bool _showStaffButton = true;

        [EzAdminGroup("VR board")]
        [Tooltip("Width of the board in metres.")]
        [SerializeField, Range(0.3f, 2f)] private float _boardWidthMetres = 0.7f;
        [Tooltip("Layers that count as a hand pressing the buttons.")]
        [SerializeField] private LayerMask _handLayers = ~0;

        [EzAdminGroup("Words")]
        [SerializeField] private string _title = "Link your Discord";
        [SerializeField] private string _subtitle = "Your Discord roles count in the game: perks, items and staff tools.";
        [SerializeField] private string _buttonLabel = "Link Discord";

        [EzAdminGroup("Look")]
        [SerializeField] private Color _accent = new Color32(0x58, 0x65, 0xF2, 0xFF);
        [SerializeField] private Color _good = new Color32(0x22, 0xC5, 0x5E, 0xFF);
        [SerializeField] private Color _panel = new Color32(0x12, 0x15, 0x1C, 0xF5);
        [Tooltip("Leave empty for Unity's built-in font.")]
        [SerializeField] private Font _font;

        [EzAdminGroup("Announcements")]
        [Tooltip("Show /announce messages from Discord: a banner at the top of the screen, or a line on the VR board. " +
                 "Turn off if your game shows EzAdminClient.Announcement itself.")]
        [SerializeField] private bool _showAnnouncements = true;
        [Tooltip("How long the banner stays up.")]
        [SerializeField, Range(3f, 30f)] private float _announcementSeconds = 8f;

        [EzAdminGroup("Events - hook your own code here")]
        [SerializeField] private StringEvent _onLinked = new StringEvent();
        [SerializeField] private StringEvent _onStatusChanged = new StringEvent();
        [Tooltip("The website opened (link page or Staff Panel). Pause your game here if you like.")]
        [SerializeField] private StringEvent _onWebsiteOpened = new StringEvent();
        [Tooltip("Turned on when the player is linked, off when they are not.")]
        [SerializeField] private GameObject[] _enableWhenLinked = new GameObject[0];
        [Tooltip("Turned off when the player is linked, on when they are not.")]
        [SerializeField] private GameObject[] _disableWhenLinked = new GameObject[0];

        private int _linkedShown = -1;
        private float _nextLinkCheck;

        private void LateUpdate()
        {
            if (!Application.isPlaying) return;
            if (Time.unscaledTime < _nextLinkCheck) return;
            _nextLinkCheck = Time.unscaledTime + 0.5f;
            int now = EzAdminLink.IsLinked ? 1 : 0;
            if (now == _linkedShown) return;
            _linkedShown = now;
            foreach (var go in _enableWhenLinked)
                if (go != null && go != gameObject && go.activeSelf != (now == 1)) go.SetActive(now == 1);
            foreach (var go in _disableWhenLinked)
                if (go != null && go != gameObject && go.activeSelf != (now == 0)) go.SetActive(now == 0);
        }

        private const float WatchSeconds = 15 * 60;

        private GameObject _root;
        private Image _brandLogo;
        private Image _mainButton;
        private Text _mainLabel;
        private Text _status;
        private Image _dot;
        private GameObject _card;
        private GameObject _staffButton;
        private Coroutine _watching;
        private static Sprite _rounded;
        private const float SpriteRadius = 22f;

        public UnityEvent<string> OnLinked => _onLinked;
        public UnityEvent<string> OnStatusChanged => _onStatusChanged;
        public UnityEvent<string> OnWebsiteOpened => _onWebsiteOpened;
        public bool IsLinked => EzAdminLink.IsLinked;

        public bool IsWorldSpace
        {
            get
            {
                if (_mode == ScreenMode.WorldSpaceVR) return true;
                if (_mode == ScreenMode.Flat) return false;
                return UnityEngine.XR.XRSettings.isDeviceActive;
            }
        }

        internal static int ShowingAnnouncements;

        private void OnEnable()
        {
            if (!Application.isPlaying)
            {
#if UNITY_EDITOR
                UnityEditor.EditorApplication.delayCall += () => { if (this != null && !Application.isPlaying) BuildPreview(); };
#endif
                return;
            }
            if (_showAnnouncements) ShowingAnnouncements++;
            EzAdminLink.Linked += HandleLinked;
            EzAdminClient.ConnectionChanged += HandleConnection;
            EzAdminClient.Announcement += HandleAnnouncement;
            EzAdminClient.FreezeChanged += HandleFreeze;
            EzAdminClient.StaffChanged += HandleStaff;
            EzAdminClient.ServerBrandChanged += ApplyBrandLogo;
        }

        private void OnDisable()
        {
            if (!Application.isPlaying)
            {
                ClearPreview();
                return;
            }
            if (_showAnnouncements) ShowingAnnouncements = Mathf.Max(0, ShowingAnnouncements - 1);
            EzAdminLink.Linked -= HandleLinked;
            EzAdminClient.ConnectionChanged -= HandleConnection;
            EzAdminClient.Announcement -= HandleAnnouncement;
            EzAdminClient.FreezeChanged -= HandleFreeze;
            EzAdminClient.StaffChanged -= HandleStaff;
            EzAdminClient.ServerBrandChanged -= ApplyBrandLogo;
        }

        private void ApplyBrandLogo()
        {
            if (_brandLogo == null) return;
            var server = EzAdminClient.ServerIcon;
            _brandLogo.sprite = server != null ? server : EzAdminBrand.LogoSprite(96);
            _brandLogo.preserveAspect = true;
        }

        private void HandleFreeze(bool frozen)
        {
            HandleAnnouncement(frozen ? "A moderator has frozen you while they take a look. Please wait."
                                      : "You can move again.");
        }

        private GameObject _banner;
        private Text _bannerText;
        private Coroutine _bannerHide;

        private void HandleAnnouncement(string message)
        {
            if (!_showAnnouncements || this == null || string.IsNullOrEmpty(message)) return;
            if (IsWorldSpace)
            {

                SetStatus("Announcement: " + message);
                EzAdminAnnouncer.Show(message, _announcementSeconds);
                return;
            }
            if (_banner == null) BuildBanner();
            _bannerText.text = message;
            _banner.SetActive(true);
            if (_bannerHide != null) StopCoroutine(_bannerHide);
            _bannerHide = StartCoroutine(HideBannerSoon());
        }

        private IEnumerator HideBannerSoon()
        {
            yield return new WaitForSecondsRealtime(_announcementSeconds);
            if (_banner != null) _banner.SetActive(false);
        }

        private void BuildBanner()
        {
            if (_font == null) _font = LoadFont();
            _banner = new GameObject("EzAdmin Announcement", typeof(RectTransform));
            _banner.transform.SetParent(transform, false);
            var canvas = _banner.AddComponent<Canvas>();
            canvas.renderMode = RenderMode.ScreenSpaceOverlay;
            canvas.sortingOrder = 600;
            var scaler = _banner.AddComponent<CanvasScaler>();
            scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
            scaler.referenceResolution = new Vector2(1600, 900);
            scaler.matchWidthOrHeight = 0.5f;

            var card = MakeRounded(_banner.transform, _panel);
            card.name = "Card";
            var rt = (RectTransform)card.transform;
            rt.anchorMin = rt.anchorMax = rt.pivot = new Vector2(0.5f, 1f);
            rt.anchoredPosition = new Vector2(0, -24);
            rt.sizeDelta = new Vector2(720, 0);
            var column = card.AddComponent<VerticalLayoutGroup>();
            column.padding = new RectOffset(20, 20, 14, 16);
            column.spacing = 6;
            column.childControlWidth = column.childControlHeight = true;
            column.childForceExpandWidth = true;
            column.childForceExpandHeight = false;
            card.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
            var shadow = card.AddComponent<Shadow>();
            shadow.effectColor = new Color(0, 0, 0, 0.45f);
            shadow.effectDistance = new Vector2(0, -4);

            var head = new GameObject("Header", typeof(RectTransform));
            head.transform.SetParent(card.transform, false);
            var row = head.AddComponent<HorizontalLayoutGroup>();
            row.spacing = 8;
            row.childAlignment = TextAnchor.MiddleLeft;
            row.childControlWidth = row.childControlHeight = true;
            row.childForceExpandWidth = row.childForceExpandHeight = false;
            var logo = new GameObject("EzAdmin logo", typeof(RectTransform), typeof(Image));
            logo.transform.SetParent(head.transform, false);
            logo.GetComponent<Image>().sprite = EzAdminBrand.LogoSprite(64);
            logo.GetComponent<Image>().raycastTarget = false;
            var size = logo.AddComponent<LayoutElement>();
            size.preferredWidth = size.minWidth = 22;
            size.preferredHeight = size.minHeight = 22;
            MakeText(head.transform, "Announcement", 15, FontStyle.Bold, _good, TextAnchor.MiddleLeft);

            _bannerText = MakeText(card.transform, "", 20, FontStyle.Normal, Color.white, TextAnchor.UpperLeft);
            _banner.SetActive(false);
        }

        private void HandleConnection(string problem)
        {
            SetLight(problem == null ? _good : problem.EndsWith("…") ? new Color32(0xFB, 0xBF, 0x24, 0xFF)
                                                                      : new Color32(0xF8, 0x71, 0x71, 0xFF));
            if (problem == null)
            {
                if (_status != null && _status.gameObject.activeSelf && !EzAdminLink.IsLinked) SetStatus("");
                return;
            }
            if (!problem.EndsWith("…")) SetStatus(problem);
        }

        private void BuildPreview()
        {
            ClearPreview();
            if (!_buildUI || !isActiveAndEnabled) return;
            if (_font == null) _font = LoadFont();
            if (_mode == ScreenMode.Flat) BuildFlat(); else BuildBoard();
            if (_root == null) return;
            foreach (var t in _root.GetComponentsInChildren<Transform>(true))
                t.gameObject.hideFlags = HideFlags.DontSave | HideFlags.NotEditable | HideFlags.HideInHierarchy;
        }

        private void ClearPreview()
        {
            if (_root == null || Application.isPlaying) return;
            DestroyImmediate(_root);
            _root = null;
        }

#if UNITY_EDITOR
        private void OnValidate()
        {
            if (Application.isPlaying) return;
            UnityEditor.EditorApplication.delayCall += () => { if (this != null && !Application.isPlaying) BuildPreview(); };
        }
#endif

        private void Start()
        {
            if (!Application.isPlaying) return;
            for (int i = transform.childCount - 1; i >= 0; i--)
            {
                var child = transform.GetChild(i).gameObject;
                if ((child.hideFlags & HideFlags.DontSave) != 0) Destroy(child);
            }
            _root = null;
            if (_font == null) _font = LoadFont();
            if (_buildUI)
            {
                if (IsWorldSpace) BuildBoard(); else { EnsureEventSystem(); BuildFlat(); }
            }
            if (EzAdminLink.IsLinked && _hideWhenLinked && _buildUI)
            {
                _linkedAndHidden = true;
                ShowCardForStaff();
            }
            StartCoroutine(CheckWhenConnected());
        }

        public void OpenLinkPage()
        {
            if (EzAdminLink.IsLinked)
            {
                SetStatus("You're already linked" + (string.IsNullOrEmpty(EzAdminLink.DiscordName) ? "." : " as " + EzAdminLink.DiscordName + "."));
                return;
            }
            var client = EzAdminClient.Instance;
            if (client == null)
            {
                SetStatus("EzAdmin is not set up in this scene.");
                Debug.LogError("EzAdminLinkScreen: no EzAdminClient in the scene. GameObject › EzAdmin › Link Screen adds one.", this);
                return;
            }
            SetMain("Opening…", _accent);
            client.OpenLinkPage((opened, message) =>
            {
                if (this == null) return;
                if (!opened)
                {
                    SetMain(_buttonLabel, _accent);
                    SetStatus(message);
                    return;
                }
                _onWebsiteOpened.Invoke("link");
                SetMain("Waiting for Discord…", _accent);
                string code = client.LinkCode;
                SetStatus(string.IsNullOrEmpty(code)
                    ? "Finish in your browser, then come back. This updates by itself."
                    : "Finish in your browser, or type this in Discord:\n<size=30><b>/link " + code + "</b></size>\n" +
                      "This updates by itself.");
                if (_watching != null) StopCoroutine(_watching);
                _watching = StartCoroutine(WatchForLink());
            });
        }

        public void OpenStaffPanel()
        {
            var client = EzAdminClient.Instance;
            if (client == null) return;
            SetStatus("Opening the Staff Panel…");
            client.OpenStaffPanel((opened, message) =>
            {
                if (this == null) return;
                if (opened) _onWebsiteOpened.Invoke("panel");
                SetStatus(opened ? "Staff Panel opened in your browser." : message);
            });
        }

        private IEnumerator CheckWhenConnected()
        {
            while (EzAdminClient.Instance != null && !EzAdminClient.Instance.IsConnected)
            {
                yield return new WaitForSecondsRealtime(1f);
            }
            EzAdminLink.Check();
        }

        private IEnumerator WatchForLink()
        {
            float until = Time.realtimeSinceStartup + WatchSeconds;
            while (!EzAdminLink.IsLinked && Time.realtimeSinceStartup < until)
            {
                yield return new WaitForSecondsRealtime(3f);
                EzAdminLink.Check();
            }
            if (!EzAdminLink.IsLinked)
            {
                SetMain(_buttonLabel, _accent);
                SetStatus("That link ran out. Press " + _buttonLabel + " to try again.");
            }
            _watching = null;
        }

        private void OnApplicationFocus(bool focused)
        {

            if (focused && _watching != null) EzAdminLink.Check();
        }

        private void HandleLinked(string discordName)
        {
            SetMain(string.IsNullOrEmpty(discordName) ? "Linked" : "Linked as " + discordName, _good);
            SetLight(_good);
            SetStatus("Your Discord is linked. Roles you have there now count here.");
            _onLinked.Invoke(discordName);
            if (_hideWhenLinked && _buildUI && _root != null)
            {
                StartCoroutine(HideSoon());
            }
        }

        private bool _linkedAndHidden;

        private IEnumerator HideSoon()
        {
            yield return new WaitForSecondsRealtime(4f);
            _linkedAndHidden = true;
            ShowCardForStaff();
        }

        private void HandleStaff(bool staff)
        {
            if (this == null) return;
            if (_staffButton != null) _staffButton.SetActive(_showStaffButton && staff);
            if (_linkedAndHidden) ShowCardForStaff();
        }

        private void ShowCardForStaff()
        {
            bool staff = _showStaffButton && EzAdminClient.IsStaff;
            if (_card == null && _root == null) return;

            if (_mainButton != null) _mainButton.gameObject.SetActive(false);
            if (_status != null) _status.gameObject.SetActive(false);
            if (_card != null) _card.SetActive(staff);
            else if (_root != null) _root.SetActive(staff);
        }

        private void SetMain(string label, Color colour)
        {
            if (_mainLabel != null) _mainLabel.text = label;
            if (_mainButton != null) _mainButton.color = colour;
        }

        private void SetStatus(string message)
        {
            if (_status != null)
            {
                _status.gameObject.SetActive(!string.IsNullOrEmpty(message));
                _status.text = message;
            }
            _onStatusChanged.Invoke(message);
        }

        private void BuildFlat()
        {
            _root = new GameObject("EzAdmin Link Button", typeof(RectTransform));
            _root.transform.SetParent(transform, false);
            var canvas = _root.AddComponent<Canvas>();
            canvas.renderMode = RenderMode.ScreenSpaceOverlay;
            canvas.sortingOrder = 500;
            var scaler = _root.AddComponent<CanvasScaler>();
            scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
            scaler.referenceResolution = new Vector2(1600, 900);
            scaler.matchWidthOrHeight = 0.5f;
            _root.AddComponent<GraphicRaycaster>();

            Vector2 anchor = _corner == Corner.BottomLeft ? new Vector2(0, 0) : _corner == Corner.BottomRight ? new Vector2(1, 0)
                           : _corner == Corner.TopLeft ? new Vector2(0, 1) : new Vector2(1, 1);
            Vector2 inward = new Vector2(anchor.x == 0 ? 1 : -1, anchor.y == 0 ? 1 : -1);

            _card = MakeRounded(_root.transform, _panel, 20);
            _card.name = "EzAdmin card";
            var c = (RectTransform)_card.transform;
            c.anchorMin = c.anchorMax = c.pivot = anchor;
            c.anchoredPosition = new Vector2(24 * inward.x, 24 * inward.y);
            c.sizeDelta = new Vector2(380, 0);
            var column = _card.AddComponent<VerticalLayoutGroup>();
            column.padding = new RectOffset(18, 18, 16, 18);
            column.spacing = 12;
            column.childControlWidth = column.childControlHeight = true;
            column.childForceExpandWidth = true;
            column.childForceExpandHeight = false;
            var fit = _card.AddComponent<ContentSizeFitter>();
            fit.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
            var shadow = _card.AddComponent<Shadow>();
            shadow.effectColor = new Color(0, 0, 0, 0.45f);
            shadow.effectDistance = new Vector2(0, -4);

            var header = new GameObject("Header", typeof(RectTransform));
            header.transform.SetParent(_card.transform, false);
            var row = header.AddComponent<HorizontalLayoutGroup>();
            row.spacing = 12;
            row.childAlignment = TextAnchor.MiddleLeft;
            row.childControlWidth = row.childControlHeight = true;
            row.childForceExpandWidth = row.childForceExpandHeight = false;
            Sized(header, -1, 40);

            var logo = new GameObject("EzAdmin logo", typeof(RectTransform), typeof(Image));
            logo.transform.SetParent(header.transform, false);
            _brandLogo = logo.GetComponent<Image>();
            _brandLogo.sprite = EzAdminBrand.LogoSprite(96);
            _brandLogo.raycastTarget = false;
            Sized(logo, 40, 40);
            ApplyBrandLogo();

            var name = MakeText(header.transform, "EzAdmin", 22, FontStyle.Bold, Color.white, TextAnchor.MiddleLeft);
            name.horizontalOverflow = HorizontalWrapMode.Overflow;
            Sized(name.gameObject, -1, 40, flexible: true);

            var dot = new GameObject("Status light", typeof(RectTransform), typeof(Image));
            dot.transform.SetParent(header.transform, false);
            _dot = dot.GetComponent<Image>();
            Round(_dot, 6);
            _dot.color = new Color32(0xFB, 0xBF, 0x24, 0xFF);
            _dot.raycastTarget = false;
            Sized(dot, 12, 12);

            _status = MakeText(_card.transform, "", 15, FontStyle.Normal, new Color(1, 1, 1, 0.78f), TextAnchor.UpperLeft);
            _status.lineSpacing = 1.1f;
            _status.gameObject.SetActive(false);

            var main = MakeButton(_card.transform, _buttonLabel, _accent, 22, OpenLinkPage, false);
            Sized(main, -1, 58);
            _mainButton = main.GetComponent<Image>();
            _mainLabel = main.GetComponentInChildren<Text>();

            if (_showStaffButton)
            {
                _staffButton = MakeButton(_card.transform, "Staff Panel", new Color(1, 1, 1, 0.08f), 17, OpenStaffPanel, false);
                Sized(_staffButton, -1, 42);
                _staffButton.SetActive(EzAdminClient.IsStaff);
            }

            if (anchor.y == 0)
            {
                header.transform.SetAsFirstSibling();
            }
        }

        private static void Sized(GameObject go, float width, float height, bool flexible = false)
        {
            var element = go.GetComponent<LayoutElement>() ?? go.AddComponent<LayoutElement>();
            if (width >= 0) { element.preferredWidth = width; element.minWidth = width; }
            if (height >= 0) { element.preferredHeight = height; element.minHeight = height; }
            if (flexible) element.flexibleWidth = 1;

            var box = go.GetComponent<BoxCollider>();
            if (box != null && width >= 0 && height >= 0) box.size = new Vector3(width, height, 40f);
        }

        private void SetLight(Color colour)
        {
            if (_dot != null) _dot.color = colour;
        }

        private static void EnsureEventSystem()
        {
            if (EventSystem.current != null || FindObjectOfType<EventSystem>() != null) return;
            var go = new GameObject("EventSystem", typeof(EventSystem));
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
            var module = Type.GetType("UnityEngine.InputSystem.UI.InputSystemUIInputModule, Unity.InputSystem");
            if (module != null) go.AddComponent(module);
#else
            go.AddComponent<StandaloneInputModule>();
#endif
        }

        private void BuildBoard()
        {
            const float width = 700f, inset = 48f;
            _root = new GameObject("EzAdmin Link Board", typeof(RectTransform));
            _root.transform.SetParent(transform, false);
            var canvas = _root.AddComponent<Canvas>();
            canvas.renderMode = RenderMode.WorldSpace;
            canvas.worldCamera = Camera.main;
            _root.AddComponent<CanvasScaler>().dynamicPixelsPerUnit = 4f;
            _root.AddComponent<GraphicRaycaster>();
            var rt = (RectTransform)_root.transform;
            rt.sizeDelta = new Vector2(width, 420f);
            rt.localScale = Vector3.one * (_boardWidthMetres / width);
            var body = _root.AddComponent<Rigidbody>();
            body.isKinematic = true;
            body.useGravity = false;

            var panel = MakeRounded(_root.transform, _panel, 28);
            panel.name = "Board";
            var p = (RectTransform)panel.transform;
            p.anchorMin = p.anchorMax = p.pivot = new Vector2(0.5f, 0.5f);
            p.sizeDelta = new Vector2(width, 0);
            var column = panel.AddComponent<VerticalLayoutGroup>();
            column.padding = new RectOffset((int)inset, (int)inset, 46, 40);
            column.spacing = 22;
            column.childAlignment = TextAnchor.UpperCenter;
            column.childControlWidth = column.childControlHeight = true;
            column.childForceExpandWidth = column.childForceExpandHeight = false;
            panel.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;

            var stripe = MakeRounded(panel.transform, _accent, 3);
            stripe.name = "Accent";
            stripe.AddComponent<LayoutElement>().ignoreLayout = true;
            var s = (RectTransform)stripe.transform;
            s.anchorMin = new Vector2(0, 1);
            s.anchorMax = new Vector2(1, 1);
            s.pivot = new Vector2(0.5f, 1);
            s.offsetMin = new Vector2(inset, -24);
            s.offsetMax = new Vector2(-inset, -18);

            var header = new GameObject("Header", typeof(RectTransform));
            header.transform.SetParent(panel.transform, false);
            var row = header.AddComponent<HorizontalLayoutGroup>();
            row.spacing = 18;
            row.childAlignment = TextAnchor.MiddleCenter;
            row.childControlWidth = row.childControlHeight = true;
            row.childForceExpandWidth = row.childForceExpandHeight = false;
            Sized(header, -1, -1, flexible: true);

            var logo = new GameObject("EzAdmin logo", typeof(RectTransform), typeof(Image));
            logo.transform.SetParent(header.transform, false);
            _brandLogo = logo.GetComponent<Image>();
            _brandLogo.sprite = EzAdminBrand.LogoSprite(128);
            _brandLogo.raycastTarget = false;
            Sized(logo, 64, 64);
            ApplyBrandLogo();
            MakeText(header.transform, _title, 44, FontStyle.Bold, Color.white, TextAnchor.MiddleLeft);

            var sub = MakeText(panel.transform, _subtitle, 24, FontStyle.Normal, new Color(1, 1, 1, 0.7f), TextAnchor.UpperCenter);
            sub.lineSpacing = 1.1f;
            Sized(sub.gameObject, -1, -1, flexible: true);

            var main = MakeButton(panel.transform, _buttonLabel, _accent, 34, OpenLinkPage, true);
            Sized(main, 520, 96);
            _mainButton = main.GetComponent<Image>();
            _mainLabel = main.GetComponentInChildren<Text>();

            _status = MakeText(panel.transform, "", 22, FontStyle.Normal, new Color(1, 1, 1, 0.85f), TextAnchor.UpperCenter);
            _status.lineSpacing = 1.1f;
            Sized(_status.gameObject, -1, -1, flexible: true);
            _status.gameObject.SetActive(false);

            if (_showStaffButton)
            {
                _staffButton = MakeButton(panel.transform, "Staff Panel", new Color(1, 1, 1, 0.1f), 22, OpenStaffPanel, true);
                Sized(_staffButton, 240, 56);
                _staffButton.SetActive(EzAdminClient.IsStaff);
            }
        }

        private GameObject MakeButton(Transform parent, string label, Color colour, int fontSize, Action onPress, bool touch)
        {
            var go = MakeRounded(parent, colour);
            go.name = label;
            var button = go.AddComponent<Button>();
            var colours = button.colors;
            colours.normalColor = Color.white;
            colours.highlightedColor = new Color(0.9f, 0.9f, 0.95f, 1f);
            colours.pressedColor = new Color(0.75f, 0.75f, 0.8f, 1f);
            colours.fadeDuration = 0.08f;
            button.colors = colours;
            var text = MakeText(go.transform, label, fontSize, FontStyle.Bold, Color.white, TextAnchor.MiddleCenter);

            text.resizeTextForBestFit = true;
            text.resizeTextMinSize = Mathf.Max(10, fontSize / 2);
            text.resizeTextMaxSize = fontSize;
            text.verticalOverflow = VerticalWrapMode.Truncate;
            Fill(text.transform, 14);
            if (touch)
            {
                var box = go.AddComponent<BoxCollider>();
                box.isTrigger = true;
                var press = go.AddComponent<EzAdminTouchButton>();
                press.TouchLayers = _handLayers;
                press.OnPressed.AddListener(() => onPress());
                button.onClick.AddListener(press.Press);
            }
            else
            {
                button.onClick.AddListener(() => onPress());
            }
            return go;
        }

        private static void AddLogo(Transform button, float size, float inset)
        {
            var logo = new GameObject("EzAdmin logo", typeof(RectTransform), typeof(Image));
            logo.transform.SetParent(button, false);
            var image = logo.GetComponent<Image>();
            image.sprite = EzAdminBrand.LogoSprite(96);
            image.raycastTarget = false;
            var rt = (RectTransform)logo.transform;
            rt.anchorMin = rt.anchorMax = rt.pivot = new Vector2(0, 0.5f);
            rt.anchoredPosition = new Vector2(inset, 0);
            rt.sizeDelta = new Vector2(size, size);
            var label = button.GetComponentInChildren<Text>();
            if (label != null)
            {
                var lt = (RectTransform)label.transform;
                lt.offsetMin = new Vector2(inset + size + 6, lt.offsetMin.y);
            }
        }

        private GameObject MakeRounded(Transform parent, Color colour, float radius = 18f)
        {
            var go = new GameObject("Shape", typeof(RectTransform), typeof(Image));
            go.transform.SetParent(parent, false);
            var image = go.GetComponent<Image>();
            Round(image, radius);
            image.color = colour;
            return go;
        }

        private static void Round(Image image, float radius)
        {
            image.sprite = RoundedSprite();
            image.type = Image.Type.Sliced;
            image.pixelsPerUnitMultiplier = SpriteRadius / Mathf.Max(1f, radius);
        }

        private Text MakeText(Transform parent, string value, int size, FontStyle style, Color colour, TextAnchor align)
        {
            var go = new GameObject("Text", typeof(RectTransform), typeof(Text));
            go.transform.SetParent(parent, false);
            var text = go.GetComponent<Text>();
            text.font = _font;
            text.text = value;
            text.fontSize = size;
            text.fontStyle = style;
            text.color = colour;
            text.alignment = align;
            text.horizontalOverflow = HorizontalWrapMode.Wrap;
            text.verticalOverflow = VerticalWrapMode.Overflow;
            text.raycastTarget = false;
            return text;
        }

        private static void Fill(Transform target, float padding)
        {
            var rt = (RectTransform)target;
            rt.anchorMin = Vector2.zero;
            rt.anchorMax = Vector2.one;
            rt.offsetMin = new Vector2(padding, 0);
            rt.offsetMax = new Vector2(-padding, 0);
        }

        private static Sprite RoundedSprite()
        {
            if (_rounded != null) return _rounded;
            const int size = 64, radius = (int)SpriteRadius;
            var texture = new Texture2D(size, size, TextureFormat.RGBA32, false) { wrapMode = TextureWrapMode.Clamp };
            var pixels = new Color32[size * size];
            for (int y = 0; y < size; y++)
            {
                for (int x = 0; x < size; x++)
                {
                    float cx = Mathf.Clamp(x + 0.5f, radius, size - radius);
                    float cy = Mathf.Clamp(y + 0.5f, radius, size - radius);
                    float d = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx, cy));
                    byte a = (byte)(Mathf.Clamp01(radius - d + 0.5f) * 255);
                    pixels[y * size + x] = new Color32(255, 255, 255, a);
                }
            }
            texture.SetPixels32(pixels);
            texture.Apply();
            _rounded = Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 100f, 0,
                                     SpriteMeshType.FullRect, new Vector4(radius, radius, radius, radius));
            return _rounded;
        }

        private static Font LoadFont()
        {
            string[] parts = Application.unityVersion.Split('.');
            int major = 0, minor = 0;
            if (parts.Length > 1)
            {
                int.TryParse(parts[0], out major);
                int.TryParse(parts[1], out minor);
            }
            bool legacyRuntime = major > 2022 || (major == 2022 && minor >= 2);
            return Resources.GetBuiltinResource<Font>(legacyRuntime ? "LegacyRuntime.ttf" : "Arial.ttf");
        }

#if UNITY_EDITOR
        [UnityEditor.MenuItem("GameObject/EzAdmin/Link Screen", false, 10)]
        private static void CreateFromMenu()
        {
            if (FindObjectOfType<EzAdminClient>() == null)
            {
                var clientObject = new GameObject("EzAdmin", typeof(EzAdminClient));
                UnityEditor.Undo.RegisterCreatedObjectUndo(clientObject, "Add EzAdmin");
            }
            var go = new GameObject("EzAdmin Link Screen", typeof(EzAdminLinkScreen));
            if (UnityEditor.SceneView.lastActiveSceneView != null)
            {
                go.transform.position = UnityEditor.SceneView.lastActiveSceneView.pivot;
            }
            UnityEditor.Undo.RegisterCreatedObjectUndo(go, "Add EzAdmin Link Screen");
            UnityEditor.Selection.activeGameObject = go;
        }
#endif
    }
}
