using TMPro;
using UnityEngine;

namespace Davanci.Nexus
{
    // Letter-by-letter reveal for a TextMesh Pro text: each character springs up (optionally
    // popping from a smaller scale) and fades in, staggered left to right. Once every letter
    // has landed, a gentle wave keeps it alive, and a glint (which can chase the reveal in)
    // travels across the letters. Driven by NexusSplashFx through SetElapsed; play mode only,
    // so the text stays untouched while editing. Letter spacing is TMP's own Character Spacing.
    [RequireComponent(typeof(TMP_Text))]
    public sealed class NexusSplashTextReveal : MonoBehaviour
    {
        [SerializeField] float stagger = 0.028f;
        [SerializeField] float duration = 0.5f;
        [SerializeField] float rise = 26f;
        [Tooltip("Letter scale when it appears; 1 = no pop.")]
        [SerializeField] float startScale = 1f;

        [Header("Wave and glint")]
        [Tooltip("Height of the letter wave (starts once every letter has landed), in canvas units; 0 = off.")]
        [SerializeField] float waveAmplitude;
        [SerializeField] float waveSpeed = 2.2f;
        [Tooltip("Seconds between glint passes; 0 = off.")]
        [SerializeField] float glintPeriod;
        [Tooltip("Seconds after the reveal starts before the first glint, so it can chase the letters in.")]
        [SerializeField] float glintDelay;
        [SerializeField] Color glintColor = Color.white;
        [Tooltip("How much a letter swells while the glint passes over it.")]
        [SerializeField] float glintScale = 0.08f;

        TMP_Text _text;
        float _elapsed = -1f;
        int _letters = 1;
        bool _applied, _applying;

        float End => (_letters - 1) * stagger + duration;
        bool Animated => waveAmplitude > 0f || glintPeriod > 0f;

        // True once every letter has landed.
        public bool Revealed => _applied && _elapsed >= End;

        // How far the reveal has got, 0 (not started) to 1 (every letter landed).
        public float Progress => _applied ? Mathf.Clamp01(_elapsed / End) : 0f;

        void Awake() => _text = GetComponent<TMP_Text>();

        void OnEnable() => TMPro_EventManager.TEXT_CHANGED_EVENT.Add(OnTextChanged);

        void OnDisable() => TMPro_EventManager.TEXT_CHANGED_EVENT.Remove(OnTextChanged);

        public void SetElapsed(float elapsed)
        {
            // Skip the work while nothing moves (before the start, and after the last letter
            // lands unless the wave or glint is running).
            bool still = _applied &&
                ((_elapsed < 0f && elapsed < 0f) || (!Animated && _elapsed >= End && elapsed >= End));
            _elapsed = elapsed;
            if (!still) Apply(regenerate: true);
        }

        // TMP rebuilt its mesh on its own (layout, font load): put the current frame back on it.
        void OnTextChanged(Object changed)
        {
            if (_applied && !_applying && changed == _text && Application.isPlaying)
                Apply(regenerate: false);
        }

        void Apply(bool regenerate)
        {
            if (_text == null || !Application.isPlaying) return;
            _applied = true;

            // Start from TMP's untouched layout, then move each character's quad. The rebuild
            // raises TEXT_CHANGED itself; the guard keeps that from applying the frame twice.
            if (regenerate)
            {
                _applying = true;
                _text.ForceMeshUpdate();
                _applying = false;
            }
            var info = _text.textInfo;

            int visible = 0;
            for (int i = 0; i < info.characterCount; i++)
                if (info.characterInfo[i].isVisible) visible++;
            _letters = Mathf.Max(1, visible);

            float idle = _elapsed - End;
            float idleIn = idle > 0f ? NexusEase.OutCubic(Mathf.Clamp01(idle / 0.6f)) : 0f;
            // The glint enters three letters before the first and leaves three after the last.
            float glintClock = _elapsed - glintDelay;
            float glintAt = glintPeriod > 0f && glintClock > 0f
                ? Mathf.Repeat(glintClock, glintPeriod) / glintPeriod * (_letters + 6f) - 3f
                : float.NegativeInfinity;

            int g = -1;
            for (int i = 0; i < info.characterCount; i++)
            {
                var ch = info.characterInfo[i];
                if (!ch.isVisible) continue;
                g++;

                float local = Mathf.Clamp01((_elapsed - g * stagger) / duration);
                float spring = NexusEase.OutBack(local, 2.2f);
                float fade = NexusEase.OutCubic(local);

                float d = (g - glintAt) / 1.3f;
                float glint = Mathf.Exp(-d * d) * fade;
                float wave = waveAmplitude * Mathf.Sin(idle * waveSpeed - g * 0.5f) * idleIn;

                float scale = Mathf.LerpUnclamped(startScale, 1f, spring) * (1f + glintScale * glint);
                var shift = new Vector3(0f, -(1f - spring) * rise + wave, 0f);

                var mesh = info.meshInfo[ch.materialReferenceIndex];
                int v0 = ch.vertexIndex;
                Vector3 centre = (mesh.vertices[v0] + mesh.vertices[v0 + 2]) * 0.5f;

                for (int k = 0; k < 4; k++)
                {
                    mesh.vertices[v0 + k] = centre + (mesh.vertices[v0 + k] - centre) * scale + shift;

                    var c = mesh.colors32[v0 + k];
                    byte alpha = c.a;
                    c = Color32.Lerp(c, glintColor, glint);
                    c.a = (byte)(alpha * fade);
                    mesh.colors32[v0 + k] = c;
                }
            }

            _text.UpdateVertexData(TMP_VertexDataUpdateFlags.Vertices | TMP_VertexDataUpdateFlags.Colors32);
        }
    }
}
