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

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
using UnityEngine.UI;
using UnityEngine.XR;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif

namespace EzAdmin.Client
{
    [Serializable]
    public sealed class EzAdminAnnouncement
    {
        public string title = "Announcement";
        public string text = "";
        public string colour = "#2ECC71";
        public string placement = "banner";
        public string avatar = "";
        public bool sound;
        public float seconds = 8f;
    }

    public enum EzAnnouncePlacement { FromDashboard, Top, TopRight, Center, Bottom }
    public enum EzGamepadClose { East, North, West, South, Start, Select }
    public enum EzXrClose { SecondaryButton, PrimaryButton, Trigger, Grip, Menu }
    public enum EzInputKind { MouseKeyboard, Gamepad, Touch, XR }

    [Serializable]
    public sealed class EzAnnouncementEvent : UnityEvent<string, string> { }

    [DisallowMultipleComponent]
    [AddComponentMenu("EzAdmin/EzAdmin Announcements")]
    public sealed class EzAdminAnnouncer : MonoBehaviour
    {
        [Header("Closing")]
        [Tooltip("Let players close an announcement before its time is up.")]
        [SerializeField] private bool _playersCanClose = true;
        [Tooltip("Key that closes it on PC.")]
        [SerializeField] private KeyCode _closeKey = KeyCode.Escape;
        [Tooltip("Controller button that closes it. East is B on Xbox and Circle on PlayStation.")]
        [SerializeField] private EzGamepadClose _gamepadButton = EzGamepadClose.East;
        [Tooltip("VR controller button that closes it. Secondary is B or Y on Quest.")]
        [SerializeField] private EzXrClose _xrButton = EzXrClose.SecondaryButton;
        [Tooltip("In VR, pointing a controller at the panel and pulling the trigger also closes it.")]
        [SerializeField] private bool _pointAndTrigger = true;

        [Header("Look")]
        [Tooltip("Where it shows on a flat screen. From dashboard uses the choice made in Game Control.")]
        [SerializeField] private EzAnnouncePlacement _placement = EzAnnouncePlacement.FromDashboard;
        [Tooltip("Use the colour picked on the dashboard. Off uses the accent colour below.")]
        [SerializeField] private bool _useDashboardColour = true;
        [SerializeField] private Color _accent = new Color32(0x22, 0xC5, 0x5E, 0xFF);
        [SerializeField] private Color _panel = new Color32(0x11, 0x13, 0x17, 0xF5);
        [SerializeField] private Color _textColour = new Color32(0xED, 0xEF, 0xF2, 0xFF);
        [Tooltip("Leave empty for Unity's built in font.")]
        [SerializeField] private Font _font;
        [SerializeField, Range(0f, 40f)] private float _cornerRadius = 18f;
        [SerializeField, Range(360f, 900f)] private float _width = 620f;
        [Tooltip("Show the picture set on the dashboard beside the heading.")]
        [SerializeField] private bool _showPicture = true;
        [Tooltip("Played when an announcement appears, if sound is switched on in Game Control. Leave empty for EzAdmin's soft two note chime.")]
        [SerializeField] private AudioClip _sound;
        [Tooltip("The longest an announcement stays up, even if the dashboard asks for longer.")]
        [SerializeField, Range(4f, 300f)] private float _longestOnScreen = 30f;

        [Header("VR")]
        [SerializeField, Range(0.8f, 3f)] private float _vrDistance = 1.6f;
        [Tooltip("Height compared with the player's eyes. A little below is easiest to read.")]
        [SerializeField, Range(-0.6f, 0.4f)] private float _vrHeight = -0.08f;
        [Tooltip("How far the player can turn away before the panel drifts back in front of them.")]
        [SerializeField, Range(10f, 90f)] private float _vrFollowAngle = 35f;
        [SerializeField, Range(0.0006f, 0.003f)] private float _vrScale = 0.0012f;

        [Header("Events")]
        public EzAnnouncementEvent onShown = new EzAnnouncementEvent();
        public UnityEvent onClosed = new UnityEvent();

        public static event Action<EzAdminAnnouncement> Shown;
        public static event Action Closed;
        public static bool Hold { get; set; }
        public static EzInputKind LastInput { get; private set; } = EzInputKind.MouseKeyboard;

        private static EzAdminAnnouncer _instance;

