// 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 System.Reflection;
using PlayFab;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using UnityEngine.XR;

namespace EzAdmin.Client
{
    [DisallowMultipleComponent]
    public sealed class EzAdminLoadingScreen : MonoBehaviour
    {
        public enum ScreenMode { Automatic, VR, Flat }

        [EzAdminGroup("Your game")]
        [Tooltip("Shown big in the middle. Leave empty to use the product name from Player Settings.")]
        [SerializeField] private string _gameName = "";
        [Tooltip("Your game's logo. Leave empty to show the EzAdmin mark.")]
        [SerializeField] private Sprite _gameLogo = null;
        [Tooltip("A line under the name, for example Season 3 or a version.")]
        [SerializeField] private string _subtitle = "";
        [Tooltip("The scene that opens once loading is done.")]
        [SerializeField] private string _nextScene = "";
        [Tooltip("Shown one at a time while loading. Leave empty for none.")]
        [SerializeField, TextArea(1, 3)] private string[] _tips = new string[0];
        [SerializeField, Range(2f, 15f)] private float _secondsPerTip = 5f;

        [EzAdminGroup("Colours")]
        [Tooltip("The progress bar, the steps, the button, the edge of the card and the floor glow in VR.")]
        [SerializeField] private Color _accent = new Color32(0x22, 0xC5, 0x5E, 0xFF);
        [Tooltip("Every word on the screen.")]
        [SerializeField] private Color _text = new Color32(0xF2, 0xF4, 0xF8, 0xFF);
        [Tooltip("The card in the middle.")]
        [SerializeField] private Color _panel = new Color32(0x10, 0x13, 0x1A, 0xF7);
        [Tooltip("Behind the card.")]
        [SerializeField] private Color _background = new Color32(0x06, 0x08, 0x0C, 0xFF);
        [Tooltip("A picture behind everything. Optional.")]
        [SerializeField] private Sprite _backgroundImage = null;
        [Tooltip("Paint the camera background in the background colour while this screen is up.")]
        [SerializeField] private bool _paintCamera = true;

        [EzAdminGroup("What it shows")]
        [Tooltip("The progress bar with Step 2 of 4.")]
        [SerializeField] private bool _showProgress = true;
        [Tooltip("The percentage next to the bar.")]
        [SerializeField] private bool _showPercent = true;
        [Tooltip("The four steps: Connecting, Signing in, Loading items, Loading the world.")]
        [SerializeField] private bool _showSteps = true;
        [Tooltip("The spinner next to the status.")]
        [SerializeField] private bool _showSpinner = true;
        [Tooltip("The logo breathes gently while loading.")]
        [SerializeField] private bool _animateLogo = true;
        [Tooltip("A small Secured by EzAdmin mark under the card.")]
        [SerializeField] private bool _showSecuredBy = true;

        [EzAdminGroup("How it behaves")]
        [Tooltip("Wait until the player is signed in to PlayFab before opening the game.")]
        [SerializeField] private bool _waitForSignIn = true;
        [Tooltip("Wait until the player's items have loaded, so nothing pops in after the world appears.")]
        [SerializeField] private bool _waitForItems = true;
        [Tooltip("Give up waiting for sign-in after this many seconds.")]
        [SerializeField, Range(5f, 90f)] private float _signInTimeout = 30f;
        [Tooltip("When sign-in fails, open the game anyway instead of showing an error.")]
        [SerializeField] private bool _continueIfSignInFails = false;
        [Tooltip("Show a Try again button when sign-in fails.")]
        [SerializeField] private bool _allowRetry = true;
        [Tooltip("In VR, pressing the trigger or A also tries again. The screen says so under the button.")]
        [SerializeField] private bool _retryWithTrigger = true;
        [Tooltip("Try again by itself after this many seconds. 0 waits for the player.")]
        [SerializeField, Range(0f, 60f)] private float _autoRetrySeconds = 0f;
        [Tooltip("Keep the screen up at least this long so it never flashes.")]
        [SerializeField, Range(0f, 8f)] private float _minimumSeconds = 2f;
        [Tooltip("Each step stays on screen at least this long, so players can read it.")]
        [SerializeField, Range(0f, 2f)] private float _stepSeconds = 0.6f;
        [Tooltip("How long the screen takes to fade in and out.")]
        [SerializeField, Range(0.1f, 2f)] private float _fadeSeconds = 0.45f;
        [Tooltip("Optional sound when the game is ready.")]
        [SerializeField] private AudioClip _readySound = null;
        [Tooltip("Optional sound when sign-in fails.")]
        [SerializeField] private AudioClip _errorSound = null;

        [EzAdminGroup("Where it shows")]
        [Tooltip("Automatic: floating in front of the player on a headset, full screen on PC, consoles and phones.")]
        [SerializeField] private ScreenMode _mode = ScreenMode.Automatic;

        [EzAdminGroup("VR")]
        [Tooltip("How far in front of the player the screen floats, in metres.")]
        [SerializeField, Range(1f, 5f)] private float _vrDistance = 2.3f;
        [Tooltip("How wide the screen is, in metres.")]
        [SerializeField, Range(0.8f, 4f)] private float _vrWidth = 2.0f;
        [Tooltip("Bring the screen back in front of the player when they look away.")]
        [SerializeField] private bool _followHead = true;
        [Tooltip("How far the player can look away before the screen comes back, in degrees.")]
        [SerializeField, Range(10f, 60f)] private float _followAngle = 30f;
        [Tooltip("A soft glow on the floor under the player, so they are not standing in a void.")]
        [SerializeField] private bool _vrSpace = true;
        [Tooltip("A short buzz in the controllers when the game is ready.")]
        [SerializeField] private bool _haptics = true;

        [EzAdminGroup("Words")]
        [SerializeField] private string _stepConnect = "Connecting";
        [SerializeField] private string _stepSignIn = "Signing in";
        [SerializeField] private string _stepItems = "Loading items";
        [SerializeField] private string _stepWorld = "Loading the world";
        [SerializeField] private string _welcome = "Welcome back";
        [SerializeField] private string _ready = "Ready";
        [SerializeField] private string _failedTitle = "Could not sign in";
        [SerializeField] private string _retryLabel = "Try again";
        [SerializeField] private string _retryHint = "or press the trigger";
        [SerializeField] private string _tipLabel = "TIP";
        [Tooltip("Optional. A TextMeshPro font asset. Used when the design is built.")]
        [SerializeField] private UnityEngine.Object _fontAsset = null;
        [Tooltip("Optional. Only used when TextMeshPro is not in the project.")]
        [SerializeField] private Font _font = null;

        [EzAdminGroup("Events")]
        [SerializeField] private UnityEvent _onSignedIn = new UnityEvent();
        [SerializeField] private UnityEvent _onReady = new UnityEvent();
        [SerializeField] private StringEvent _onFailed = new StringEvent();

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

        [Serializable]
        private sealed class Parts
        {
            public Canvas canvas;
            public CanvasGroup group;
            public CanvasGroup content;
            public GameObject backdrop;
            public GameObject backdropArt;
            public GameObject cardArt;
            public GameObject floor;
            public GameObject progress;
            public GameObject steps;
            public GameObject footer;
            public Image bar;
            public RectTransform barFill;
            public Graphic percent;
            public Graphic stepCount;
            public Graphic status;
            public Graphic title;
            public Graphic subtitle;
            public Graphic tip;
            public Graphic tipLabel;
            public Graphic error;
            public Graphic retryLabel;
            public Graphic retryHint;
            public GameObject tipRow;
            public Button retry;
            public RectTransform spinner;
            public RectTransform logoFrame;
            public Image logo;
            public Image backdropImage;
            public Image cardImage;
            public Image[] nodes;
            public Image[] marks;
            public Image[] links;
            public Graphic[] stepTexts;
            public Graphic[] tintGraphics;
            public int[] tintRoles;
            public float[] tintAlphas;
        }

        [SerializeField, HideInInspector] private Parts _parts = new Parts();

        public static Func<string, Sprite, Sprite> SpriteStore;

