using System.Collections;
using UnityEngine;
using UnityEngine.UI;

namespace Davanci.Nexus
{
    // Code-driven splash motion. It opens on black, waits for launch hitches to pass and fades
    // up slowly. Intro: the cube's corners pop in and its edges trace, then the logo fills in
    // with a punch, a shockwave and a shine sweep, and the title writes itself in. Afterwards
    // a calm idle loop (float, breathing glow, twinkling corners, sparkles) runs for as long
    // as startup takes, and Outro() zooms through into the game. Every frame is a pure function of
    // elapsed unscaled time, so long first-launch waits (ATT prompt) never drift or stutter.
    [RequireComponent(typeof(CanvasGroup))]
    public sealed class NexusSplashFx : MonoBehaviour
    {
        [SerializeField] RectTransform logo;
        [SerializeField] RectTransform glow;
        [SerializeField] NexusSplashTextReveal title;
        [SerializeField] NexusSplashTextReveal tagline;
        [SerializeField] Slider progressBar;

        [Header("Palette")]
        [SerializeField] Color accent = new Color(0.47f, 0.46f, 1f, 1f);
        [SerializeField] Color backdrop = new Color(0.22f, 0.18f, 0.62f, 1f);

        // Intro beats are authored on a 1x clock and played Pace times slower.
        const float Pace = 1.5f;
        const float IntroSeconds = 4.2f;
        // Opening: black until frame times settle, a short black hold, then a slow fade up.
        const float SteadyFrameSeconds = 1f / 24f;
        const int SteadyFramesNeeded = 6;
        const float MaxSteadyWait = 2f;
        const float CurtainHold = 0.3f;
        const float CurtainFade = 1.0f;
        const float IntroLead = 0.4f;     // the intro starts this long into the fade
        // How fast the timeline plays on first launch (IntroSeconds / Speed of wall time).
        const float Speed = 1.35f;
        // Return visits: the same timeline played faster behind a shorter fade, so players get
        // into the game quickly once they have seen the full intro.
        const float ReturnSpeed = 2.8f;
        const float ReturnCurtainHold = 0.05f;
        const float ReturnCurtainFade = 0.35f;
        const float ReturnIntroLead = 0.1f;
        // Permission prompts appear once this much of the title is written.
        const float PromptsAtTitle = 0.5f;
        const float OutroSeconds = 0.5f;
        const float BarHeight = 10f;
        const int SparkleCount = 22;

        // Cube corners as a fraction of the logo's size (y up), traced from logo-icon-cube.png.
        static readonly Vector2[] Corners =
        {
            new Vector2(0f, 0.309f),       // top
            new Vector2(0.268f, 0.154f),   // upper right
            new Vector2(0.268f, -0.154f),  // lower right
            new Vector2(0f, -0.307f),      // bottom
            new Vector2(-0.268f, -0.154f), // lower left
            new Vector2(-0.268f, 0.154f),  // upper left
            new Vector2(0f, 0f),           // centre
        };

        // The outline runs around the hexagon first, then the three inner edges leave the centre.
        static readonly int[,] Edges = { { 0, 1 }, { 1, 2 }, { 2, 3 }, { 3, 4 }, { 4, 5 }, { 5, 0 }, { 6, 5 }, { 6, 1 }, { 6, 3 } };

        struct Sparkle
        {
            public RawImage Image;
            public Color Color;
            public Vector2 Origin;   // normalised screen position at t = 0
            public float Speed;      // screen heights per second
            public float Phase, Rate;
        }

        CanvasGroup _group;
        RectTransform _root, _rig, _wire, _sparkleLayer;
        Vector2 _rigHome;
        float _logoSize;
        Image _logoImage;
        Color _logoColor;
        RawImage _vignette, _grid, _ringA, _ringB, _shine;
        Graphic _glowGraphic;
        readonly RawImage[] _dots = new RawImage[7];
        readonly RawImage[] _halos = new RawImage[7];
        Image[] _edges;
        float[] _edgeLengths;
        Sparkle[] _sparkles;
        CanvasGroup _barGroup;
        RectTransform _barFill;
        RawImage _barShimmer, _barHead;
        Image _curtain;