        private Canvas _canvas;
        private CanvasGroup _group;
        private RectTransform _card;
        private RectTransform _closeButton;
        private Image _bar;
        private Image _progress;
        private Image _avatar;
        private Text _title;
        private Text _body;
        private GameObject _hint;
        private Image _hintIcon;
        private Text _hintKey;
        private Text _hintWords;
        private GameObject _chip;
        private Text _chipText;
        private bool _vr;
        private bool _showing;
        private float _shownAt;
        private float _length;
        private EzAdminAnnouncement _current;
        private EzAdminAnnouncement _waiting;
        private readonly Dictionary<string, Sprite> _sprites = new Dictionary<string, Sprite>();
        private readonly Dictionary<string, bool> _xrWasDown = new Dictionary<string, bool>();

        private static AudioClip _chime;

        private static AudioClip Chime()
        {
            if (_chime != null) return _chime;
            const int rate = 44100;
            int length = (int)(rate * 0.7f);
            var data = new float[length];
            for (int i = 0; i < length; i++)
            {
                float t = i / (float)rate;
                float first = Mathf.Sin(2f * Mathf.PI * 880f * t) * Mathf.Exp(-t * 9f);
                float late = t - 0.12f;
                float second = late > 0f ? Mathf.Sin(2f * Mathf.PI * 1318.5f * late) * Mathf.Exp(-late * 6f) : 0f;
                float attack = Mathf.Clamp01(t / 0.004f) * Mathf.Clamp01((0.7f - t) / 0.05f);
                data[i] = (first * 0.45f + second * 0.4f) * attack * 0.5f;
            }
            _chime = AudioClip.Create("EzAdminChime", length, 1, rate, false);
            _chime.SetData(data, 0);
            return _chime;
        }

        public static void Show(string message, float seconds = 8f)
        {
            if (string.IsNullOrEmpty(message)) return;
            Show(new EzAdminAnnouncement { text = message, seconds = seconds });
        }

        public static void Show(EzAdminAnnouncement announcement)
        {
            if (announcement == null || string.IsNullOrEmpty(announcement.text)) return;
            Instance().Present(announcement);
        }

        public static void Close()
        {
            if (_instance != null) _instance.Dismiss();
        }

        private static EzAdminAnnouncer Instance()
        {
            if (_instance != null) return _instance;
            _instance = FindObjectOfType<EzAdminAnnouncer>();
            if (_instance != null) return _instance;
            var go = new GameObject("EzAdmin Announcements");
            DontDestroyOnLoad(go);
            _instance = go.AddComponent<EzAdminAnnouncer>();
            return _instance;
        }

        private void Awake()
        {
            if (_instance != null && _instance != this)
            {
                Destroy(this);
                return;
            }
            _instance = this;
            if (Application.isMobilePlatform) LastInput = EzInputKind.Touch;
            else if (Application.isConsolePlatform) LastInput = EzInputKind.Gamepad;
        }

        private void OnDestroy()
        {
            if (_instance == this) _instance = null;
        }

        private void Present(EzAdminAnnouncement a)
        {
            if (Hold)
            {
                _waiting = a;
                EnsureBuilt();
                ShowChip(true);
                return;
            }
            _waiting = null;
            EnsureBuilt();
            ShowChip(false);
            _card.gameObject.SetActive(true);
            _current = a;
            Color accent = _accent;
            if (_useDashboardColour && !string.IsNullOrEmpty(a.colour))
            {
                Color parsed;
                if (ColorUtility.TryParseHtmlString(a.colour, out parsed)) accent = parsed;
            }
            _bar.color = accent;
            _progress.color = new Color(accent.r, accent.g, accent.b, 0.85f);
            _title.color = accent;
            _title.text = string.IsNullOrEmpty(a.title) ? "Announcement" : a.title;
            _body.text = a.text;
            _avatar.transform.parent.gameObject.SetActive(false);
            if (_showPicture && !string.IsNullOrEmpty(a.avatar) && a.avatar.StartsWith("https://"))
                StartCoroutine(LoadPicture(a.avatar));
            _length = Mathf.Clamp(a.seconds <= 0f ? 8f : a.seconds, 4f, _longestOnScreen);
            _shownAt = Time.unscaledTime;
            _showing = true;
            _canvas.gameObject.SetActive(true);
            Place(true);
            RefreshHint();
            StopCoroutine("FadeIn");
            StartCoroutine("FadeIn");
            if (a.sound)
            {
                var cam = Camera.main;
                AudioSource.PlayClipAtPoint(_sound != null ? _sound : Chime(), cam != null ? cam.transform.position : Vector3.zero, 0.6f);
            }
            onShown.Invoke(_title.text, a.text);
            if (Shown != null) Shown(a);
        }