        private enum Role { Accent, Text, Panel, Background, Lift }

        private sealed class Tint
        {
            public Graphic Graphic;
            public Role Role;
            public float Alpha;
        }

        private sealed class Words
        {
            public Graphic Graphic;
            private PropertyInfo _textProp;
            private PropertyInfo _styleProp;
            private object _bold, _normal;

            public GameObject gameObject => Graphic != null ? Graphic.gameObject : null;

            public static Words Wrap(Graphic g)
            {
                if (g == null) return null;
                var w = new Words { Graphic = g };
                w._textProp = g.GetType().GetProperty("text", BindingFlags.Public | BindingFlags.Instance);
                w._styleProp = g.GetType().GetProperty("fontStyle", BindingFlags.Public | BindingFlags.Instance);
                if (w._styleProp != null && w._styleProp.PropertyType.IsEnum)
                {
                    var type = w._styleProp.PropertyType;
                    w._bold = Enum.Parse(type, "Bold");
                    w._normal = Enum.Parse(type, "Normal");
                }
                return w;
            }

            public string text
            {
                get { return _textProp != null ? _textProp.GetValue(Graphic) as string : ""; }
                set { if (_textProp != null && Graphic != null) _textProp.SetValue(Graphic, value ?? ""); }
            }

            public Color color
            {
                get { return Graphic != null ? Graphic.color : Color.white; }
                set { if (Graphic != null) Graphic.color = value; }
            }

            public bool bold
            {
                set { if (_styleProp != null && Graphic != null) _styleProp.SetValue(Graphic, value ? _bold : _normal); }
            }
        }

        private const float CardW = 1100f;
        private const float CardH = 820f;
        private const float BarW = 780f;

        private readonly List<CanvasGroup> _groups = new List<CanvasGroup>();
        private readonly List<Tint> _tints = new List<Tint>();
        private Canvas _canvas;
        private CanvasGroup _group;
        private CanvasGroup _content;
        private Words _percent;
        private Words _stepCount;
        private Words _status;
        private Words _tip;
        private Words _error;
        private Words[] _stepTexts;
        private int[] _stepState;
        private float[] _popAt;
        private Font _usedFont;
        private float _shown;
        private float _target;
        private string _failure;
        private bool _signedIn;
        private Camera _camera;
        private CameraClearFlags _oldFlags;
        private Color _oldColour;
        private bool _painted;
        private bool _placed;
        private bool _recentring;
        private bool _pressedLast;
        private bool _vrDecided;
        private bool _vrResult;
        private bool _vrNow;
        private bool _built;
        private Color _appliedAccent, _appliedText, _appliedPanel, _appliedBackground;

        private static Type _tmpType;
        private static bool _tmpLooked;
        private static readonly Dictionary<string, Sprite> Sprites = new Dictionary<string, Sprite>();

        public static EzAdminLoadingScreen Current { get; private set; }
        public float Progress => _shown;
        public bool HasDesign => _parts != null && _parts.canvas != null;

        public Color Accent
        {
            get { return _accent; }
            set { _accent = value; ApplyTheme(); }
        }

        private bool IsVR
        {
            get
            {
                if (_mode == ScreenMode.VR) return true;
                if (_mode == ScreenMode.Flat) return false;
                if (_vrDecided) return _vrResult;
                return HeadsetOn();
            }
        }

        private static bool HeadsetOn()
        {
            if (XRSettings.isDeviceActive) return true;
            if (XRSettings.enabled && !string.IsNullOrEmpty(XRSettings.loadedDeviceName)
                && XRSettings.loadedDeviceName != "None") return true;
            var heads = new List<InputDevice>();
            InputDevices.GetDevicesWithCharacteristics(InputDeviceCharacteristics.HeadMounted, heads);
            return heads.Count > 0;
        }

        private static bool XrConfigured()
        {
            try
            {
                if (XRSettings.enabled) return true;
            }
            catch (Exception)
            {
            }
            return Type.GetType("UnityEngine.XR.Management.XRGeneralSettings, Unity.XR.Management") != null;
        }

        private void Awake()
        {
            if (!Application.isPlaying) return;
            Current = this;
            _usedFont = _font != null ? _font : LoadFont();
            _camera = Camera.main;
            if (_paintCamera && _camera != null)
            {
                _oldFlags = _camera.clearFlags;
                _oldColour = _camera.backgroundColor;
                _camera.clearFlags = CameraClearFlags.SolidColor;
                _camera.backgroundColor = _background;
                _painted = true;
            }
            if (HasDesign)
            {
                if (_parts.group != null) _parts.group.alpha = 0f;
                if (_parts.floor != null) _parts.floor.SetActive(false);
            }
        }

        private void OnEnable()
        {
            EzAdminAuth.SignedIn += OnSignedIn;
            EzAdminAuth.Failed += OnFailed;
            EzAdminAuth.Banned += OnBanned;
        }

        private void OnDisable()
        {
            EzAdminAuth.SignedIn -= OnSignedIn;
            EzAdminAuth.Failed -= OnFailed;
            EzAdminAuth.Banned -= OnBanned;
            if (Current == this) Current = null;
        }

        private void OnValidate()
        {
            if (!HasDesign) return;
#if UNITY_EDITOR
            if (!Application.isPlaying)
            {
                UnityEditor.EditorApplication.delayCall += () =>
                {
                    if (this == null || !HasDesign) return;
                    Adopt();
                    ApplyMode(_mode == ScreenMode.VR);
                    Refresh();
                };
                return;
            }
#endif
            if (_built) Refresh();
        }

        private void Start()
        {
            if (Application.isPlaying) StartCoroutine(Boot());
        }

        private IEnumerator Boot()
        {
            if (_mode == ScreenMode.Automatic && XrConfigured())
            {
                float until = Time.realtimeSinceStartup + 2f;
                while (!HeadsetOn() && Time.realtimeSinceStartup < until) yield return null;
            }
            _vrResult = HeadsetOn();
            _vrDecided = true;
            if (!HasDesign)
            {
                foreach (var key in new[] { "round", "softbox", "glow", "vfade", "hfade", "arc", "disc", "hoop",
                                            "check", "cross", "vignette", "hoopthin" })
                {
                    Sprite(key);
                    yield return null;
                }
                BuildDesign();
            }
            Adopt();
            ApplyMode(IsVR);
            Refresh();
            if (_parts.group != null) _parts.group.alpha = 0f;
            EnsureEventSystem();
            _built = true;
            yield return null;
            StartCoroutine(Run());
            if (_tip != null && _tips != null && _tips.Length > 0) StartCoroutine(Tips());
        }

        private void OnSignedIn(string playFabId, EzAdminAuth.Method method)
        {
            _signedIn = true;
        }

        private void OnFailed(string reason)
        {
            _failure = string.IsNullOrEmpty(reason) ? "Sign-in did not work." : reason;
        }

        private void OnBanned(DateTime until)
        {
            _failure = until > DateTime.UtcNow.AddYears(50)
                ? "This account is banned."
                : "This account is banned until " + until.ToLocalTime().ToString("d MMM yyyy, HH:mm") + ".";
            _continueIfSignInFails = false;
        }