        bool _built, _waiting, _playing, _wireframe = true;
        int _steadyFrames;
        float _waitStart, _curtainStart = -1f, _start, _outroStart = -1f, _shown, _target;
        float _speed = Speed, _curtainHold = CurtainHold, _curtainFade = CurtainFade, _introLead = IntroLead;

        static float Now => Time.unscaledTime;
        public float ShownProgress => _shown;

        // Timeline position: seconds since the intro started, sped up on return visits.
        float Elapsed => (Now - _start) * _speed;

        // True once the full intro has played; the controller never leaves before this.
        public bool IntroDone => _playing && Elapsed >= IntroSeconds;
        public float IntroProgress => _playing ? Mathf.Clamp01(Elapsed / IntroSeconds) : 0f;

        // The brand is on screen (the title is half written): permission prompts may appear.
        public bool ReadyForPrompts => _playing &&
            (title != null ? title.Progress >= PromptsAtTitle : IntroProgress >= PromptsAtTitle);

        public void Play(bool wireframe, bool returnVisit = false)
        {
            Build();
            _wireframe = wireframe;
            _wire.gameObject.SetActive(wireframe);
            if (returnVisit)
            {
                _speed = ReturnSpeed;
                _curtainHold = ReturnCurtainHold;
                _curtainFade = ReturnCurtainFade;
                _introLead = ReturnIntroLead;
            }
            _waiting = true;
            _waitStart = Now;
        }

        public void SetProgress(float value) => _target = Mathf.Clamp01(value);

        public IEnumerator Outro()
        {
            _outroStart = Now;
            while (Now - _outroStart < OutroSeconds)
                yield return null;
            Apply(Elapsed);
        }

        void Awake()
        {
            Build();
            Apply(0f);
        }

        void Update()
        {
            if (_waiting) WaitForSteadyFrames();
            if (!_playing) return;

            _shown = Mathf.Lerp(_shown, _target, 1f - Mathf.Exp(-9f * Time.unscaledDeltaTime));
            if (progressBar != null) progressBar.value = _shown;

            Apply(Elapsed);
        }

        void OnDestroy() => NexusSplashTextures.Release();

        // Launch frames hitch (scene load, SDK start-up, shader warm-up). Hold the black curtain
        // until frame times settle so the opening beats are actually seen.
        void WaitForSteadyFrames()
        {
            _steadyFrames = Time.unscaledDeltaTime < SteadyFrameSeconds ? _steadyFrames + 1 : 0;
            if (_steadyFrames < SteadyFramesNeeded && Now - _waitStart < MaxSteadyWait) return;

            _waiting = false;
            _curtainStart = Now + _curtainHold;
            _start = _curtainStart + _introLead;
            _playing = true;
        }

        // ---- Frame ------------------------------------------------------------------------

        void Apply(float t)
        {
            float it = t / Pace;
            // Logo beats start earlier when there is no wireframe to draw first (custom logo).
            float lt = _wireframe ? it : it + 0.55f;
            float idle = Mathf.Max(0f, t - IntroSeconds);
            float settle = NexusEase.OutCubic(NexusEase.Window(t, IntroSeconds - 0.4f, 0.8f));
            float outro = _outroStart < 0f ? 0f : Mathf.Clamp01((Now - _outroStart) / OutroSeconds);

            ApplyBackdrop(t, settle);
            ApplySparkles(t);
            ApplyRig(lt, idle, settle, outro);
            if (_wireframe) ApplyWireframe(it, lt, idle, settle);
            ApplyBar(lt, t);

            if (title != null) title.SetElapsed(lt - 1.15f);
            if (tagline != null) tagline.SetElapsed(lt - 1.5f);

            SetAlpha(_curtain, Color.black, _curtainStart < 0f
                ? 1f
                : 1f - NexusEase.InOutCubic(NexusEase.Window(Now, _curtainStart, _curtainFade)));

            _group.alpha = _outroStart < 0f
                ? 1f
                : 1f - NexusEase.InOutCubic(NexusEase.Window(Now - _outroStart, 0.15f, OutroSeconds - 0.15f));
        }

