// ---------------------------------------------------------------------------
//  SquishyProceduralAnimator.cs
//  AppalachAI Asset Forge  -  Procedural "Squishy" Animator (Unity)
// ---------------------------------------------------------------------------
//  Made with care by AppalachAI Studios -- where mountain craft meets AI making.
//
//  WHAT THIS IS
//  ------------
//  A single, self-contained MonoBehaviour that brings a *skeleton-free* mesh to
//  life with squash, stretch, bounce and wobble. No rig, no bones, no external
//  tween libraries -- just honest math on a Transform's scale, position and
//  rotation. Drop it on (or near) a single-mesh character and call the methods.
//
//  It is built for the "single_mesh_procedural" asset class: one mesh, one
//  Transform, animated relative to a captured base pose so your art always
//  settles back home when the motion is done.
//
//  THE API (the same friendly surface AppalachAI ships in every engine)
//  -------------------------------------------------------------------
//    PlayIdle(target, profile?)      gentle looping bounce + subtle squash
//    PlaySquish(target, intensity)   fast squash then elastic rebound (tap/hit)
//    PlayStretch(target, direction)  elongate along a direction (jump/launch)
//    PlayWobble(target, intensity)   damped jelly oscillation after a big hit
//    PlayCelebrate(target, rarity)   excited multi-bounce + optional sparkle
//    Stop(target)                    cancel motion, restore the captured pose
//
//  QUICK START
//  -----------
//      // 1) Add this component to your character (the mesh you want to squish).
//      // 2) From your own script, grab a reference and call away:
//
//      public class Critter : MonoBehaviour
//      {
//          [SerializeField] SquishyProceduralAnimator squishy;
//
//          void Start()      { squishy.PlayIdle(transform); }
//          void OnTapped()   { squishy.PlaySquish(transform, 0.8f); }
//          void OnJump()     { squishy.PlayStretch(transform, Vector3.up); }
//          void OnBigHit()   { squishy.PlayWobble(transform, 1f); }
//          void OnWin()      { squishy.PlayCelebrate(transform, Rarity.Legendary); }
//          void OnDespawn()  { squishy.Stop(transform); }
//      }
//
//  A FEW HONEST NOTES
//  ------------------
//  - You can animate one target or many; each target keeps its own state and
//    settles back to the pose captured the first time it was used.
//  - These are tunable, springy approximations -- tweak the [SerializeField]
//    values until they feel right for your art. There's no single "correct"
//    bounce; trust your eyes.
//  - All sample IP and bundled motion presets are owned by AppalachAI Studios.
//
//  (c) AppalachAI Studios. Family-friendly, maker-made, mountain-grown.
// ---------------------------------------------------------------------------

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace AppalachAI.AssetForge
{
    /// <summary>
    /// Rarity tiers used by <see cref="SquishyProceduralAnimator.PlayCelebrate"/>
    /// to scale up the excitement and tint any optional sparkle particles.
    /// </summary>
    public enum Rarity
    {
        /// <summary>Everyday find -- a small, happy hop.</summary>
        Common,
        /// <summary>A little something special -- a touch more pep.</summary>
        Uncommon,
        /// <summary>A genuine score -- bigger bounces, brighter sparkle.</summary>
        Rare,
        /// <summary>A real keepsake -- exuberant, with warm shimmer.</summary>
        Epic,
        /// <summary>Once-in-a-blue-moon -- the full celebration.</summary>
        Legendary
    }

    /// <summary>
    /// Optional tuning bundle for <see cref="SquishyProceduralAnimator.PlayIdle"/>.
    /// Pass one to flavor an idle (sleepy, perky, etc.); pass nothing to use the
    /// component's serialized defaults. Values are multipliers / seconds so they
    /// read the same no matter how big your art is.
    /// </summary>
    [System.Serializable]
    public class IdleProfile
    {
        /// <summary>Seconds for one full bob-up-and-down cycle. Bigger = slower, calmer.</summary>
        public float cycleSeconds = 2.2f;

        /// <summary>Multiplier on the component's <c>bounceHeight</c> for this idle.</summary>
        public float bounceScale = 1f;

        /// <summary>Multiplier on the component's <c>squashAmount</c> for the breathing squash.</summary>
        public float squashScale = 0.5f;

        /// <summary>A handy "calm" preset for sleeping or resting characters.</summary>
        public static IdleProfile Calm => new IdleProfile { cycleSeconds = 3.4f, bounceScale = 0.6f, squashScale = 0.35f };

        /// <summary>A handy "perky" preset for eager, lively characters.</summary>
        public static IdleProfile Perky => new IdleProfile { cycleSeconds = 1.5f, bounceScale = 1.35f, squashScale = 0.75f };
    }

    /// <summary>
    /// Reusable, skeleton-free procedural animator for a single mesh. Capture a
    /// base pose once, then squash, stretch, bounce and wobble relative to it.
    /// Tracks per-<see cref="Transform"/> state so any number of characters can
    /// animate independently from one shared component (or one each -- your call).
    /// </summary>
    /// <remarks>
    /// Pure and self-contained: motion is driven by coroutines plus
    /// <see cref="Mathf"/> / <see cref="AnimationCurve"/> easing. No external
    /// animation packages required. Built by AppalachAI Studios for the
    /// Asset Forge "single_mesh_procedural" pipeline.
    /// </remarks>
    [AddComponentMenu("AppalachAI/Asset Forge/Squishy Procedural Animator")]
    public class SquishyProceduralAnimator : MonoBehaviour
    {
        // ---------------------------------------------------------------
        //  Tunables -- nudge these in the Inspector until the feel is yours.
        // ---------------------------------------------------------------

        [Header("Bounce / Bob")]
        [Tooltip("How far the character lifts on a full bounce, in local units.")]
        [SerializeField] private float bounceHeight = 0.25f;

        [Tooltip("How strongly the mesh squashes & stretches (0 = none, ~0.5 = lively jelly).")]
        [Range(0f, 0.9f)]
        [SerializeField] private float squashAmount = 0.35f;

        [Tooltip("How quickly a wobble calms down. Lower = floppier and longer; higher = stiffer.")]
        [Range(0.5f, 12f)]
        [SerializeField] private float wobbleDamping = 4f;

        [Header("Timing (seconds)")]
        [Tooltip("Length of a single squish-and-rebound at full intensity.")]
        [SerializeField] private float squishDuration = 0.45f;

        [Tooltip("Length of a stretch-and-recover at full strength.")]
        [SerializeField] private float stretchDuration = 0.5f;

        [Tooltip("How long a full wobble runs before fully settling.")]
        [SerializeField] private float wobbleDuration = 1.4f;

        [Tooltip("Max side-to-side tilt of a wobble, in degrees, at full intensity.")]
        [SerializeField] private float wobbleTiltDegrees = 14f;

        [Header("Celebrate")]
        [Tooltip("How many happy hops a Common-rarity celebrate plays (scales up with rarity).")]
        [SerializeField] private int celebrateBaseHops = 2;

        [Tooltip("Optional sparkle/confetti system. Spawned & tinted by rarity on PlayCelebrate. Leave empty to skip particles.")]
        [SerializeField] private ParticleSystem celebrateParticles;

        [Header("Easing curves (optional -- sensible springs are used if left empty)")]
        [Tooltip("Drives the squash phase of PlaySquish (0..1 over normalized time).")]
        [SerializeField] private AnimationCurve squishEase;

        [Tooltip("Drives the elastic rebound of PlaySquish (0..1, may overshoot above 1).")]
        [SerializeField] private AnimationCurve reboundEase;

        // ---------------------------------------------------------------
        //  Per-target state -- the heart of "many characters, one component".
        // ---------------------------------------------------------------

        /// <summary>
        /// Everything we remember about a single animated target: its captured
        /// base pose and the coroutine currently driving it (if any).
        /// </summary>
        private sealed class TargetState
        {
            public Vector3 baseScale;
            public Vector3 basePosition;
            public Quaternion baseRotation;
            public bool captured;
            public Coroutine routine;
        }

        private readonly Dictionary<Transform, TargetState> _states = new Dictionary<Transform, TargetState>();

        // ---------------------------------------------------------------
        //  Lifecycle
        // ---------------------------------------------------------------

        /// <summary>
        /// Captures the base local scale and position of the GameObject this
        /// component lives on, so the most common single-character setup just
        /// works without any extra wiring.
        /// </summary>
        private void Awake()
        {
            // Pre-capture our own transform. Other targets are captured lazily
            // the first time they're handed to one of the Play methods.
            EnsureState(transform);
        }

        // ---------------------------------------------------------------
        //  Public API
        // ---------------------------------------------------------------

        /// <summary>
        /// Gentle looping bounce with a subtle breathing squash -- the "this
        /// character is alive" motion. Loops until <see cref="Stop"/> is called
        /// (or another Play call replaces it).
        /// </summary>
        /// <param name="target">The single-mesh transform to animate.</param>
        /// <param name="profile">Optional flavor (calm, perky...). Null uses defaults.</param>
        public void PlayIdle(Transform target, IdleProfile profile = null)
        {
            if (target == null) return;
            TargetState s = EnsureState(target);
            StartMotion(target, s, IdleRoutine(target, s, profile ?? new IdleProfile()));
        }

        /// <summary>
        /// Fast squash followed by an elastic rebound -- great for taps, hits,
        /// landings and stomps. One-shot; returns to the captured base pose.
        /// </summary>
        /// <param name="target">The single-mesh transform to animate.</param>
        /// <param name="intensity">How hard the squish hits, clamped to 0..1.</param>
        public void PlaySquish(Transform target, float intensity)
        {
            if (target == null) return;
            TargetState s = EnsureState(target);
            intensity = Mathf.Clamp01(intensity);
            StartMotion(target, s, SquishRoutine(target, s, intensity));
        }

        /// <summary>
        /// Elongates the mesh along a direction (think jump or launch), then
        /// eases back home. One-shot.
        /// </summary>
        /// <param name="target">The single-mesh transform to animate.</param>
        /// <param name="direction">
        /// World-space direction to stretch toward; length is treated as extra
        /// emphasis (longer vector = stronger stretch). <see cref="Vector3.up"/>
        /// is the classic jump.
        /// </param>
        public void PlayStretch(Transform target, Vector3 direction)
        {
            if (target == null) return;
            TargetState s = EnsureState(target);
            StartMotion(target, s, StretchRoutine(target, s, direction));
        }

        /// <summary>
        /// Damped, jelly-like side-to-side oscillation -- the lingering wobble
        /// that sells a big squish. One-shot that decays to stillness.
        /// </summary>
        /// <param name="target">The single-mesh transform to animate.</param>
        /// <param name="intensity">Starting wobble strength, clamped to 0..1.</param>
        public void PlayWobble(Transform target, float intensity)
        {
            if (target == null) return;
            TargetState s = EnsureState(target);
            intensity = Mathf.Clamp01(intensity);
            StartMotion(target, s, WobbleRoutine(target, s, intensity));
        }

        /// <summary>
        /// Excited multi-bounce celebration, scaled by <paramref name="rarity"/>,
        /// with an optional sparkle burst tinted to match the tier. One-shot.
        /// </summary>
        /// <param name="target">The single-mesh transform to animate.</param>
        /// <param name="rarity">Higher tiers bounce more and sparkle warmer.</param>
        public void PlayCelebrate(Transform target, Rarity rarity)
        {
            if (target == null) return;
            TargetState s = EnsureState(target);
            StartMotion(target, s, CelebrateRoutine(target, s, rarity));
        }

        /// <summary>
        /// Cancels any active motion on <paramref name="target"/> and snaps it
        /// back to the pose captured on first use. Safe to call any time, even
        /// if nothing is currently playing.
        /// </summary>
        /// <param name="target">The transform to settle.</param>
        public void Stop(Transform target)
        {
            if (target == null) return;
            if (!_states.TryGetValue(target, out TargetState s)) return;

            if (s.routine != null)
            {
                StopCoroutine(s.routine);
                s.routine = null;
            }
            RestoreBase(target, s);
        }

        // ---------------------------------------------------------------
        //  Coroutines -- the actual motion
        // ---------------------------------------------------------------

        private IEnumerator IdleRoutine(Transform target, TargetState s, IdleProfile profile)
        {
            float cycle = Mathf.Max(0.05f, profile.cycleSeconds);
            float lift = bounceHeight * profile.bounceScale;
            float squash = squashAmount * profile.squashScale;
            float t = 0f;

            while (true)
            {
                if (target == null) yield break;

                t += Time.deltaTime;
                // Normalized phase 0..1 around the loop.
                float phase = (t / cycle) % 1f;
                // Smooth vertical bob: a clean sine feels like easy breathing.
                float bob = Mathf.Sin(phase * Mathf.PI * 2f);
                // Positive bob => lift up a touch; convert -1..1 to a soft 0..1 rise.
                float rise = (bob * 0.5f + 0.5f) * lift;

                // Squash is the inverse of the rise: low point = wide & short.
                float squashT = -bob; // +1 at the bottom of the bob
                ApplySquashStretch(target, s, Vector3.up, squashT * squash * 0.5f);
                target.localPosition = s.basePosition + Vector3.up * rise;

                yield return null;
            }
        }

        private IEnumerator SquishRoutine(Transform target, TargetState s, float intensity)
        {
            float amount = squashAmount * Mathf.Lerp(0.4f, 1.4f, intensity);
            float dur = squishDuration;
            float squishPortion = 0.30f; // first chunk slams down, rest rebounds
            float t = 0f;

            while (t < dur)
            {
                if (target == null) yield break;
                t += Time.deltaTime;
                float nt = Mathf.Clamp01(t / dur);

                float squashT;
                if (nt < squishPortion)
                {
                    // Drive into the squash quickly.
                    float p = nt / squishPortion;
                    squashT = EvalSquish(p); // 0 -> 1
                }
                else
                {
                    // Elastic rebound back toward (and slightly past) base.
                    float p = (nt - squishPortion) / (1f - squishPortion);
                    float rebound = EvalRebound(p); // 1 -> 0 with a little overshoot
                    squashT = rebound;
                }

                // Negative squashT here means "squash down" (shorter, wider).
                ApplySquashStretch(target, s, Vector3.up, -squashT * amount);
                yield return null;
            }

            RestoreBase(target, s);
            s.routine = null;
        }

        private IEnumerator StretchRoutine(Transform target, TargetState s, Vector3 direction)
        {
            Vector3 axis = direction.sqrMagnitude > 0.0001f ? direction.normalized : Vector3.up;
            // Emphasis: a longer input vector stretches a bit more (capped, gently).
            float emphasis = Mathf.Clamp(direction.magnitude, 0.5f, 2f);
            float amount = squashAmount * Mathf.Lerp(0.8f, 1.5f, (emphasis - 0.5f) / 1.5f);
            float dur = stretchDuration;
            float risePortion = 0.4f;
            float t = 0f;

            while (t < dur)
            {
                if (target == null) yield break;
                t += Time.deltaTime;
                float nt = Mathf.Clamp01(t / dur);

                float stretchT;
                if (nt < risePortion)
                {
                    float p = nt / risePortion;
                    stretchT = EaseOutCubic(p); // 0 -> 1, snappy launch
                }
                else
                {
                    float p = (nt - risePortion) / (1f - risePortion);
                    stretchT = 1f - EaseInOutCubic(p); // settle 1 -> 0
                }

                // Positive => elongate along the axis.
                ApplySquashStretch(target, s, axis, stretchT * amount);
                // A little travel in the stretch direction sells the launch.
                target.localPosition = s.basePosition + axis * (stretchT * bounceHeight * emphasis);
                yield return null;
            }

            RestoreBase(target, s);
            s.routine = null;
        }

        private IEnumerator WobbleRoutine(Transform target, TargetState s, float intensity)
        {
            float dur = wobbleDuration;
            float tilt = wobbleTiltDegrees * intensity;
            float squash = squashAmount * 0.4f * intensity;
            // Oscillation frequency -- a few full sways across the duration.
            float freq = 3.2f;
            float t = 0f;

            while (t < dur)
            {
                if (target == null) yield break;
                t += Time.deltaTime;
                float nt = t / dur;

                // Exponential decay envelope: starts strong, calms smoothly.
                float decay = Mathf.Exp(-wobbleDamping * nt);
                float sway = Mathf.Sin(nt * Mathf.PI * 2f * freq) * decay;

                // Rock side to side around the base rotation...
                target.localRotation = s.baseRotation * Quaternion.AngleAxis(sway * tilt, Vector3.forward);
                // ...with a touch of correlated squash for that jelly feel.
                ApplySquashStretch(target, s, Vector3.up, sway * squash);
                yield return null;
            }

            RestoreBase(target, s);
            s.routine = null;
        }

        private IEnumerator CelebrateRoutine(Transform target, TargetState s, Rarity rarity)
        {
            int tier = (int)rarity; // 0..4
            int hops = celebrateBaseHops + tier; // more excitement with rarity
            float lift = bounceHeight * Mathf.Lerp(1.4f, 2.6f, tier / 4f);
            float squash = squashAmount * Mathf.Lerp(0.9f, 1.3f, tier / 4f);
            float hopDur = Mathf.Lerp(0.42f, 0.32f, tier / 4f); // higher tiers feel snappier

            // Optional sparkle, tinted by rarity. Honest-to-goodness optional --
            // if no system is assigned, we simply skip it.
            EmitCelebrateParticles(target, rarity);

            for (int i = 0; i < hops; i++)
            {
                float t = 0f;
                while (t < hopDur)
                {
                    if (target == null) yield break;
                    t += Time.deltaTime;
                    float nt = Mathf.Clamp01(t / hopDur);

                    // A single hop: rise then fall (a clean sin arc).
                    float arc = Mathf.Sin(nt * Mathf.PI);
                    target.localPosition = s.basePosition + Vector3.up * (arc * lift);

                    // Stretch on the way up, squash on the launch/landing.
                    float stretchT = Mathf.Sin(nt * Mathf.PI) - 0.35f;
                    ApplySquashStretch(target, s, Vector3.up, stretchT * squash);
                    yield return null;
                }
            }

            RestoreBase(target, s);
            s.routine = null;
        }

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

        /// <summary>
        /// Looks up (or lazily creates and captures) the state for a target.
        /// The first time a transform is seen, its base pose is recorded.
        /// </summary>
        private TargetState EnsureState(Transform target)
        {
            if (!_states.TryGetValue(target, out TargetState s))
            {
                s = new TargetState();
                _states[target] = s;
            }
            if (!s.captured)
            {
                s.baseScale = target.localScale;
                s.basePosition = target.localPosition;
                s.baseRotation = target.localRotation;
                s.captured = true;
            }
            return s;
        }

        /// <summary>
        /// Stops whatever is currently playing on the target and launches a new
        /// coroutine, recording it so <see cref="Stop"/> can cancel it later.
        /// </summary>
        private void StartMotion(Transform target, TargetState s, IEnumerator routine)
        {
            if (s.routine != null)
            {
                StopCoroutine(s.routine);
                s.routine = null;
            }
            s.routine = StartCoroutine(routine);
        }

        /// <summary>
        /// Restores the target to its captured base scale, position and rotation.
        /// </summary>
        private void RestoreBase(Transform target, TargetState s)
        {
            if (target == null || !s.captured) return;
            target.localScale = s.baseScale;
            target.localPosition = s.basePosition;
            target.localRotation = s.baseRotation;
        }

        /// <summary>
        /// Applies a volume-preserving-ish squash/stretch around an axis.
        /// Positive <paramref name="amount"/> elongates along <paramref name="axis"/>
        /// (and pinches the other two); negative squashes along the axis (and
        /// bulges the other two). It's an approximation tuned to feel good, not a
        /// physics solver.
        /// </summary>
        private void ApplySquashStretch(Transform target, TargetState s, Vector3 axis, float amount)
        {
            if (target == null) return;

            float along = 1f + amount;
            // Counter-scale the perpendicular axes to fake constant volume.
            float perp = 1f / Mathf.Sqrt(Mathf.Max(0.0001f, along));

            Vector3 a = axis.sqrMagnitude > 0.0001f ? axis.normalized : Vector3.up;
            // Blend per-component between "along" and "perp" by how aligned each
            // local axis is with the squash axis.
            float bx = Mathf.Abs(a.x);
            float by = Mathf.Abs(a.y);
            float bz = Mathf.Abs(a.z);

            Vector3 factor = new Vector3(
                Mathf.Lerp(perp, along, bx),
                Mathf.Lerp(perp, along, by),
                Mathf.Lerp(perp, along, bz));

            target.localScale = Vector3.Scale(s.baseScale, factor);
        }

        /// <summary>
        /// Fires the optional celebrate particle system, tinted to the rarity
        /// tier. Does nothing (gracefully) if no system was assigned.
        /// </summary>
        private void EmitCelebrateParticles(Transform target, Rarity rarity)
        {
            if (celebrateParticles == null) return;

            var main = celebrateParticles.main;
            main.startColor = RarityTint(rarity);

            // Position the burst at the character so multiple targets read right.
            celebrateParticles.transform.position = target.position;
            celebrateParticles.Play();
        }

        /// <summary>
        /// Warm, family-friendly color tint per rarity tier -- earthy for common
        /// finds, brighter and more golden as things get special.
        /// </summary>
        private static Color RarityTint(Rarity rarity)
        {
            switch (rarity)
            {
                case Rarity.Common:    return new Color(0.78f, 0.80f, 0.74f); // soft sage
                case Rarity.Uncommon:  return new Color(0.55f, 0.82f, 0.55f); // fresh green
                case Rarity.Rare:      return new Color(0.45f, 0.68f, 0.95f); // sky blue
                case Rarity.Epic:      return new Color(0.74f, 0.50f, 0.92f); // mountain violet
                case Rarity.Legendary: return new Color(1.00f, 0.82f, 0.36f); // warm gold
                default:               return Color.white;
            }
        }

        // ----- Easing -----------------------------------------------------

        private float EvalSquish(float p)
        {
            p = Mathf.Clamp01(p);
            if (squishEase != null && squishEase.length > 0)
                return squishEase.Evaluate(p);
            return EaseOutCubic(p);
        }

        private float EvalRebound(float p)
        {
            p = Mathf.Clamp01(p);
            if (reboundEase != null && reboundEase.length > 0)
                return reboundEase.Evaluate(p);
            // Elastic-ish settle from 1 -> 0 with a gentle overshoot below zero.
            float decay = Mathf.Exp(-5f * p);
            return Mathf.Cos(p * Mathf.PI * 2.0f) * decay;
        }

        private static float EaseOutCubic(float p)
        {
            p = Mathf.Clamp01(p);
            float inv = 1f - p;
            return 1f - inv * inv * inv;
        }

        private static float EaseInOutCubic(float p)
        {
            p = Mathf.Clamp01(p);
            return p < 0.5f
                ? 4f * p * p * p
                : 1f - Mathf.Pow(-2f * p + 2f, 3f) * 0.5f;
        }
    }
}