        private IEnumerator Run()
        {
            float started = Time.unscaledTime;
            yield return FadeAll(1f, _fadeSeconds);

            Step(0, 1);
            float stepAt = Time.unscaledTime;
            _target = Mathf.Max(_target, 0.08f);
            float waitClient = Time.unscaledTime + 3f;
            while (EzAdminClient.Instance == null && Time.unscaledTime < waitClient) yield return null;
            yield return Hold(stepAt);
            Step(0, 2);

            if (_waitForSignIn)
            {
                Step(1, 1);
                stepAt = Time.unscaledTime;
                yield return SignInPhase();
                if (!string.IsNullOrEmpty(_failure) && !_continueIfSignInFails) yield break;
                if (string.IsNullOrEmpty(_failure)) yield return Hold(stepAt);
                bool inNow = _signedIn || PlayFabClientAPI.IsClientLoggedIn();
                Step(1, inNow ? 2 : 3);
                if (inNow)
                {
                    _onSignedIn.Invoke();
                    SetStatus(_welcome);
                    yield return new WaitForSecondsRealtime(0.5f);
                }
            }
            else
            {
                Step(1, 3);
            }
            _target = Mathf.Max(_target, 0.4f);

            if (_waitForItems && PlayFabClientAPI.IsClientLoggedIn())
            {
                Step(2, 1);
                stepAt = Time.unscaledTime;
                EzAdminItems.Start();
                float until = Time.unscaledTime + 10f;
                while (!EzAdminItems.Loaded && Time.unscaledTime < until)
                {
                    _target = Mathf.MoveTowards(_target, 0.52f, Time.unscaledDeltaTime * 0.05f);
                    yield return null;
                }
                yield return Hold(stepAt);
                Step(2, EzAdminItems.Loaded ? 2 : 3);
            }
            else
            {
                Step(2, 3);
            }
            _target = Mathf.Max(_target, 0.55f);

            Step(3, 1);
            AsyncOperation load = StartLoad();
            if (load != null)
            {
                load.allowSceneActivation = false;
                while (load.progress < 0.9f)
                {
                    _target = Mathf.Max(_target, 0.55f + load.progress / 0.9f * 0.42f);
                    yield return null;
                }
            }
            _target = 0.97f;
            while (Time.unscaledTime - started < _minimumSeconds) yield return null;
            _target = 1f;
            while (_shown < 0.995f) yield return null;
            Step(3, 2);
            SetStatus(_ready);
            if (_parts.spinner != null) _parts.spinner.gameObject.SetActive(false);
            Buzz(0.25f, 0.08f);
            Play(_readySound);
            _onReady.Invoke();
            yield return new WaitForSecondsRealtime(0.35f);

            yield return FadeAll(0f, _fadeSeconds);
            if (_parts.canvas != null) _parts.canvas.gameObject.SetActive(false);
            if (_parts.floor != null) _parts.floor.SetActive(false);
            if (load == null)
            {
                Restore();
                yield break;
            }
            load.allowSceneActivation = true;
        }

        private IEnumerator Hold(float since)
        {
            while (Time.unscaledTime - since < _stepSeconds) yield return null;
        }

        private IEnumerator SignInPhase()
        {
            _failure = null;
            if (PlayFabClientAPI.IsClientLoggedIn())
            {
                _signedIn = true;
                yield break;
            }
            float until = Time.unscaledTime + _signInTimeout;
            while (!PlayFabClientAPI.IsClientLoggedIn() && string.IsNullOrEmpty(_failure) && Time.unscaledTime < until)
            {
                _target = Mathf.MoveTowards(_target, 0.34f, Time.unscaledDeltaTime * 0.03f);
                yield return null;
            }
            if (PlayFabClientAPI.IsClientLoggedIn()) yield break;
            if (string.IsNullOrEmpty(_failure))
            {
                _failure = FindObjectOfType<EzAdminAuth>() == null
                    ? "Nothing signed the player in. Add the EzAdmin Sign-in component to this scene."
                    : "Sign-in is taking too long. Check the internet connection.";
            }
            _onFailed.Invoke(_failure);
            if (_continueIfSignInFails) yield break;
            ShowError(_failure);
        }

        private void ShowError(string message)
        {
            Step(1, 4);
            SetStatus(_failedTitle);
            if (_error != null)
            {
                _error.text = message;
                _error.gameObject.SetActive(true);
            }
            bool banned = message.Contains("banned");
            bool retry = _allowRetry && !banned;
            if (_parts.retry != null) _parts.retry.gameObject.SetActive(retry);
            if (_parts.retryHint != null) _parts.retryHint.gameObject.SetActive(retry && _vrNow && _retryWithTrigger);
            if (_parts.tipRow != null) _parts.tipRow.SetActive(false);
            if (_parts.spinner != null) _parts.spinner.gameObject.SetActive(false);
            Buzz(0.4f, 0.12f);
            Play(_errorSound);
            if (!banned && _autoRetrySeconds > 0f) StartCoroutine(AutoRetry());
        }

        private IEnumerator AutoRetry()
        {
            yield return new WaitForSecondsRealtime(_autoRetrySeconds);
            if (!string.IsNullOrEmpty(_failure)) Retry();
        }

        public void Retry()
        {
            if (_error != null) _error.gameObject.SetActive(false);
            if (_parts.retry != null) _parts.retry.gameObject.SetActive(false);
            if (_parts.retryHint != null) _parts.retryHint.gameObject.SetActive(false);
            if (_parts.tipRow != null) _parts.tipRow.SetActive(true);
            if (_parts.spinner != null) _parts.spinner.gameObject.SetActive(_showSpinner);
            _failure = null;
            var auth = FindObjectOfType<EzAdminAuth>();
            if (auth != null) auth.SignIn();
            StopAllCoroutines();
            for (int i = 0; _stepState != null && i < _stepState.Length; i++) Step(i, 0);
            StartCoroutine(Run());
            if (_tip != null && _tips != null && _tips.Length > 0) StartCoroutine(Tips());
        }

        private AsyncOperation StartLoad()
        {
            if (!string.IsNullOrEmpty(_nextScene))
                return Application.CanStreamedLevelBeLoaded(_nextScene) ? SceneManager.LoadSceneAsync(_nextScene) : null;
            int next = SceneManager.GetActiveScene().buildIndex + 1;
            return next < SceneManager.sceneCountInBuildSettings ? SceneManager.LoadSceneAsync(next) : null;
        }

        private IEnumerator Tips()
        {
            var tipGroup = _tip.gameObject.GetComponent<CanvasGroup>();
            if (tipGroup == null) tipGroup = _tip.gameObject.AddComponent<CanvasGroup>();
            int i = UnityEngine.Random.Range(0, _tips.Length);
            while (_tip != null && _tip.Graphic != null)
            {
                _tip.text = _tips[i % _tips.Length];
                yield return Fade(tipGroup, 0f, 1f, 0.35f);
                yield return new WaitForSecondsRealtime(_secondsPerTip);
                yield return Fade(tipGroup, 1f, 0f, 0.35f);
                i++;
            }
        }

        private void Update()
        {
            if (!_built) return;
            float dt = Mathf.Min(Time.unscaledDeltaTime, 1f / 30f);
            if (_accent != _appliedAccent || _text != _appliedText || _panel != _appliedPanel || _background != _appliedBackground)
                ApplyTheme();
            _shown = Mathf.MoveTowards(_shown, _target, dt * (0.08f + Mathf.Abs(_target - _shown) * 3f));
            if (_parts.barFill != null) _parts.barFill.anchorMax = new Vector2(Mathf.Max(0.02f, _shown), 1f);
            if (_percent != null) _percent.text = Mathf.RoundToInt(_shown * 100f) + "%";
            if (_parts.spinner != null && _parts.spinner.gameObject.activeSelf) _parts.spinner.Rotate(0, 0, -320f * dt);
            if (_parts.logoFrame != null)
                _parts.logoFrame.localScale = Vector3.one * (_animateLogo ? 1f + Mathf.Sin(Time.unscaledTime * 1.6f) * 0.015f : 1f);
            AnimateSteps();
            if (_vrNow && _retryWithTrigger && _parts.retry != null && _parts.retry.gameObject.activeInHierarchy
                && TriggerPressed()) Retry();
        }

        private void ApplyTheme()
        {
            foreach (var t in _tints)
            {
                if (t.Graphic == null) continue;
                Color baseColour;
                switch (t.Role)
                {
                    case Role.Accent: baseColour = _accent; break;
                    case Role.Text: baseColour = _text; break;
                    case Role.Panel: baseColour = _panel; break;
                    case Role.Background: baseColour = _background; break;
                    default: baseColour = Color.Lerp(_panel, Color.white, 0.08f); break;
                }
                float alpha = t.Role == Role.Panel || t.Role == Role.Background ? baseColour.a * t.Alpha : t.Alpha;
                t.Graphic.color = new Color(baseColour.r, baseColour.g, baseColour.b, alpha);
            }
            if (_parts.retryLabel != null) _parts.retryLabel.color = OnAccent();
            if (_painted && _camera != null) _camera.backgroundColor = _background;
            PaintSteps();
            _appliedAccent = _accent;
            _appliedText = _text;
            _appliedPanel = _panel;
            _appliedBackground = _background;
        }