        void ApplyBackdrop(float t, float settle)
        {
            SetAlpha(_vignette, backdrop,
                0.9f * NexusEase.OutCubic(NexusEase.Window(t, 0f, 0.8f)) + 0.07f * Mathf.Sin(t * 1.1f) * settle);

            var size = _root.rect.size;
            _grid.uvRect = new Rect(t * 0.05f, t * 0.12f, size.x / 40f, size.y / 40f);
            SetAlpha(_grid, accent, 0.11f * NexusEase.OutCubic(NexusEase.Window(t, 0.2f, 1.2f)));
        }

        void ApplySparkles(float t)
        {
            var size = _root.rect.size;
            float fade = NexusEase.OutCubic(NexusEase.Window(t, 0.5f, 1.0f));

            foreach (var s in _sparkles)
            {
                float y = Mathf.Repeat(s.Origin.y + s.Speed * t, 1.1f) - 0.05f;
                float x = s.Origin.x + 0.012f * Mathf.Sin(t * s.Rate + s.Phase);
                s.Image.rectTransform.anchoredPosition = new Vector2((x - 0.5f) * size.x, (y - 0.5f) * size.y);

                float twinkle = 0.35f + 0.65f * (0.5f + 0.5f * Mathf.Sin(t * s.Rate * 2f + s.Phase));
                float edges = Mathf.Sin(Mathf.Clamp01(y) * Mathf.PI);
                SetAlpha(s.Image, s.Color, fade * twinkle * edges);
            }
        }

        void ApplyRig(float lt, float idle, float settle, float outro)
        {
            // Float while idle; on exit a small anticipation dip, then zoom through.
            _rig.anchoredPosition = _rigHome + new Vector2(0f, 9f * Mathf.Sin(idle * 1.5f) * settle);
            _rig.localScale = Vector3.one * (1f + 0.22f * NexusEase.InBack(outro, 2.4f));

            float appear = NexusEase.OutBack(NexusEase.Window(lt, 0.85f, 0.55f), 2f);
            float punch = 0.07f * NexusEase.Bump(lt, 1.28f, 0.32f);
            logo.localScale = Vector3.one * (Mathf.LerpUnclamped(0.86f, 1f, appear) + punch);
            _logoImage.fillAmount = NexusEase.OutCubic(NexusEase.Window(lt, 0.85f, 0.5f));
            SetAlpha(_logoImage, _logoColor, NexusEase.OutCubic(NexusEase.Window(lt, 0.8f, 0.25f)));

            float glowIn = NexusEase.OutCubic(NexusEase.Window(lt, 0.6f, 0.9f));
            glow.localScale = Vector3.one * (Mathf.Lerp(0.6f, 1f, glowIn) + 0.035f * Mathf.Sin(idle * 1.3f) * settle);
            SetAlpha(_glowGraphic, accent,
                0.3f * glowIn + 0.06f * Mathf.Sin(idle * 2.1f) * settle + 0.35f * Mathf.Sin(outro * Mathf.PI));

            if (_outroStart >= 0f)
                ApplyRing(_ringA, Now - _outroStart, 0.6f, 1f, 3.2f, 0.6f);
            else
                ApplyRing(_ringA, lt - 1.3f, 0.85f, 1f, 2.7f, 0.75f);
            ApplyRing(_ringB, lt - 1.42f, 1f, 1f, 2f, 0.45f);

            // First sweep right after the fill lands, then one every 4 s while idle.
            float sweep = lt < 1.45f ? -1f : Mathf.Repeat(lt - 1.45f, 4f) / 0.6f;
            bool sweeping = sweep >= 0f && sweep <= 1f;
            if (sweeping)
                _shine.rectTransform.anchoredPosition =
                    new Vector2(Mathf.Lerp(-0.85f, 0.85f, NexusEase.InOutCubic(sweep)) * _logoSize, 0f);
            SetAlpha(_shine, Color.white, sweeping ? 0.5f * Mathf.Sin(sweep * Mathf.PI) : 0f);
        }

