// Assets/_Project/Scripts/Gameplay/SelectionVisualizer.cs
using UnityEngine;
using TD.Core;

namespace TD.Gameplay
{
    /// <summary>
    /// Scene-wide selection ring renderer. One per scene. Listens to
    /// <see cref="SelectionState.OnSelectionChanged"/> and drives a single pooled
    /// ring GameObject — sized from the new selection's <see cref="ISelectable.SelectionRadius"/>,
    /// positioned each <see cref="LateUpdate"/> from <see cref="ISelectable.SelectionTransform"/>.
    /// </summary>
    /// <remarks>
    /// <para><b>Why scene-wide instead of per-prefab.</b> A per-prefab selection ring
    /// child requires authoring every new selectable (towers, enemies, future races)
    /// with the right mesh, material, scale, and child script. That doesn't scale.
    /// A single scene-wide visualizer reuses one prefab, derives size and position
    /// from the ISelectable hints, and adding a new selectable type costs ~2 lines
    /// of interface implementation rather than a whole prefab subtree.</para>
    ///
    /// <para><b>Color isolation.</b> Because the ring is instantiated as a child of
    /// THIS visualizer (not of the selectable), <c>TowerInstance.ApplyTint</c>'s
    /// MaterialPropertyBlock writes can never reach it. The ring's authored material
    /// (green) shows through regardless of the selectable's own tinting.</para>
    ///
    /// <para><b>Single instance, recycled.</b> One ring GameObject is instantiated on
    /// the first selection and reused for every subsequent one (SetActive toggles
    /// visibility). Avoids the GC churn of Instantiate/Destroy on every click.
    /// When multi-select lands, pool to N instances here.</para>
    ///
    /// <para><b>Prefab requirements.</b> Assign a prefab whose mesh is authored at
    /// 1-unit DIAMETER (i.e., radius = 0.5). The visualizer multiplies localScale's
    /// X and Z by <c>2 * SelectionRadius</c> so the rendered ring matches the
    /// selectable's intended size. Y scale is preserved so the disc stays flat.</para>
    /// </remarks>
    public class SelectionVisualizer : MonoBehaviour
    {
        [Tooltip("Flat ground-decal disc/ring prefab with the green selection " +
                 "material. Mesh must be authored at 1-unit base diameter — the " +
                 "visualizer scales by 2 * ISelectable.SelectionRadius to match " +
                 "the selectable's intended size.")]
        [SerializeField] private GameObject selectionRingPrefab;

        [Tooltip("Offset along the projected surface's normal to keep the ring " +
                 "from z-fighting with whatever surface it lands on.")]
        [SerializeField] private float yOffset = 0.02f;

        [Tooltip("Physics layers the selection ring projects onto. Typically " +
                 "BuildablePlane + TerrainGeometry + Selection (the last so the " +
                 "ring lands on top of a tower the selectable is standing on). " +
                 "The visualizer automatically filters out hits on the selectable's " +
                 "own hierarchy. Default ~0 = everything.")]
        [SerializeField] private LayerMask projectionLayerMask = ~0;

        // How far above the selectable we start the downward raycast. Must be tall
        // enough that the origin is OUTSIDE any collider the selectable might be
        // sitting on or inside; 50 covers any reasonable tower/terrain stack.
        private const float RaycastOriginUpOffset = 50f;

        // How far down we cast PAST the selectable's position. 100 covers any
        // map's terrain depth; deeper hits we don't care about anyway.
        private const float MaxProjectionDistance = 100f;

        // Reused buffer for downward raycasts so the per-frame projection is GC-free.
        // 8 is plenty: typical case has 1-3 hits (selectable's own collider, a
        // tower below, the buildable plane).
        private static readonly RaycastHit[] s_raycastBuffer = new RaycastHit[8];

        // The single pooled ring instance. Created lazily on the first non-null
        // selection and reused thereafter. Toggled via SetActive on selection
        // change so we don't churn through Instantiate/Destroy.
        private GameObject ringInstance;

        // Cached current selection so LateUpdate can keep position in sync as the
        // selectable moves. Cleared (and ring hidden) when selection is null.
        private ISelectable currentSelection;

        // Standard deferred-subscribe pattern — see HUDController. SelectionState.Awake
        // may not have run by the time our OnEnable fires; Update retries until it has.
        private bool subscribed;

        private void OnEnable() => TrySubscribe();
        private void OnDisable() => Unsubscribe();
        private void OnDestroy() => Unsubscribe();

        private void Update()
        {
            if (!subscribed) TrySubscribe();
        }

        // Position sync runs in LateUpdate so any earlier-frame movement (server
        // tick on the builder, NetworkTransform interpolation, etc.) is already
        // applied before we read it. Otherwise the ring lags one frame.
        private void LateUpdate()
        {
            if (currentSelection == null || ringInstance == null) return;

            // The interface reference doesn't go through Unity's overloaded == null,
            // so a destroyed Unity object still reads as non-null on the C# side
            // and throws MissingReferenceException when we access any inherited
            // member (e.g., transform). Detect destruction explicitly and treat
            // it as a deselect. This is a backstop — Builder/TowerInstance.OnNetworkDespawn
            // also call SelectionState.Clear so we normally see the event first.
            if (IsDestroyedUnityObject(currentSelection))
            {
                currentSelection = null;
                ringInstance.SetActive(false);
                return;
            }

            var t = currentSelection.SelectionTransform;
            if (t == null)
            {
                // SelectionTransform legitimately returned null (the implementer
                // chose to suppress its own visual). Hide and bail.
                ringInstance.SetActive(false);
                return;
            }

            ProjectRingDown(t);
        }