        private void Adopt()
        {
            var p = _parts;
            _canvas = p.canvas;
            _group = p.group;
            _content = p.content;
            _groups.Clear();
            if (_group != null) _groups.Add(_group);
            if (p.floor != null)
            {
                var fg = p.floor.GetComponent<CanvasGroup>();
                if (fg != null) _groups.Add(fg);
            }
            _percent = Words.Wrap(p.percent);
            _stepCount = Words.Wrap(p.stepCount);
            _status = Words.Wrap(p.status);
            _tip = Words.Wrap(p.tip);
            _error = Words.Wrap(p.error);
            int n = p.stepTexts != null ? p.stepTexts.Length : 0;
            _stepTexts = new Words[n];
            for (int i = 0; i < n; i++) _stepTexts[i] = Words.Wrap(p.stepTexts[i]);
            if (_stepState == null || _stepState.Length != n)
            {
                _stepState = new int[n];
                _popAt = new float[n];
                for (int i = 0; i < n; i++) _popAt[i] = -10f;
            }
            _tints.Clear();
            int count = p.tintGraphics != null ? p.tintGraphics.Length : 0;
            for (int i = 0; i < count; i++)
                _tints.Add(new Tint { Graphic = p.tintGraphics[i], Role = (Role)p.tintRoles[i], Alpha = p.tintAlphas[i] });
            if (Application.isPlaying && p.retry != null)
            {
                p.retry.onClick.RemoveListener(Retry);
                p.retry.onClick.AddListener(Retry);
                var touch = p.retry.GetComponent<EzAdminTouchButton>();
                if (touch != null)
                {
                    touch.OnPressed.RemoveListener(Retry);
                    touch.OnPressed.AddListener(Retry);
                }
            }
        }

        private void ApplyMode(bool vr)
        {
            _vrNow = vr;
            var p = _parts;
            if (p.canvas == null) return;
            var rt = (RectTransform)p.canvas.transform;
            var scaler = p.canvas.GetComponent<CanvasScaler>();
            if (vr)
            {
                p.canvas.renderMode = RenderMode.WorldSpace;
                p.canvas.worldCamera = _camera;
                rt.sizeDelta = new Vector2(CardW + 240f, CardH + 240f);
                float scale = _vrWidth / CardW;
                rt.localScale = new Vector3(scale, scale, scale);
                if (scaler != null)
                {
                    scaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize;
                    scaler.dynamicPixelsPerUnit = 4f;
                    scaler.referencePixelsPerUnit = 100f;
                }
            }
            else
            {
                p.canvas.renderMode = RenderMode.ScreenSpaceOverlay;
                rt.localScale = Vector3.one;
                if (scaler != null)
                {
                    scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
                    scaler.referenceResolution = new Vector2(1920, 1080);
                    scaler.matchWidthOrHeight = 0.5f;
                }
            }
            if (p.backdrop != null) p.backdrop.SetActive(!vr);
            if (p.floor != null) p.floor.SetActive(vr && _vrSpace && Application.isPlaying);
            if (vr && Application.isPlaying) LateUpdate();
        }

        private void Refresh()
        {
            var p = _parts;
            if (p.canvas == null) return;
            SetWords(p.title, string.IsNullOrEmpty(_gameName) ? Application.productName : _gameName);
            SetWords(p.subtitle, _subtitle);
            if (p.subtitle != null) p.subtitle.gameObject.SetActive(!string.IsNullOrEmpty(_subtitle));
            if (p.logo != null)
            {
                if (_gameLogo != null) p.logo.sprite = _gameLogo;
                else if (p.logo.sprite == null && Application.isPlaying) p.logo.sprite = EzAdminBrand.LogoSprite(256);
            }
            if (p.backdropImage != null) p.backdropImage.sprite = _backgroundImage;
            if (p.backdropArt != null) p.backdropArt.SetActive(_backgroundImage != null);
            if (p.cardImage != null) p.cardImage.sprite = _backgroundImage;
            if (p.cardArt != null) p.cardArt.SetActive(_backgroundImage != null && _vrNow);
            string[] words = { _stepConnect, _stepSignIn, _stepItems, _stepWorld };
            for (int i = 0; p.stepTexts != null && i < p.stepTexts.Length && i < words.Length; i++) SetWords(p.stepTexts[i], words[i]);
            SetWords(p.retryLabel, _retryLabel);
            SetWords(p.retryHint, _retryHint);
            SetWords(p.tipLabel, _tipLabel);
            if (p.tipRow != null) p.tipRow.SetActive(_tips != null && _tips.Length > 0);
            if (p.progress != null) p.progress.SetActive(_showProgress);
            if (p.percent != null) p.percent.gameObject.SetActive(_showPercent);
            if (p.steps != null) p.steps.SetActive(_showSteps);
            if (p.spinner != null) p.spinner.gameObject.SetActive(_showSpinner);
            if (p.footer != null) p.footer.SetActive(_showSecuredBy);
            if (!Application.isPlaying) SetWords(p.status, _stepConnect);
            ApplyTheme();
        }

        private static void SetWords(Graphic g, string value)
        {
            var w = Words.Wrap(g);
            if (w != null) w.text = value;
        }

        private T Tinted<T>(T graphic, Role role, float alpha) where T : Graphic
        {
            if (graphic == null) return null;
            _tints.Add(new Tint { Graphic = graphic, Role = role, Alpha = alpha });
            return graphic;
        }

        private GameObject Tinted(GameObject go, Role role, float alpha)
        {
            Tinted(go.GetComponent<Graphic>(), role, alpha);
            return go;
        }

        private void LateUpdate()
        {
            if (!_vrNow || _canvas == null || _canvas.renderMode != RenderMode.WorldSpace) return;
            if (_camera == null) _camera = Camera.main;
            if (_camera == null) return;
            Transform head = _camera.transform;
            Vector3 flat = Vector3.ProjectOnPlane(head.forward, Vector3.up);
            if (flat.sqrMagnitude < 0.0001f) flat = Vector3.ProjectOnPlane(head.up, Vector3.up);
            if (flat.sqrMagnitude < 0.0001f) flat = Vector3.forward;
            flat.Normalize();
            Vector3 wanted = head.position + flat * _vrDistance + Vector3.down * 0.1f;
            Transform t = _canvas.transform;

            if (!_placed)
            {
                t.position = wanted;
                t.rotation = Quaternion.LookRotation(flat, Vector3.up);
                _placed = true;
            }
            else if (_followHead)
            {
                Vector3 toCanvas = Vector3.ProjectOnPlane(t.position - head.position, Vector3.up);
                float angle = Vector3.Angle(toCanvas, flat);
                float distanceOff = Mathf.Abs(toCanvas.magnitude - _vrDistance);
                if (angle > _followAngle || distanceOff > 0.6f) _recentring = true;
                if (_recentring)
                {
                    float k = 1f - Mathf.Exp(-4.5f * Time.unscaledDeltaTime);
                    t.position = Vector3.Lerp(t.position, wanted, k);
                    Vector3 face = Vector3.ProjectOnPlane(t.position - head.position, Vector3.up);
                    if (face.sqrMagnitude > 0.0001f)
                        t.rotation = Quaternion.Slerp(t.rotation, Quaternion.LookRotation(face, Vector3.up), k);
                    if (angle < 2.5f && distanceOff < 0.05f) _recentring = false;
                }
            }

            if (_parts.floor != null && _parts.floor.activeSelf)
            {
                Vector3 f = head.position;
                f.y = FloorHeight(head);
                _parts.floor.transform.position = f;
            }
        }

        private static float FloorHeight(Transform head)
        {
            if (head.parent != null && head.localPosition.y > 0.5f) return head.parent.position.y;
            return head.position.y - 1.6f;
        }