        void ApplyRing(RawImage ring, float local, float duration, float from, float to, float peak)
        {
            float w = local / duration;
            if (w <= 0f || w >= 1f) { SetAlpha(ring, accent, 0f); return; }

            ring.rectTransform.sizeDelta = Vector2.one * (_logoSize * Mathf.Lerp(from, to, NexusEase.OutCubic(w)));
            SetAlpha(ring, accent, peak * (1f - w) * (1f - w) * Mathf.Clamp01(w * 12f));
        }

        void ApplyWireframe(float t, float lt, float idle, float settle)
        {
            // Edges trace in, then hand over to the filled logo, which draws its own.
            float handover = 1f - NexusEase.Window(t, 1.1f, 0.35f);
            for (int i = 0; i < _edges.Length; i++)
            {
                float grow = NexusEase.OutCubic(NexusEase.Window(t, 0.28f + i * 0.055f, 0.24f));
                _edges[i].rectTransform.sizeDelta = new Vector2(grow * _edgeLengths[i], 4f);
                SetAlpha(_edges[i], Color.Lerp(accent, Color.white, 0.45f), handover);
            }

            float flash = NexusEase.Bump(lt, 1.28f, 0.4f);
            for (int i = 0; i < _dots.Length; i++)
            {
                float pop = NexusEase.OutBack(NexusEase.Window(t, 0.08f + i * 0.065f, 0.5f), 2.6f);
                float wave = Mathf.Pow(Mathf.Max(0f, Mathf.Sin(idle * 2.6f - i * 0.75f)), 3f) * settle;

                _dots[i].rectTransform.localScale = Vector3.one * pop;
                _halos[i].rectTransform.localScale = Vector3.one * (pop * (1f + 0.6f * flash + 0.45f * wave));
                SetAlpha(_halos[i], accent, 0.55f + 0.35f * wave + 0.3f * flash);
            }
        }

        void ApplyBar(float lt, float t)
        {
            if (_barGroup == null) return;

            _barGroup.alpha = NexusEase.OutCubic(NexusEase.Window(lt, 1.2f, 0.45f));
            float grow = NexusEase.OutBack(NexusEase.Window(lt, 1.2f, 0.55f));
            progressBar.transform.localScale = new Vector3(Mathf.LerpUnclamped(0.6f, 1f, grow), 1f, 1f);

            float width = _barFill.rect.width;
            float pass = Mathf.Repeat(t * 0.9f, 1.4f) / 1.4f;
            _barShimmer.rectTransform.anchoredPosition = new Vector2(pass * (width + 120f) - 60f, 0f);
            SetAlpha(_barShimmer, Color.white, 0.55f);

            float head = Mathf.Clamp01(_shown * 20f);
            _barHead.rectTransform.anchoredPosition = new Vector2(((RectTransform)_barFill.parent).rect.width * _shown, 0f);
            _barHead.rectTransform.localScale = Vector3.one * (0.9f + 0.15f * Mathf.Sin(t * 6f));
            SetAlpha(_barHead, Color.Lerp(accent, Color.white, 0.3f), head * (0.7f + 0.3f * Mathf.Sin(t * 6f)));
        }

        // ---- Build ------------------------------------------------------------------------

        void Build()
        {
            if (_built) return;
            _built = true;

            NexusSplashTextures.Ensure();
            _group = GetComponent<CanvasGroup>();
            _root = (RectTransform)transform;

            // Backdrop layers sit behind everything already on the canvas.
            _vignette = NewRaw("Vignette", _root, NexusSplashTextures.Vignette, backdrop, Vector2.zero);
            Stretch(_vignette.rectTransform, 260f);
            _grid = NewRaw("DotGrid", _root, NexusSplashTextures.Grid, accent, Vector2.zero);
            Stretch(_grid.rectTransform, 0f);
            _sparkleLayer = NewRect("Sparkles", _root);
            Stretch(_sparkleLayer, 0f);
            _vignette.transform.SetSiblingIndex(0);
            _grid.transform.SetSiblingIndex(1);
            _sparkleLayer.SetSiblingIndex(2);
            BuildSparkles();

            BuildRig();
            if (progressBar != null) BuildBar();

            var curtain = NewRect("Curtain", _root);
            Stretch(curtain, 0f);
            _curtain = curtain.gameObject.AddComponent<Image>();
            _curtain.color = Color.black;
            _curtain.raycastTarget = false;
        }