        private void Dismiss()
        {
            if (!_showing) return;
            _showing = false;
            StopCoroutine("FadeIn");
            StartCoroutine(FadeOut());
            onClosed.Invoke();
            if (Closed != null) Closed();
        }

        private IEnumerator FadeIn()
        {
            float t = 0f;
            Vector2 home = _card.anchoredPosition;
            Vector2 from = home + new Vector2(0f, _vr ? -18f : 14f);
            while (t < 1f)
            {
                t += Time.unscaledDeltaTime / 0.28f;
                float e = 1f - Mathf.Pow(1f - Mathf.Clamp01(t), 3f);
                _group.alpha = e;
                _card.anchoredPosition = Vector2.LerpUnclamped(from, home, e);
                yield return null;
            }
            _group.alpha = 1f;
            _card.anchoredPosition = home;
        }

        private IEnumerator FadeOut()
        {
            float t = 0f;
            while (t < 1f && _group != null)
            {
                t += Time.unscaledDeltaTime / 0.2f;
                _group.alpha = 1f - Mathf.Clamp01(t);
                yield return null;
            }
            if (!_showing && _canvas != null) _canvas.gameObject.SetActive(false);
        }

        private void Update()
        {
            TrackInput();
            if (_waiting != null && !Hold)
            {
                var next = _waiting;
                Present(next);
                return;
            }
            if (_chip != null && _chip.activeSelf && ClosePressed())
            {
                bool keep = Hold;
                Hold = false;
                if (_waiting != null) Present(_waiting);
                Hold = keep;
                return;
            }
            if (!_showing) return;
            float left = 1f - (Time.unscaledTime - _shownAt) / _length;
            _progress.fillAmount = Mathf.Clamp01(left);
            if (left <= 0f)
            {
                Dismiss();
                return;
            }
            if (_playersCanClose && (ClosePressed() || ClickedClose())) Dismiss();
            RefreshHint();
        }

        private void LateUpdate()
        {
            if (_vr && (_showing || (_chip != null && _chip.activeSelf))) Place(false);
        }

        private void TrackInput()
        {
            if (XRSettings.isDeviceActive)
            {
                LastInput = EzInputKind.XR;
                return;
            }
#if ENABLE_INPUT_SYSTEM
            var pad = Gamepad.current;
            if (pad != null && (pad.buttonSouth.wasPressedThisFrame || pad.buttonEast.wasPressedThisFrame
                                || pad.buttonNorth.wasPressedThisFrame || pad.buttonWest.wasPressedThisFrame
                                || pad.startButton.wasPressedThisFrame || pad.leftStick.ReadValue().sqrMagnitude > 0.25f))
                LastInput = EzInputKind.Gamepad;
            if (Touchscreen.current != null && Touchscreen.current.primaryTouch.press.wasPressedThisFrame)
                LastInput = EzInputKind.Touch;
            if ((Keyboard.current != null && Keyboard.current.anyKey.wasPressedThisFrame)
                || (Mouse.current != null && (Mouse.current.leftButton.wasPressedThisFrame
                                              || Mouse.current.delta.ReadValue().sqrMagnitude > 4f)))
                LastInput = EzInputKind.MouseKeyboard;
#endif
#if ENABLE_LEGACY_INPUT_MANAGER
            if (Input.touchCount > 0)
            {
                LastInput = EzInputKind.Touch;
                return;
            }
            for (int i = 0; i < 20; i++)
            {
                if (Input.GetKeyDown(KeyCode.JoystickButton0 + i))
                {
                    LastInput = EzInputKind.Gamepad;
                    return;
                }
            }
            if (Input.anyKeyDown || Input.GetMouseButtonDown(0)
                || Mathf.Abs(Input.GetAxisRaw("Mouse X")) + Mathf.Abs(Input.GetAxisRaw("Mouse Y")) > 0.5f)
                LastInput = EzInputKind.MouseKeyboard;
#endif
        }