        private void Restore()
        {
            if (!_painted || _camera == null) return;
            _camera.clearFlags = _oldFlags;
            _camera.backgroundColor = _oldColour;
            _painted = false;
        }

        private void SetStatus(string text)
        {
            if (_status != null) _status.text = text;
        }

        private void Step(int index, int state)
        {
            string[] words = { _stepConnect, _stepSignIn, _stepItems, _stepWorld };
            if (state == 1 && index >= 0 && index < words.Length) SetStatus(words[index]);
            if (_stepState == null || index < 0 || index >= _stepState.Length) return;
            if (state == 2 && _stepState[index] != 2) _popAt[index] = Time.unscaledTime;
            _stepState[index] = state;
            int done = 0;
            for (int i = 0; i < _stepState.Length; i++) if (_stepState[i] == 2 || _stepState[i] == 3) done++;
            int current = Mathf.Min(_stepState.Length, done + 1);
            if (_stepCount != null) _stepCount.text = "Step " + current + " of " + _stepState.Length;
            PaintSteps();
        }

        private void PaintSteps()
        {
            var p = _parts;
            if (p.nodes == null || _stepState == null || _stepTexts == null) return;
            var red = new Color32(0xF8, 0x71, 0x71, 0xFF);
            for (int i = 0; i < p.nodes.Length && i < _stepState.Length; i++)
            {
                int s = _stepState[i];
                bool done = s == 2;
                bool active = s == 1;
                bool failed = s == 4;
                if (p.nodes[i] == null) continue;
                if (Application.isPlaying) p.nodes[i].sprite = done || failed ? Sprite("disc") : Sprite("hoop");
                p.nodes[i].color = failed ? (Color)red : done || active ? _accent : Dim(0.22f);
                p.marks[i].gameObject.SetActive(done || active || failed);
                if (Application.isPlaying) p.marks[i].sprite = done ? Sprite("check") : failed ? Sprite("cross") : Sprite("disc");
                p.marks[i].color = done || failed ? OnAccent() : _accent;
                p.marks[i].rectTransform.sizeDelta = done || failed ? new Vector2(24, 24) : new Vector2(13, 13);
                if (_stepTexts[i] != null)
                {
                    _stepTexts[i].color = active ? _text : done ? Dim(0.78f) : Dim(0.4f);
                    _stepTexts[i].bold = active;
                }
                if (p.links != null && i < p.links.Length && p.links[i] != null) p.links[i].color = done ? _accent : Dim(0.14f);
            }
        }

        private Color OnAccent()
        {
            float lum = _accent.r * 0.299f + _accent.g * 0.587f + _accent.b * 0.114f;
            return lum > 0.55f ? new Color(0.03f, 0.05f, 0.04f, 1f) : Color.white;
        }

        private void AnimateSteps()
        {
            var p = _parts;
            if (p.nodes == null || _stepState == null) return;
            float now = Time.unscaledTime;
            for (int i = 0; i < p.nodes.Length && i < _stepState.Length; i++)
            {
                if (p.nodes[i] == null) continue;
                float scale = 1f;
                float since = now - _popAt[i];
                if (since < 0.35f) scale = 1f + Mathf.Sin(since / 0.35f * Mathf.PI) * 0.28f;
                p.nodes[i].rectTransform.localScale = Vector3.one * scale;
                if (_stepState[i] == 1)
                {
                    var c = _accent;
                    c.a = 0.65f + Mathf.Sin(now * 5f) * 0.35f;
                    p.marks[i].color = c;
                }
            }
        }

        private void Play(AudioClip clip)
        {
            if (clip == null) return;
            var at = _camera != null ? _camera.transform.position : Vector3.zero;
            AudioSource.PlayClipAtPoint(clip, at);
        }

        private bool TriggerPressed()
        {
            bool down = false;
            var devices = new List<InputDevice>();
            InputDevices.GetDevicesWithCharacteristics(InputDeviceCharacteristics.Controller, devices);
            foreach (var d in devices)
            {
                bool b;
                if ((d.TryGetFeatureValue(CommonUsages.triggerButton, out b) && b)
                    || (d.TryGetFeatureValue(CommonUsages.primaryButton, out b) && b)) down = true;
            }
            bool pressed = down && !_pressedLast;
            _pressedLast = down;
            return pressed;
        }

        private void Buzz(float amplitude, float seconds)
        {
            if (!_haptics || !_vrNow) return;
            var devices = new List<InputDevice>();
            InputDevices.GetDevicesWithCharacteristics(InputDeviceCharacteristics.Controller, devices);
            foreach (var d in devices)
            {
                HapticCapabilities caps;
                if (d.TryGetHapticCapabilities(out caps) && caps.supportsImpulse) d.SendHapticImpulse(0, amplitude, seconds);
            }
        }