        void BuildSparkles()
        {
            var rng = new System.Random(7);
            float Next(float min, float max) => min + (float)rng.NextDouble() * (max - min);

            _sparkles = new Sparkle[SparkleCount];
            for (int i = 0; i < SparkleCount; i++)
            {
                float size = Next(5f, 13f);
                _sparkles[i] = new Sparkle
                {
                    Image = NewRaw("Sparkle", _sparkleLayer, NexusSplashTextures.SoftDot, Color.clear, Vector2.one * size * 2.2f),
                    Color = Color.Lerp(accent, Color.white, Next(0.2f, 0.8f)),
                    Origin = new Vector2(Next(0.04f, 0.96f), Next(0f, 1.1f)),
                    Speed = Next(0.015f, 0.045f),
                    Phase = Next(0f, Mathf.PI * 2f),
                    Rate = Next(0.8f, 2.2f),
                };
            }
        }

        void BuildRig()
        {
            // Glow, rings, logo and wireframe float and zoom together.
            _rig = NewRect("LogoRig", _root);
            _rig.anchorMin = logo.anchorMin;
            _rig.anchorMax = logo.anchorMax;
            _rig.sizeDelta = logo.sizeDelta;
            _rigHome = _rig.anchoredPosition = logo.anchoredPosition;
            _rig.SetSiblingIndex(logo.GetSiblingIndex());
            _logoSize = logo.rect.width;

            glow.SetParent(_rig, false);
            glow.anchoredPosition = Vector2.zero;
            _glowGraphic = glow.GetComponent<Graphic>();

            _ringA = NewRaw("RingA", _rig, NexusSplashTextures.Ring, Color.clear, Vector2.one * _logoSize);
            _ringB = NewRaw("RingB", _rig, NexusSplashTextures.Ring, Color.clear, Vector2.one * _logoSize);

            logo.SetParent(_rig, false);
            logo.anchoredPosition = Vector2.zero;
            _logoImage = logo.GetComponent<Image>();
            _logoColor = _logoImage.color;
            _logoImage.type = Image.Type.Filled;
            _logoImage.fillMethod = Image.FillMethod.Radial360;
            _logoImage.fillOrigin = (int)Image.Origin360.Top;
            _logoImage.fillClockwise = true;
            if (logo.GetComponent<Mask>() == null)
                logo.gameObject.AddComponent<Mask>().showMaskGraphic = true;

            _shine = NewRaw("Shine", logo, NexusSplashTextures.Shine, Color.clear,
                new Vector2(_logoSize * 0.3f, _logoSize * 1.6f));
            _shine.rectTransform.localRotation = Quaternion.Euler(0f, 0f, -25f);

            BuildWireframe();
        }

        void BuildWireframe()
        {
            _wire = NewRect("Wireframe", _rig);
            Stretch(_wire, 0f);

            int count = Edges.GetLength(0);
            _edges = new Image[count];
            _edgeLengths = new float[count];
            for (int i = 0; i < count; i++)
            {
                Vector2 a = Corners[Edges[i, 0]] * _logoSize, b = Corners[Edges[i, 1]] * _logoSize, d = b - a;
                var rt = NewRect("Edge", _wire);
                rt.pivot = new Vector2(0f, 0.5f);
                rt.anchoredPosition = a;
                rt.localRotation = Quaternion.Euler(0f, 0f, Mathf.Atan2(d.y, d.x) * Mathf.Rad2Deg);
                var img = rt.gameObject.AddComponent<Image>();
                img.raycastTarget = false;
                _edges[i] = img;
                _edgeLengths[i] = d.magnitude;
            }

            for (int i = 0; i < Corners.Length; i++)
            {
                Vector2 at = Corners[i] * _logoSize;
                _halos[i] = NewRaw("Halo", _wire, NexusSplashTextures.SoftDot, Color.clear, Vector2.one * 72f);
                _halos[i].rectTransform.anchoredPosition = at;
                _dots[i] = NewRaw("Corner", _wire, NexusSplashTextures.Disc, Color.white, Vector2.one * 18f);
                _dots[i].rectTransform.anchoredPosition = at;
            }
        }