        private bool ClosePressed()
        {
            if (XRSettings.isDeviceActive) return XrPressed(XrUsage(_xrButton)) || (_pointAndTrigger && PointedAndTriggered());
#if ENABLE_INPUT_SYSTEM
            if (Keyboard.current != null)
            {
                Key key;
                if (Enum.TryParse(_closeKey == KeyCode.Return ? "Enter" : _closeKey.ToString(), out key)
                    && Keyboard.current[key].wasPressedThisFrame) return true;
            }
            var pad = Gamepad.current;
            if (pad != null && PadButton(pad).wasPressedThisFrame) return true;
#endif
#if ENABLE_LEGACY_INPUT_MANAGER
            if (Input.GetKeyDown(_closeKey)) return true;
            if (Input.GetKeyDown(LegacyPadButton(_gamepadButton))) return true;
#endif
            return false;
        }

#if ENABLE_INPUT_SYSTEM
        private UnityEngine.InputSystem.Controls.ButtonControl PadButton(Gamepad pad)
        {
            switch (_gamepadButton)
            {
                case EzGamepadClose.North: return pad.buttonNorth;
                case EzGamepadClose.West: return pad.buttonWest;
                case EzGamepadClose.South: return pad.buttonSouth;
                case EzGamepadClose.Start: return pad.startButton;
                case EzGamepadClose.Select: return pad.selectButton;
                default: return pad.buttonEast;
            }
        }
#endif

        private static KeyCode LegacyPadButton(EzGamepadClose button)
        {
            switch (button)
            {
                case EzGamepadClose.South: return KeyCode.JoystickButton0;
                case EzGamepadClose.West: return KeyCode.JoystickButton2;
                case EzGamepadClose.North: return KeyCode.JoystickButton3;
                case EzGamepadClose.Select: return KeyCode.JoystickButton6;
                case EzGamepadClose.Start: return KeyCode.JoystickButton7;
                default: return KeyCode.JoystickButton1;
            }
        }

        private static InputFeatureUsage<bool> XrUsage(EzXrClose button)
        {
            switch (button)
            {
                case EzXrClose.PrimaryButton: return UnityEngine.XR.CommonUsages.primaryButton;
                case EzXrClose.Trigger: return UnityEngine.XR.CommonUsages.triggerButton;
                case EzXrClose.Grip: return UnityEngine.XR.CommonUsages.gripButton;
                case EzXrClose.Menu: return UnityEngine.XR.CommonUsages.menuButton;
                default: return UnityEngine.XR.CommonUsages.secondaryButton;
            }
        }

        private bool XrPressed(InputFeatureUsage<bool> usage)
        {
            bool pressed = false;
            var devices = new List<UnityEngine.XR.InputDevice>();
            InputDevices.GetDevicesWithCharacteristics(InputDeviceCharacteristics.Controller, devices);
            foreach (var d in devices)
            {
                bool down;
                if (!d.TryGetFeatureValue(usage, out down)) continue;
                string key = usage.name + d.name + d.characteristics;
                bool was;
                _xrWasDown.TryGetValue(key, out was);
                if (down && !was) pressed = true;
                _xrWasDown[key] = down;
            }
            return pressed;
        }

        private bool PointedAndTriggered()
        {
            if (_canvas == null || !_vr) return false;
            var cam = Camera.main;
            Transform space = cam != null && cam.transform.parent != null ? cam.transform.parent : null;
            var devices = new List<UnityEngine.XR.InputDevice>();
            InputDevices.GetDevicesWithCharacteristics(InputDeviceCharacteristics.Controller, devices);
            foreach (var d in devices)
            {
                bool down;
                if (!d.TryGetFeatureValue(UnityEngine.XR.CommonUsages.triggerButton, out down)) continue;
                string key = "point" + d.name + d.characteristics;
                bool was;
                _xrWasDown.TryGetValue(key, out was);
                _xrWasDown[key] = down;
                if (!down || was) continue;
                Vector3 pos;
                Quaternion rot;
                if (!d.TryGetFeatureValue(UnityEngine.XR.CommonUsages.devicePosition, out pos)) continue;
                if (!d.TryGetFeatureValue(UnityEngine.XR.CommonUsages.deviceRotation, out rot)) continue;
                Vector3 origin = space != null ? space.TransformPoint(pos) : pos;
                Vector3 dir = space != null ? space.TransformDirection(rot * Vector3.forward) : rot * Vector3.forward;
                var plane = new Plane(-_canvas.transform.forward, _canvas.transform.position);
                float hit;
                if (!plane.Raycast(new Ray(origin, dir), out hit)) continue;
                Vector3 local = _card.InverseTransformPoint(origin + dir * hit);
                if (_card.rect.Contains(new Vector2(local.x, local.y))) return true;
            }
            return false;
        }