        private void EnsureEventSystem()
        {
            if (_vrNow || 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
        }

        public void RemoveDesign()
        {
            if (_parts == null) return;
            if (_parts.canvas != null) DestroyImmediate(_parts.canvas.gameObject);
            if (_parts.floor != null) DestroyImmediate(_parts.floor);
            _parts = new Parts();
        }

        public void BuildDesign()
        {
            RemoveDesign();
            if (SpriteStore != null) Sprites.Clear();
            _tints.Clear();
            _usedFont = _font != null ? _font : LoadFont();
            var p = _parts;

            var root = NewCanvas("Loading Screen", 30000);
            p.canvas = root.GetComponent<Canvas>();
            p.group = root.GetComponent<CanvasGroup>();
            var rt = (RectTransform)root.transform;
            rt.sizeDelta = new Vector2(CardW + 240f, CardH + 240f);

            var backdrop = new GameObject("Backdrop", typeof(RectTransform));
            backdrop.transform.SetParent(root.transform, false);
            Stretch(backdrop, 0f);
            p.backdrop = backdrop;
            Stretch(Tinted(Img(backdrop.transform, "Base", null, Color.white), Role.Background, 1f), 0f);
            Stretch(Tinted(Img(backdrop.transform, "Top light", "vfade", Color.white), Role.Lift, 0.35f), 0f);
            var art = Img(backdrop.transform, "Picture", null, Color.white);
            Stretch(art, 0f);
            p.backdropImage = art.GetComponent<Image>();
            p.backdropArt = art;
            Stretch(Tinted(Img(art.transform, "Shade", null, Color.white), Role.Background, 0.74f), 0f);
            var glow = Tinted(Img(backdrop.transform, "Glow", "glow", Color.white), Role.Accent, 0.12f);
            Place(glow, new Vector2(0.5f, 0.5f), new Vector2(0, 60), new Vector2(2000, 1450));
            Stretch(Img(backdrop.transform, "Vignette", "vignette", new Color(0, 0, 0, 0.72f)), 0f);

            var shadow = Img(root.transform, "Shadow", "softbox", new Color(0, 0, 0, 0.6f));
            Sliced(shadow, 64f, 64f);
            Place(shadow, new Vector2(0.5f, 0.5f), new Vector2(0, -20), new Vector2(CardW + 130, CardH + 130));

            var edge = Tinted(Img(root.transform, "Edge", "round", Color.white), Role.Accent, 0.28f);
            Sliced(edge, 32f, 40f);
            Place(edge, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(CardW + 4, CardH + 4));

            var card = Tinted(Img(root.transform, "Card", "round", Color.white), Role.Panel, 1f);
            Sliced(card, 32f, 38f);
            Place(card, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(CardW, CardH));
            var cardT = card.transform;
            card.AddComponent<Mask>().showMaskGraphic = true;

            var cardArt = Img(cardT, "Picture", null, Color.white);
            Stretch(cardArt, 0f);
            p.cardImage = cardArt.GetComponent<Image>();
            p.cardArt = cardArt;
            Stretch(Tinted(Img(cardArt.transform, "Shade", null, Color.white), Role.Panel, 0.82f), 0f);
            var wash = Tinted(Img(cardT, "Glow", "glow", Color.white), Role.Accent, 0.14f);
            Place(wash, new Vector2(0.5f, 1f), new Vector2(0, -40), new Vector2(1300, 620));
            var line = Tinted(Img(cardT, "Accent line", "hfade", Color.white), Role.Accent, 1f);
            Place(line, new Vector2(0.5f, 1f), new Vector2(0, -2f), new Vector2(CardW * 0.7f, 4));

            var inner = new GameObject("Content", typeof(RectTransform), typeof(CanvasGroup));
            inner.transform.SetParent(cardT, false);
            Stretch(inner, 0f);
            p.content = inner.GetComponent<CanvasGroup>();
            var c = inner.transform;

            var frame = Img(c, "Logo", "round", Color.white);
            Sliced(frame, 32f, 34f);
            frame.AddComponent<Mask>().showMaskGraphic = false;
            Place(frame, new Vector2(0.5f, 1f), new Vector2(0, -150), new Vector2(156, 156));
            var logo = Img(frame.transform, "Image", null, Color.white);
            p.logo = logo.GetComponent<Image>();
            p.logo.sprite = _gameLogo != null ? _gameLogo : Stored("logo", EzAdminBrand.LogoSprite(256));
            p.logo.preserveAspect = true;
            Stretch(logo, 0f);
            p.logoFrame = (RectTransform)frame.transform;

            var title = Label(c, "Name", "Game", 66, true, Role.Text, 1f, TextAnchor.MiddleCenter);
            Place(title.gameObject, new Vector2(0.5f, 1f), new Vector2(0, -284), new Vector2(CardW - 120, 84));
            Spacing(title, 1.5f);
            p.title = title.Graphic;
            var sub = Label(c, "Subtitle", "", 28, false, Role.Text, 0.55f, TextAnchor.MiddleCenter);
            Place(sub.gameObject, new Vector2(0.5f, 1f), new Vector2(0, -336), new Vector2(CardW - 120, 38));
            p.subtitle = sub.Graphic;

            var row = new GameObject("Status", typeof(RectTransform), typeof(HorizontalLayoutGroup), typeof(ContentSizeFitter));
            row.transform.SetParent(c, false);
            var layout = row.GetComponent<HorizontalLayoutGroup>();
            layout.spacing = 16;
            layout.childAlignment = TextAnchor.MiddleCenter;
            layout.childControlWidth = layout.childControlHeight = true;
            layout.childForceExpandWidth = layout.childForceExpandHeight = false;
            row.GetComponent<ContentSizeFitter>().horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
            Place(row, new Vector2(0.5f, 1f), new Vector2(0, -408), new Vector2(400, 52));
            var spin = Tinted(Img(row.transform, "Spinner", "arc", Color.white), Role.Accent, 1f);
            var spinLayout = spin.AddComponent<LayoutElement>();
            spinLayout.preferredWidth = spinLayout.preferredHeight = 36;
            p.spinner = (RectTransform)spin.transform;
            var status = Label(row.transform, "Text", _stepConnect, 36, true, Role.Text, 1f, TextAnchor.MiddleLeft);
            NoWrap(status);
            p.status = status.Graphic;

            var progress = new GameObject("Progress", typeof(RectTransform));
            progress.transform.SetParent(c, false);
            Stretch(progress, 0f);
            p.progress = progress;
            var track = Tinted(Img(progress.transform, "Track", "round", Color.white), Role.Text, 0.08f);
            Sliced(track, 32f, 9f);
            Place(track, new Vector2(0.5f, 1f), new Vector2(0, -466), new Vector2(BarW, 18));
            var fill = Tinted(Img(track.transform, "Fill", "round", Color.white), Role.Accent, 1f);
            Sliced(fill, 32f, 9f);
            var frt = (RectTransform)fill.transform;
            frt.anchorMin = Vector2.zero;
            frt.anchorMax = new Vector2(0.35f, 1f);
            frt.offsetMin = frt.offsetMax = Vector2.zero;
            p.bar = fill.GetComponent<Image>();
            p.barFill = frt;
            Stretch(Img(fill.transform, "Shine", "vfade", new Color(1, 1, 1, 0.22f)), 0f);
            var count = Label(progress.transform, "Step", "Step 1 of 4", 24, false, Role.Text, 0.5f, TextAnchor.MiddleLeft);
            Place(count.gameObject, new Vector2(0.5f, 1f), new Vector2(-BarW / 2 + 160, -502), new Vector2(320, 32));
            p.stepCount = count.Graphic;
            var percent = Label(progress.transform, "Percent", "35%", 24, true, Role.Text, 0.75f, TextAnchor.MiddleRight);
            Place(percent.gameObject, new Vector2(0.5f, 1f), new Vector2(BarW / 2 - 110, -502), new Vector2(220, 32));
            p.percent = percent.Graphic;

            BuildSteps(c);

            var divider = Tinted(Img(c, "Divider", "hfade", Color.white), Role.Text, 0.1f);
            Place(divider, new Vector2(0.5f, 0f), new Vector2(0, 148), new Vector2(CardW - 160, 2));

            var tipRow = new GameObject("Tip", typeof(RectTransform));
            tipRow.transform.SetParent(c, false);
            Place(tipRow, new Vector2(0.5f, 0f), new Vector2(0, 74), new Vector2(CardW - 120, 90));
            p.tipRow = tipRow;
            var chip = Tinted(Img(tipRow.transform, "Label", "round", Color.white), Role.Accent, 0.16f);
            Sliced(chip, 32f, 15f);
            Place(chip, new Vector2(0.5f, 0.5f), new Vector2(0, 26), new Vector2(84, 32));
            var chipText = Label(chip.transform, "Text", _tipLabel, 19, true, Role.Accent, 1f, TextAnchor.MiddleCenter);
            Stretch(chipText.gameObject, 0f);
            Spacing(chipText, 3f);
            p.tipLabel = chipText.Graphic;
            var tip = Label(tipRow.transform, "Tip text", "Your loading tips show here.", 27, false, Role.Text, 0.72f,
                            TextAnchor.MiddleCenter);
            Place(tip.gameObject, new Vector2(0.5f, 0.5f), new Vector2(0, -18), new Vector2(CardW - 160, 60));
            p.tip = tip.Graphic;

            var error = Label(c, "Error", "", 28, false, Role.Text, 1f, TextAnchor.MiddleCenter);
            _tints.RemoveAt(_tints.Count - 1);
            error.color = new Color32(0xFF, 0x8F, 0x91, 0xFF);
            Place(error.gameObject, new Vector2(0.5f, 0f), new Vector2(0, 180), new Vector2(CardW - 160, 64));
            error.gameObject.SetActive(false);
            p.error = error.Graphic;

            var retry = Tinted(Img(c, "Try again", "round", Color.white), Role.Accent, 1f);
            Sliced(retry, 32f, 24f);
            retry.GetComponent<Image>().raycastTarget = true;
            Place(retry, new Vector2(0.5f, 0f), new Vector2(0, 92), new Vector2(300, 76));
            var button = retry.AddComponent<Button>();
            var colours = button.colors;
            colours.highlightedColor = new Color(1.12f, 1.12f, 1.12f, 1f);
            colours.pressedColor = new Color(0.85f, 0.85f, 0.85f, 1f);
            button.colors = colours;
            var box = retry.AddComponent<BoxCollider>();
            box.size = new Vector3(300, 76, 60);
            box.isTrigger = true;
            retry.AddComponent<EzAdminTouchButton>();
            var retryText = Label(retry.transform, "Text", _retryLabel, 30, true, Role.Text, 1f, TextAnchor.MiddleCenter);
            _tints.RemoveAt(_tints.Count - 1);
            Stretch(retryText.gameObject, 0f);
            p.retry = button;
            p.retryLabel = retryText.Graphic;
            retry.SetActive(false);
            var hint = Label(c, "Trigger hint", _retryHint, 22, false, Role.Text, 0.45f, TextAnchor.MiddleCenter);
            Place(hint.gameObject, new Vector2(0.5f, 0f), new Vector2(0, 34), new Vector2(520, 30));
            hint.gameObject.SetActive(false);
            p.retryHint = hint.Graphic;

            var foot = new GameObject("Secured by EzAdmin", typeof(RectTransform), typeof(HorizontalLayoutGroup),
                                      typeof(ContentSizeFitter));
            foot.transform.SetParent(root.transform, false);
            var fl = foot.GetComponent<HorizontalLayoutGroup>();
            fl.spacing = 10;
            fl.childAlignment = TextAnchor.MiddleCenter;
            fl.childControlWidth = fl.childControlHeight = true;
            fl.childForceExpandWidth = fl.childForceExpandHeight = false;
            foot.GetComponent<ContentSizeFitter>().horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
            Place(foot, new Vector2(0.5f, 0.5f), new Vector2(0, -CardH / 2 - 56), new Vector2(260, 32));
            var mark = Img(foot.transform, "Mark", null, new Color(1, 1, 1, 0.8f));
            mark.GetComponent<Image>().sprite = Stored("mark", EzAdminBrand.LogoSprite(64));
            var ml = mark.AddComponent<LayoutElement>();
            ml.preferredWidth = ml.preferredHeight = 26;
            NoWrap(Label(foot.transform, "Text", "Secured by EzAdmin", 22, false, Role.Text, 0.45f, TextAnchor.MiddleLeft));
            p.footer = foot;

            BuildFloor();

            p.tintGraphics = new Graphic[_tints.Count];
            p.tintRoles = new int[_tints.Count];
            p.tintAlphas = new float[_tints.Count];
            for (int i = 0; i < _tints.Count; i++)
            {
                p.tintGraphics[i] = _tints[i].Graphic;
                p.tintRoles[i] = (int)_tints[i].Role;
                p.tintAlphas[i] = _tints[i].Alpha;
            }
            Adopt();
            ApplyMode(_mode == ScreenMode.VR);
            Refresh();
        }

        private void BuildSteps(Transform c)
        {
            var p = _parts;
            var holder = new GameObject("Steps", typeof(RectTransform));
            holder.transform.SetParent(c, false);
            Stretch(holder, 0f);
            p.steps = holder;
            var h = holder.transform;
            string[] steps = { _stepConnect, _stepSignIn, _stepItems, _stepWorld };
            int n = steps.Length;
            p.nodes = new Image[n];
            p.marks = new Image[n];
            p.stepTexts = new Graphic[n];
            p.links = new Image[n - 1];
            float gap = BarW / (n - 1);
            const float y = -572f;
            for (int i = 0; i < n - 1; i++)
            {
                var link = Img(h, "Line", null, Dim(0.14f));
                Place(link, new Vector2(0.5f, 1f), new Vector2(-BarW / 2 + gap * (i + 0.5f), y), new Vector2(gap - 56, 4));
                p.links[i] = link.GetComponent<Image>();
            }
            for (int i = 0; i < n; i++)
            {
                float x = -BarW / 2 + gap * i;
                var node = Img(h, "Step " + (i + 1), "hoop", Dim(0.22f));
                Place(node, new Vector2(0.5f, 1f), new Vector2(x, y), new Vector2(42, 42));
                p.nodes[i] = node.GetComponent<Image>();
                var mark = Img(node.transform, "Mark", "disc", _accent);
                Place(mark, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(13, 13));
                p.marks[i] = mark.GetComponent<Image>();
                mark.SetActive(false);
                var label = Label(h, "Step " + (i + 1) + " name", steps[i], 25, false, Role.Text, 0.4f, TextAnchor.UpperCenter);
                _tints.RemoveAt(_tints.Count - 1);
                Place(label.gameObject, new Vector2(0.5f, 1f), new Vector2(x, y - 54), new Vector2(gap + 30, 60));
                p.stepTexts[i] = label.Graphic;
            }
        }

        private void BuildFloor()
        {
            var floor = NewCanvas("Loading Screen Floor", 29990);
            var canvas = floor.GetComponent<Canvas>();
            canvas.renderMode = RenderMode.WorldSpace;
            var frt = (RectTransform)floor.transform;
            frt.sizeDelta = new Vector2(600, 600);
            frt.localScale = Vector3.one * 0.01f;
            frt.rotation = Quaternion.Euler(90f, 0f, 0f);
            var pool = Tinted(Img(floor.transform, "Glow", "glow", Color.white), Role.Accent, 0.16f);
            Stretch(pool, 0f);
            var ring = Tinted(Img(floor.transform, "Ring", "hoopthin", Color.white), Role.Accent, 0.35f);
            Place(ring, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(120, 120));
            floor.SetActive(false);
            _parts.floor = floor;
        }

        private GameObject NewCanvas(string name, int order)
        {
            var root = new GameObject(name, typeof(RectTransform), typeof(Canvas), typeof(CanvasGroup), typeof(CanvasScaler),
                                      typeof(GraphicRaycaster));
            root.GetComponent<Canvas>().sortingOrder = order;
            return root;
        }

        private Color Dim(float alpha)
        {
            return new Color(_text.r, _text.g, _text.b, alpha);
        }

        private IEnumerator FadeAll(float to, float seconds)
        {
            var from = new float[_groups.Count];
            for (int i = 0; i < _groups.Count; i++) from[i] = _groups[i] != null ? _groups[i].alpha : to;
            float t = 0f;
            while (t < seconds)
            {
                t += Time.unscaledDeltaTime;
                float k = Mathf.SmoothStep(0f, 1f, t / seconds);
                for (int i = 0; i < _groups.Count; i++)
                    if (_groups[i] != null) _groups[i].alpha = Mathf.Lerp(from[i], to, k);
                if (_content != null && to > 0f)
                    ((RectTransform)_content.transform).anchoredPosition = new Vector2(0, Mathf.Lerp(-18f, 0f, k));
                yield return null;
            }
            foreach (var g in _groups)
                if (g != null) g.alpha = to;
        }

        private static IEnumerator Fade(CanvasGroup group, float from, float to, float seconds)
        {
            if (group == null) yield break;
            float t = 0f;
            while (t < seconds)
            {
                t += Time.unscaledDeltaTime;
                if (group == null) yield break;
                group.alpha = Mathf.Lerp(from, to, Mathf.SmoothStep(0f, 1f, t / seconds));
                yield return null;
            }
            if (group != null) group.alpha = to;
        }

        private GameObject Img(Transform parent, string name, string sprite, Color colour)
        {
            var go = new GameObject(name, typeof(RectTransform), typeof(Image));
            go.transform.SetParent(parent, false);
            var image = go.GetComponent<Image>();
            if (sprite != null) image.sprite = Sprite(sprite);
            image.color = colour;
            image.raycastTarget = false;
            return go;
        }

        private static void Sliced(GameObject go, float textureRadius, float radius)
        {
            var image = go.GetComponent<Image>();
            image.type = Image.Type.Sliced;
            image.pixelsPerUnitMultiplier = textureRadius / Mathf.Max(1f, radius);
        }

        private static Type TmpType()
        {
            if (_tmpLooked) return _tmpType;
            _tmpLooked = true;
            _tmpType = Type.GetType("TMPro.TextMeshProUGUI, Unity.TextMeshPro");
            if (_tmpType == null) return null;
            var settings = Type.GetType("TMPro.TMP_Settings, Unity.TextMeshPro");
            var font = settings?.GetProperty("defaultFontAsset", BindingFlags.Public | BindingFlags.Static);
            try
            {
                if (font == null || font.GetValue(null) == null) _tmpType = null;
            }
            catch (Exception)
            {
                _tmpType = null;
            }
            return _tmpType;
        }

        private static void Set(object target, string property, object value)
        {
            try
            {
                var p = target.GetType().GetProperty(property, BindingFlags.Public | BindingFlags.Instance);
                if (p == null || !p.CanWrite) return;
                if (p.PropertyType.IsEnum && value is string) value = Enum.Parse(p.PropertyType, (string)value);
                p.SetValue(target, value);
            }
            catch (Exception)
            {
            }
        }

        private Words Label(Transform parent, string name, string value, int size, bool bold, Role role, float alpha,
                            TextAnchor align)
        {
            var tmp = TmpType();
            Graphic graphic;
            if (tmp != null)
            {
                var go = new GameObject(name, typeof(RectTransform));
                go.transform.SetParent(parent, false);
                graphic = (Graphic)go.AddComponent(tmp);
                if (_fontAsset != null && _fontAsset.GetType().Name == "TMP_FontAsset") Set(graphic, "font", _fontAsset);
                Set(graphic, "fontSize", (float)size);
                Set(graphic, "alignment", align == TextAnchor.MiddleLeft ? "Left" : align == TextAnchor.MiddleRight ? "Right"
                                          : align == TextAnchor.UpperCenter ? "Top" : "Center");
                Set(graphic, "enableWordWrapping", true);
                Set(graphic, "overflowMode", "Overflow");
            }
            else
            {
                var go = new GameObject(name, typeof(RectTransform), typeof(Text));
                go.transform.SetParent(parent, false);
                var text = go.GetComponent<Text>();
                text.font = _usedFont;
                text.fontSize = size;
                text.alignment = align;
                text.horizontalOverflow = HorizontalWrapMode.Wrap;
                text.verticalOverflow = VerticalWrapMode.Overflow;
                graphic = text;
            }
            graphic.raycastTarget = false;
            var words = Words.Wrap(graphic);
            words.text = value;
            words.bold = bold;
            Tinted(graphic, role, alpha);
            graphic.color = role == Role.Accent ? new Color(_accent.r, _accent.g, _accent.b, alpha) : Dim(alpha);
            return words;
        }

        private static void NoWrap(Words words)
        {
            var legacy = words.Graphic as Text;
            if (legacy != null) legacy.horizontalOverflow = HorizontalWrapMode.Overflow;
            else Set(words.Graphic, "enableWordWrapping", false);
        }

        private static void Spacing(Words words, float amount)
        {
            if (!(words.Graphic is Text)) Set(words.Graphic, "characterSpacing", amount);
        }

        private static void Place(GameObject go, Vector2 anchor, Vector2 position, Vector2 size)
        {
            var rt = (RectTransform)go.transform;
            rt.anchorMin = rt.anchorMax = anchor;
            rt.pivot = new Vector2(0.5f, 0.5f);
            rt.anchoredPosition = position;
            rt.sizeDelta = size;
        }

        private static void Stretch(GameObject go, float inset)
        {
            var rt = (RectTransform)go.transform;
            rt.anchorMin = Vector2.zero;
            rt.anchorMax = Vector2.one;
            rt.offsetMin = new Vector2(inset, inset);
            rt.offsetMax = new Vector2(-inset, -inset);
        }

        private static Sprite Stored(string key, Sprite sprite)
        {
            return SpriteStore != null ? SpriteStore(key, sprite) : sprite;
        }

        private static Sprite Sprite(string key)
        {
            Sprite cached;
            if (Sprites.TryGetValue(key, out cached) && cached != null) return cached;
            Sprite made;
            switch (key)
            {
                case "round": made = Make(128, 32, (x, y) => RoundRect(x, y, 128, 32f)); break;
                case "softbox": made = Make(128, 64, (x, y) => SoftBox(x, y, 128)); break;
                case "disc": made = Make(64, 0, (x, y) => Disc(x, y, 64, 31f)); break;
                case "hoop": made = Make(64, 0, (x, y) => Ring(x, y, 64, 30f, 4.5f)); break;
                case "hoopthin": made = Make(128, 0, (x, y) => Ring(x, y, 128, 62f, 2f)); break;
                case "arc": made = Make(96, 0, (x, y) => Arc(x, y, 96)); break;
                case "check": made = Make(64, 0, (x, y) => Check(x, y, 64)); break;
                case "cross": made = Make(64, 0, (x, y) => Cross(x, y, 64)); break;
                case "glow": made = Make(128, 0, (x, y) => Glow(x, y, 128)); break;
                case "vignette": made = Make(128, 0, (x, y) => Vignette(x, y, 128)); break;
                case "hfade": made = Make(256, 0, (x, y) => Mathf.Sin(Mathf.Clamp01(x / 256f) * Mathf.PI)); break;
                case "vfade": made = Make(64, 0, (x, y) => Mathf.Clamp01(y / 64f)); break;
                default: made = Make(4, 0, (x, y) => 1f); break;
            }
            made.name = "EzAdmin " + key;
            made = Stored(key, made);
            Sprites[key] = made;
            return made;
        }

        private static Sprite Make(int size, int border, Func<float, float, float> alpha)
        {
            var texture = new Texture2D(size, size, TextureFormat.RGBA32, true)
            {
                hideFlags = HideFlags.HideAndDontSave,
                wrapMode = TextureWrapMode.Clamp,
                filterMode = FilterMode.Trilinear,
                anisoLevel = 4
            };
            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(true, SpriteStore == null);
            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 float RoundRect(float x, float y, int size, float r)
        {
            float cx = Mathf.Clamp(x, r, size - r), cy = Mathf.Clamp(y, r, size - r);
            float d = Mathf.Sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy));
            return Mathf.Clamp01(r - d + 0.5f);
        }