        void BuildBar()
        {
            progressBar.interactable = false;
            progressBar.transition = Selectable.Transition.None;
            _barGroup = progressBar.GetComponent<CanvasGroup>();
            if (_barGroup == null) _barGroup = progressBar.gameObject.AddComponent<CanvasGroup>();
            _barGroup.blocksRaycasts = false;

            _barFill = progressBar.fillRect;
            var track = progressBar.transform.Find("Background");
            if (track != null) StylePill(track as RectTransform, new Color(1f, 1f, 1f, 0.09f));
            if (_barFill == null) return;

            if (_barFill.parent is RectTransform area) CentreLine(area);
            _barFill.sizeDelta = Vector2.zero;
            _barFill.anchoredPosition = Vector2.zero;
            StylePill(_barFill, accent, centre: false);
            _barFill.gameObject.AddComponent<RectMask2D>();

            _barShimmer = NewRaw("Shimmer", _barFill, NexusSplashTextures.Shine, Color.clear, new Vector2(120f, BarHeight));
            _barShimmer.rectTransform.anchorMin = _barShimmer.rectTransform.anchorMax = new Vector2(0f, 0.5f);

            // The head glow lives outside the mask so it can bloom past the bar; it follows the fill's edge.
            _barHead = NewRaw("Head", _barFill.parent, NexusSplashTextures.SoftDot, Color.clear, Vector2.one * 38f);
            _barHead.rectTransform.anchorMin = _barHead.rectTransform.anchorMax = new Vector2(0f, 0.5f);
        }

        void StylePill(RectTransform rt, Color color, bool centre = true)
        {
            if (rt == null) return;
            if (centre) CentreLine(rt);
            var img = rt.GetComponent<Image>();
            if (img == null) return;
            img.sprite = NexusSplashTextures.Pill;
            img.type = Image.Type.Sliced;
            img.pixelsPerUnitMultiplier = NexusSplashTextures.PillRadius / (BarHeight * 0.5f);
            img.color = color;
            img.raycastTarget = false;
        }

        static void CentreLine(RectTransform rt)
        {
            rt.anchorMin = new Vector2(0f, 0.5f);
            rt.anchorMax = new Vector2(1f, 0.5f);
            rt.anchoredPosition = Vector2.zero;
            rt.sizeDelta = new Vector2(0f, BarHeight);
        }

        // ---- Helpers ----------------------------------------------------------------------

        static RectTransform NewRect(string name, Transform parent)
        {
            var go = new GameObject(name, typeof(RectTransform)) { layer = parent.gameObject.layer };
            var rt = (RectTransform)go.transform;
            rt.SetParent(parent, false);
            return rt;
        }

        static RawImage NewRaw(string name, Transform parent, Texture texture, Color color, Vector2 size)
        {
            var rt = NewRect(name, parent);
            rt.sizeDelta = size;
            var img = rt.gameObject.AddComponent<RawImage>();
            img.texture = texture;
            img.color = color;
            img.raycastTarget = false;
            return img;
        }

        static void Stretch(RectTransform rt, float bleed)
        {
            rt.anchorMin = Vector2.zero;
            rt.anchorMax = Vector2.one;
            rt.offsetMin = new Vector2(-bleed, -bleed);
            rt.offsetMax = new Vector2(bleed, bleed);
        }

        static void SetAlpha(Graphic g, Color color, float alpha)
        {
            color.a *= Mathf.Clamp01(alpha);
            g.color = color;
        }
    }
}