        private bool ClickedClose()
        {
            if (_vr || _closeButton == null) return false;
            Vector2 at;
            bool clicked = false;
            at = Vector2.zero;
#if ENABLE_INPUT_SYSTEM
            if (Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame)
            {
                at = Mouse.current.position.ReadValue();
                clicked = true;
            }
            else if (Touchscreen.current != null && Touchscreen.current.primaryTouch.press.wasPressedThisFrame)
            {
                at = Touchscreen.current.primaryTouch.position.ReadValue();
                clicked = true;
            }
#endif
#if ENABLE_LEGACY_INPUT_MANAGER
            if (!clicked && Input.GetMouseButtonDown(0))
            {
                at = Input.mousePosition;
                clicked = true;
            }
            if (!clicked && Input.touchCount > 0 && Input.GetTouch(0).phase == UnityEngine.TouchPhase.Began)
            {
                at = Input.GetTouch(0).position;
                clicked = true;
            }
#endif
            if (!clicked) return false;
            var padded = _closeButton;
            return RectTransformUtility.RectangleContainsScreenPoint(padded, at, null)
                   || (LastInput == EzInputKind.Touch && RectTransformUtility.RectangleContainsScreenPoint(_card, at, null)
                       && Vector2.Distance(at, RectTransformUtility.WorldToScreenPoint(null, padded.position)) < 90f);
        }

        private void RefreshHint()
        {
            if (_hint == null) return;
            bool show = _playersCanClose && LastInput != EzInputKind.Touch;
            _hint.SetActive(show);
            _closeButton.gameObject.SetActive(_playersCanClose && !_vr);
            if (!show) return;
            string key, words = "Close";
            bool round;
            switch (LastInput)
            {
                case EzInputKind.Gamepad:
                    key = PadLabel(_gamepadButton);
                    round = _gamepadButton != EzGamepadClose.Start && _gamepadButton != EzGamepadClose.Select;
                    break;
                case EzInputKind.XR:
                    key = XrLabel(_xrButton);
                    round = _xrButton == EzXrClose.PrimaryButton || _xrButton == EzXrClose.SecondaryButton;
                    if (_pointAndTrigger && _xrButton != EzXrClose.Trigger) words = "Close, or point and pull the trigger";
                    break;
                default:
                    key = KeyLabel(_closeKey);
                    round = false;
                    break;
            }
            _hintKey.text = key;
            _hintWords.text = words;
            _hintIcon.sprite = Sprite(round ? "disc" : "round");
            var size = ((RectTransform)_hintIcon.transform).sizeDelta;
            float w = round ? 28f : Mathf.Max(34f, key.Length * 10f + 18f);
            ((RectTransform)_hintIcon.transform).sizeDelta = new Vector2(w, size.y);
            var le = _hintIcon.GetComponent<LayoutElement>();
            if (le != null) le.preferredWidth = w;
        }

        private static string PadLabel(EzGamepadClose b)
        {
            switch (b)
            {
                case EzGamepadClose.North: return "Y";
                case EzGamepadClose.West: return "X";
                case EzGamepadClose.South: return "A";
                case EzGamepadClose.Start: return "Start";
                case EzGamepadClose.Select: return "Select";
                default: return "B";
            }
        }

        private static string XrLabel(EzXrClose b)
        {
            switch (b)
            {
                case EzXrClose.PrimaryButton: return "A";
                case EzXrClose.Trigger: return "Trigger";
                case EzXrClose.Grip: return "Grip";
                case EzXrClose.Menu: return "Menu";
                default: return "B";
            }
        }

        private static string KeyLabel(KeyCode k)
        {
            switch (k)
            {
                case KeyCode.Escape: return "Esc";
                case KeyCode.Return: return "Enter";
                case KeyCode.Backspace: return "Backspace";
                case KeyCode.Space: return "Space";
                default: return k.ToString();
            }
        }

        private void ShowChip(bool on)
        {
            if (_chip == null) return;
            _chip.SetActive(on);
            if (!on) return;
            if (!_showing) _canvas.gameObject.SetActive(true);
            _group.alpha = 1f;
            _card.gameObject.SetActive(false);
            string key = LastInput == EzInputKind.XR ? XrLabel(_xrButton) : LastInput == EzInputKind.Gamepad
                ? PadLabel(_gamepadButton) : KeyLabel(_closeKey);
            _chipText.text = LastInput == EzInputKind.Touch ? "New announcement" : "New announcement  ·  " + key + " to read";
        }