        // True iff the selectable is a Unity object that has been destroyed. Plain
        // `s == null` against the interface ref doesn't invoke Unity's overloaded
        // equality — it does C# reference equality and returns false for destroyed
        // objects. Cast first; then Unity's overload kicks in.
        private static bool IsDestroyedUnityObject(ISelectable s)
        {
            return s is UnityEngine.Object uo && uo == null;
        }

        /// <summary>
        /// Raycasts straight down from above the selectable and places the ring
        /// at the first hit that isn't on the selectable's own hierarchy. The
        /// ring's up vector is aligned to the surface normal so it sits flat on
        /// slanted terrain or on top of towers below the selectable. When no
        /// surface is hit (rare — selectable floating in space), falls back to
        /// the buildable plane Y so the ring stays visible.
        /// </summary>
        private void ProjectRingDown(Transform selectableTransform)
        {
            Vector3 origin = selectableTransform.position
                             + Vector3.up * RaycastOriginUpOffset;
            float maxDist = RaycastOriginUpOffset + MaxProjectionDistance;

            int count = Physics.RaycastNonAlloc(
                origin, Vector3.down, s_raycastBuffer, maxDist,
                projectionLayerMask, QueryTriggerInteraction.Collide);

            if (count > 0)
            {
                // RaycastNonAlloc doesn't promise order; sort by distance ascending
                // so the first non-self hit is the closest one BELOW the origin
                // (and therefore the topmost surface beneath the selectable).
                SortBufferByDistance(count);

                Transform selfRoot = selectableTransform.root;
                for (int i = 0; i < count; i++)
                {
                    var hit = s_raycastBuffer[i];
                    // Skip hits on the selectable itself (its selection collider,
                    // its visual collider if any). Hierarchy comparison via
                    // transform.root catches all children of the same root.
                    if (hit.collider.transform.root == selfRoot) continue;

                    ringInstance.transform.position = hit.point + hit.normal * yOffset;
                    ringInstance.transform.rotation =
                        Quaternion.FromToRotation(Vector3.up, hit.normal);
                    return;
                }
            }

            // Fallback: no usable surface found. Park on the buildable plane
            // directly under the selectable so the ring stays visible.
            Vector3 fallback = selectableTransform.position;
            fallback.y = GridCoordinates.BUILDABLE_PLANE_Y + yOffset;
            ringInstance.transform.position = fallback;
            ringInstance.transform.rotation = Quaternion.identity;
        }

        // Insertion sort — count is bounded by buffer size (8). Typical case has
        // 1–3 hits. No GC, no allocation, no LINQ.
        private static void SortBufferByDistance(int count)
        {
            for (int i = 1; i < count; i++)
            {
                var current = s_raycastBuffer[i];
                int j = i - 1;
                while (j >= 0 && s_raycastBuffer[j].distance > current.distance)
                {
                    s_raycastBuffer[j + 1] = s_raycastBuffer[j];
                    j--;
                }
                s_raycastBuffer[j + 1] = current;
            }
        }

        // ----- Subscription -----------------------------------------------

        private void TrySubscribe()
        {
            if (subscribed) return;
            var sel = SelectionState.Instance;
            if (sel == null) return;

            sel.OnSelectionChanged += HandleSelectionChanged;
            subscribed = true;

            // Pick up whatever was selected before we managed to subscribe.
            HandleSelectionChanged(sel.SelectedObject);
        }

        private void Unsubscribe()
        {
            if (!subscribed) return;
            var sel = SelectionState.Instance;
            if (sel != null) sel.OnSelectionChanged -= HandleSelectionChanged;
            subscribed = false;
        }

        // ----- Selection handling -----------------------------------------

        private void HandleSelectionChanged(ISelectable newSelection)
        {
            // Treat a destroyed Unity object the same as null. Same reasoning as
            // the LateUpdate guard — interface refs don't trigger Unity's == null.
            if (IsDestroyedUnityObject(newSelection)) newSelection = null;

            currentSelection = newSelection;

            if (newSelection == null)
            {
                if (ringInstance != null) ringInstance.SetActive(false);
                return;
            }

            if (!EnsureRingInstance()) return;

            // Scale the ring to 2 * radius (mesh is authored at 1-unit diameter).
            // Y is preserved so the disc's flatness from the prefab is kept.
            float diameter = newSelection.SelectionRadius * 2f;
            Vector3 ls = ringInstance.transform.localScale;
            ringInstance.transform.localScale = new Vector3(diameter, ls.y, diameter);

            // Position will be re-projected in LateUpdate, but project now too so
            // the first frame doesn't flash at the previous selection's position
            // (or at the origin when the instance is first created).
            var t = newSelection.SelectionTransform;
            if (t != null) ProjectRingDown(t);

            ringInstance.SetActive(true);
        }

        // Lazy-initializes the ring instance on first use. Returns true if the
        // instance is usable; false (and logs) if the prefab field is empty.
        private bool EnsureRingInstance()
        {
            if (ringInstance != null) return true;
            if (selectionRingPrefab == null)
            {
                Debug.LogError("[SelectionVisualizer] selectionRingPrefab is not " +
                               "assigned. Selection rings will not render.");
                return false;
            }

            // Parent under this visualizer so the ring travels with us in the
            // hierarchy and is auto-destroyed when the scene unloads. It still
            // moves freely in world space — we drive its position absolutely.
            ringInstance = Instantiate(selectionRingPrefab, transform);
            ringInstance.name = "ActiveSelectionRing";
            ringInstance.SetActive(false);   // hidden until HandleSelectionChanged turns it on
            return true;
        }
    }
}