        private static float SoftBox(float x, float y, int size)
        {
            const float inset = 44f, r = 20f, blur = 38f;
            float cx = Mathf.Clamp(x, inset + r, size - inset - r), cy = Mathf.Clamp(y, inset + r, size - inset - r);
            float d = Mathf.Sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy)) - r;
            float t = Mathf.Clamp01(1f - (d + 6f) / blur);
            return t * t * (3f - 2f * t);
        }

        private static float Disc(float x, float y, int size, float r)
        {
            float d = Mathf.Sqrt((x - size / 2f) * (x - size / 2f) + (y - size / 2f) * (y - size / 2f));
            return Mathf.Clamp01(r - d + 0.5f);
        }

        private static float Ring(float x, float y, int size, float r, float width)
        {
            float d = Mathf.Sqrt((x - size / 2f) * (x - size / 2f) + (y - size / 2f) * (y - size / 2f));
            return Mathf.Clamp01(r - d + 0.5f) * Mathf.Clamp01(d - (r - width) + 0.5f);
        }

        private static float Arc(float x, float y, int size)
        {
            float dx = x - size / 2f, dy = y - size / 2f;
            float ring = Ring(x, y, size, size / 2f - 3f, 9f);
            float angle = (Mathf.Atan2(dy, dx) / (Mathf.PI * 2f) + 1f) % 1f;
            return ring * Mathf.SmoothStep(0.05f, 1f, angle);
        }

        private static float Segment(float x, float y, Vector2 a, Vector2 b, float width)
        {
            var p = new Vector2(x, y);
            var ab = b - a;
            float t = Mathf.Clamp01(Vector2.Dot(p - a, ab) / ab.sqrMagnitude);
            float d = Vector2.Distance(p, a + ab * t);
            return Mathf.Clamp01(width / 2f - d + 0.5f);
        }

        private static float Check(float x, float y, int size)
        {
            float s = size / 64f;
            return Mathf.Max(Segment(x, y, new Vector2(14, 33) * s, new Vector2(27, 20) * s, 8f * s),
                             Segment(x, y, new Vector2(27, 20) * s, new Vector2(50, 45) * s, 8f * s));
        }

        private static float Cross(float x, float y, int size)
        {
            float s = size / 64f;
            return Mathf.Max(Segment(x, y, new Vector2(18, 18) * s, new Vector2(46, 46) * s, 8f * s),
                             Segment(x, y, new Vector2(18, 46) * s, new Vector2(46, 18) * s, 8f * s));
        }

        private static float Glow(float x, float y, int size)
        {
            float d = Mathf.Sqrt((x - size / 2f) * (x - size / 2f) + (y - size / 2f) * (y - size / 2f)) / (size / 2f);
            float t = Mathf.Clamp01(1f - d);
            return t * t;
        }

        private static float Vignette(float x, float y, int size)
        {
            float dx = (x / size - 0.5f) * 2f, dy = (y / size - 0.5f) * 2f;
            float d = Mathf.Sqrt(dx * dx + dy * dy) / 1.414f;
            return Mathf.SmoothStep(0.45f, 1.05f, d);
        }

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