        private IEnumerator LoadPicture(string url)
        {
            using (var request = UnityWebRequestTexture.GetTexture(url))
            {
                request.timeout = 8;
                yield return request.SendWebRequest();
                if (request.result != UnityWebRequest.Result.Success || _avatar == null) yield break;
                var texture = DownloadHandlerTexture.GetContent(request);
                if (texture == null) yield break;
                _avatar.sprite = UnityEngine.Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height),
                                                           new Vector2(0.5f, 0.5f));
                _avatar.transform.parent.gameObject.SetActive(true);
            }
        }

        private void Place(bool snap)
        {
            if (!_vr)
            {
                EzAnnouncePlacement where = _placement;
                if (where == EzAnnouncePlacement.FromDashboard && _current != null)
                    where = _current.placement == "toast" ? EzAnnouncePlacement.TopRight
                          : _current.placement == "centre" ? EzAnnouncePlacement.Center : EzAnnouncePlacement.Top;
                if (where == EzAnnouncePlacement.FromDashboard) where = EzAnnouncePlacement.Top;
                Vector2 anchor;
                Vector2 offset;
                switch (where)
                {
                    case EzAnnouncePlacement.TopRight: anchor = new Vector2(1f, 1f); offset = new Vector2(-28f, -28f); break;
                    case EzAnnouncePlacement.Center: anchor = new Vector2(0.5f, 0.5f); offset = Vector2.zero; break;
                    case EzAnnouncePlacement.Bottom: anchor = new Vector2(0.5f, 0f); offset = new Vector2(0f, 36f); break;
                    default: anchor = new Vector2(0.5f, 1f); offset = new Vector2(0f, -28f); break;
                }
                _card.anchorMin = _card.anchorMax = _card.pivot = anchor;
                if (snap) _card.anchoredPosition = offset;
                var chipRt = (RectTransform)_chip.transform;
                chipRt.anchorMin = chipRt.anchorMax = chipRt.pivot = new Vector2(0.5f, 1f);
                chipRt.anchoredPosition = new Vector2(0f, -20f);
                return;
            }
            var cam = Camera.main;
            if (cam == null) return;
            Vector3 flat = cam.transform.forward;
            flat.y = 0f;
            if (flat.sqrMagnitude < 0.001f) flat = cam.transform.up;
            flat.Normalize();
            Vector3 target = cam.transform.position + flat * _vrDistance + Vector3.up * _vrHeight;
            Transform t = _canvas.transform;
            Vector3 toPanel = t.position - cam.transform.position;
            toPanel.y = 0f;
            bool away = Vector3.Angle(flat, toPanel) > _vrFollowAngle;
            if (away) _drifting = true;
            if (snap) t.position = target;
            else if (_drifting)
            {
                t.position = Vector3.Lerp(t.position, target, 1f - Mathf.Exp(-Time.unscaledDeltaTime * 4f));
                if ((t.position - target).sqrMagnitude < 0.0004f) _drifting = false;
            }
            t.rotation = Quaternion.LookRotation(t.position - cam.transform.position, Vector3.up);
        }

        private bool _drifting;

        private void EnsureBuilt()
        {
            bool vr = XRSettings.isDeviceActive;
            if (_canvas != null && vr == _vr) return;
            if (_canvas != null) Destroy(_canvas.gameObject);
            _vr = vr;
            Font font = _font != null ? _font : LoadFont();

            var root = new GameObject("EzAdmin Announcement", typeof(RectTransform));
            root.transform.SetParent(transform, false);
            _canvas = root.AddComponent<Canvas>();
            _group = root.AddComponent<CanvasGroup>();
            _group.blocksRaycasts = false;
            _group.interactable = false;
            if (vr)
            {
                _canvas.renderMode = RenderMode.WorldSpace;
                ((RectTransform)root.transform).sizeDelta = new Vector2(_width + 80f, 420f);
                root.transform.localScale = Vector3.one * _vrScale;
                root.AddComponent<CanvasScaler>().dynamicPixelsPerUnit = 4f;
            }
            else
            {
                _canvas.renderMode = RenderMode.ScreenSpaceOverlay;
                _canvas.sortingOrder = 650;
                var scaler = root.AddComponent<CanvasScaler>();
                scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
                scaler.referenceResolution = new Vector2(1600, 900);
                scaler.matchWidthOrHeight = 0.5f;
            }

            float scale = vr ? 1.7f : 1f;
            var card = Box(root.transform, "Card", _panel, "round");
            _card = (RectTransform)card.transform;
            _card.sizeDelta = new Vector2(_width * (vr ? 1.15f : 1f), 0f);
            var fitter = card.AddComponent<ContentSizeFitter>();
            fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
            var column = card.AddComponent<VerticalLayoutGroup>();
            column.padding = new RectOffset((int)(26 * scale), (int)(22 * scale), (int)(18 * scale), (int)(20 * scale));
            column.spacing = 8f * scale;
            column.childControlWidth = column.childControlHeight = true;
            column.childForceExpandWidth = true;
            column.childForceExpandHeight = false;

            _bar = Box(card.transform, "Accent", Color.white, "round").GetComponent<Image>();
            var barRt = (RectTransform)_bar.transform;
            _bar.gameObject.AddComponent<LayoutElement>().ignoreLayout = true;
            barRt.anchorMin = new Vector2(0f, 0f);
            barRt.anchorMax = new Vector2(0f, 1f);
            barRt.pivot = new Vector2(0f, 0.5f);
            barRt.sizeDelta = new Vector2(6f * scale, -28f * scale);
            barRt.anchoredPosition = new Vector2(10f * scale, 0f);

            var head = new GameObject("Head", typeof(RectTransform));
            head.transform.SetParent(card.transform, false);
            var row = head.AddComponent<HorizontalLayoutGroup>();
            row.spacing = 12f * scale;
            row.childAlignment = TextAnchor.MiddleLeft;
            row.childControlWidth = row.childControlHeight = true;
            row.childForceExpandWidth = false;
            row.childForceExpandHeight = false;

            var frame = Box(head.transform, "Picture", Color.white, "disc");
            frame.AddComponent<Mask>().showMaskGraphic = false;
            var frameLe = frame.AddComponent<LayoutElement>();
            frameLe.preferredWidth = frameLe.preferredHeight = 34f * scale;
            var pic = new GameObject("Image", typeof(RectTransform), typeof(Image));
            pic.transform.SetParent(frame.transform, false);
            Stretch((RectTransform)pic.transform);
            _avatar = pic.GetComponent<Image>();
            _avatar.preserveAspect = true;
            frame.SetActive(false);

            _title = MakeText(head.transform, font, "Announcement", Mathf.RoundToInt(17 * scale), FontStyle.Bold, _accent);
            _title.gameObject.AddComponent<LayoutElement>().flexibleWidth = 1f;

            var close = Box(head.transform, "Close", new Color(1f, 1f, 1f, 0.08f), "disc");
            _closeButton = (RectTransform)close.transform;
            var closeLe = close.AddComponent<LayoutElement>();
            closeLe.preferredWidth = closeLe.preferredHeight = (Application.isMobilePlatform ? 44f : 30f) * scale;
            var cross = Box(close.transform, "X", new Color(_textColour.r, _textColour.g, _textColour.b, 0.85f), "cross");
            var crossRt = (RectTransform)cross.transform;
            crossRt.anchorMin = crossRt.anchorMax = new Vector2(0.5f, 0.5f);
            crossRt.sizeDelta = Vector2.one * 14f * scale;

            _body = MakeText(card.transform, font, "", Mathf.RoundToInt(20 * scale), FontStyle.Normal, _textColour);
            _body.lineSpacing = 1.15f;

            var foot = new GameObject("Foot", typeof(RectTransform));
            foot.transform.SetParent(card.transform, false);
            var footRow = foot.AddComponent<HorizontalLayoutGroup>();
            footRow.spacing = 10f * scale;
            footRow.childAlignment = TextAnchor.MiddleLeft;
            footRow.childControlWidth = footRow.childControlHeight = true;
            footRow.childForceExpandWidth = false;
            footRow.childForceExpandHeight = false;
            _hint = foot;

            var icon = Box(foot.transform, "Key", new Color(1f, 1f, 1f, 0.12f), "round");
            _hintIcon = icon.GetComponent<Image>();
            var iconLe = icon.AddComponent<LayoutElement>();
            iconLe.preferredHeight = 26f * scale;
            iconLe.preferredWidth = 40f * scale;
            _hintKey = MakeText(icon.transform, font, "Esc", Mathf.RoundToInt(12 * scale), FontStyle.Bold, _textColour);
            _hintKey.alignment = TextAnchor.MiddleCenter;
            Stretch((RectTransform)_hintKey.transform);
            _hintWords = MakeText(foot.transform, font, "Close", Mathf.RoundToInt(13 * scale), FontStyle.Normal,
                                  new Color(_textColour.r, _textColour.g, _textColour.b, 0.6f));

            var track = Box(card.transform, "Time", new Color(1f, 1f, 1f, 0.07f), "round");
            track.AddComponent<LayoutElement>().preferredHeight = 4f * scale;
            var fill = Box(track.transform, "Left", Color.white, "round");
            Stretch((RectTransform)fill.transform);
            _progress = fill.GetComponent<Image>();
            _progress.type = Image.Type.Filled;
            _progress.fillMethod = Image.FillMethod.Horizontal;

            var chip = Box(root.transform, "New", _panel, "round");
            var chipRt = (RectTransform)chip.transform;
            if (vr)
            {
                chipRt.anchorMin = chipRt.anchorMax = chipRt.pivot = new Vector2(0.5f, 0f);
                chipRt.anchoredPosition = new Vector2(0f, 10f);
            }
            var chipFit = chip.AddComponent<ContentSizeFitter>();
            chipFit.horizontalFit = chipFit.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
            var chipRow = chip.AddComponent<HorizontalLayoutGroup>();
            chipRow.padding = new RectOffset((int)(16 * scale), (int)(16 * scale), (int)(9 * scale), (int)(9 * scale));
            chipRow.childControlWidth = chipRow.childControlHeight = true;
            _chipText = MakeText(chip.transform, font, "New announcement", Mathf.RoundToInt(14 * scale), FontStyle.Bold, _textColour);
            _chip = chip;
            chip.SetActive(false);

            root.SetActive(false);
        }

        private GameObject Box(Transform parent, string name, Color colour, string sprite)
        {
            var go = new GameObject(name, typeof(RectTransform), typeof(Image));
            go.transform.SetParent(parent, false);
            var image = go.GetComponent<Image>();
            image.sprite = Sprite(sprite);
            image.type = sprite == "round" ? Image.Type.Sliced : Image.Type.Simple;
            image.color = colour;
            image.raycastTarget = false;
            return go;
        }

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

        private Sprite Sprite(string key)
        {
            Sprite cached;
            if (_sprites.TryGetValue(key, out cached) && cached != null) return cached;
            int radius = Mathf.Max(2, Mathf.RoundToInt(_cornerRadius));
            Sprite made;
            switch (key)
            {
                case "disc": made = Make(64, 0, (x, y) => Mathf.Clamp01(31f - Vector2.Distance(new Vector2(x, y), new Vector2(32f, 32f)) + 0.5f)); break;
                case "cross": made = Make(64, 0, Cross); break;
                default:
                    int size = radius * 2 + 8;
                    made = Make(size, radius + 2, (x, y) => Round(x, y, size, radius));
                    break;
            }
            _sprites[key] = made;
            return made;
        }

        private static float Round(float x, float y, int size, float r)
        {
            float cx = Mathf.Clamp(x, r, size - r);
            float cy = Mathf.Clamp(y, r, size - r);
            return Mathf.Clamp01(r - Vector2.Distance(new Vector2(x, y), new Vector2(cx, cy)) + 0.5f);
        }

        private static float Cross(float x, float y)
        {
            Vector2 p = new Vector2(x - 32f, y - 32f);
            if (Mathf.Abs(p.x) > 24f || Mathf.Abs(p.y) > 24f) return 0f;
            float a = Mathf.Abs(p.x - p.y) / 1.4142f;
            float b = Mathf.Abs(p.x + p.y) / 1.4142f;
            return Mathf.Clamp01(4.5f - Mathf.Min(a, b));
        }

        private static Sprite Make(int size, int border, Func<float, float, float> alpha)
        {
            var texture = new Texture2D(size, size, TextureFormat.RGBA32, false)
            {
                hideFlags = HideFlags.HideAndDontSave,
                wrapMode = TextureWrapMode.Clamp,
                filterMode = FilterMode.Bilinear
            };
            var pixels = new Color32[size * size];
            for (int y = 0; y < size; y++)
                for (int x = 0; x < size; x++)
                    pixels[y * size + x] = new Color32(255, 255, 255, (byte)(Mathf.Clamp01(alpha(x + 0.5f, y + 0.5f)) * 255f));
            texture.SetPixels32(pixels);
            texture.Apply(false, true);
            return UnityEngine.Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 100f, 0,
                                             SpriteMeshType.FullRect, new Vector4(border, border, border, border));
        }

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

        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");
        }
    }
}
