using UnityEngine;
using TD.Core;

namespace TD.Levels
{
    /// <summary>
    /// Abstract base class for all level authoring volumes (player zones, spawners, leak exits, goals).
    /// </summary>
    /// <remarks>
    /// Every concrete volume requires a <see cref="BoxCollider"/>. The collider's bounds (in world
    /// space) define the volume's spatial extent, which the bake script rasterizes to tiles via
    /// <see cref="BoxCollider.bounds"/> and <c>bounds.Contains(tileCenter)</c>.
    ///
    /// Volumes are authoring-time artifacts only — they are NOT part of the runtime gameplay scene.
    /// At runtime the bake's output (<see cref="LevelData"/>) is the sole source of truth.
    ///
    /// Visibility model: gizmos draw via <c>OnDrawGizmosSelected</c> by default (selection-only).
    /// The owning <see cref="LevelAuthoring"/> exposes per-type toggles to make a category always-on.
    /// Volumes not parented under a <c>LevelAuthoring</c> (orphans) always behave as selection-only;
    /// they will also produce warnings during bake.
    /// </remarks>
    [RequireComponent(typeof(BoxCollider))]
    [DisallowMultipleComponent]
    public abstract class VolumeBase : MonoBehaviour
    {
        // Cached collider reference. Looked up lazily and refreshed if it goes null
        // (e.g., the user manually removed the component).
        private BoxCollider _cachedCollider;

        // Cached LevelAuthoring lookup. We walk parents to find it once, then keep the reference.
        // If the cached reference becomes null (parent moved, scene changed), the next access re-resolves.
        private LevelAuthoring _cachedAuthoring;

        /// <summary>
        /// Gets the volume's <see cref="BoxCollider"/>, looking it up lazily. Returns null if the
        /// component has been removed (which violates <c>[RequireComponent]</c> but can happen if
        /// a user manually removes it).
        /// </summary>
        protected BoxCollider Collider
        {
            get
            {
                if (_cachedCollider == null)
                {
                    _cachedCollider = GetComponent<BoxCollider>();
                }
                return _cachedCollider;
            }
        }

        /// <summary>
        /// Walks the parent transform hierarchy to find the owning <see cref="LevelAuthoring"/>
        /// component. Returns null if this volume is orphaned (not parented under a LevelAuthoring).
        /// Result is cached; the cache invalidates automatically if the reference goes null.
        /// </summary>
        protected LevelAuthoring FindOwningAuthoring()
        {
            if (_cachedAuthoring != null)
            {
                return _cachedAuthoring;
            }

            // GetComponentInParent walks up the hierarchy. Includes inactive parents to handle the
            // case where _LevelAuthoring is temporarily disabled during scene work.
            _cachedAuthoring = GetComponentInParent<LevelAuthoring>(includeInactive: true);
            return _cachedAuthoring;
        }

        /// <summary>
        /// When implemented by a subclass, returns whether the corresponding "always show" toggle
        /// is enabled on the supplied <see cref="LevelAuthoring"/>. Each volume type maps to a
        /// different toggle field.
        /// </summary>
        /// <param name="authoring">The owning LevelAuthoring (guaranteed non-null).</param>
        protected abstract bool GetAlwaysShowToggle(LevelAuthoring authoring);

        /// <summary>
        /// Decides whether the "always show" path should run for this volume. Returns true if a
        /// LevelAuthoring is found in parents AND its corresponding toggle is enabled. Orphans
        /// always return false (always-show requires an authoring to read the toggle from).
        /// </summary>
        protected bool ShouldDrawAlwaysOn()
        {
            var authoring = FindOwningAuthoring();
            if (authoring == null)
            {
                return false;
            }
            return GetAlwaysShowToggle(authoring);
        }

        // -------------------------------------------------------------------
        // Static rasterization helper. Single source of truth for converting a Bounds to the
        // set of tiles its rasterization covers. Uses a half-open interval (min inclusive,
        // max exclusive) on XZ so that:
        //   - a volume sized to N whole tiles rasterizes to exactly N tiles (no off-by-one),
        //   - two volumes that share a boundary (e.g., one ending at X=20, next starting at
        //     X=20) do not double-claim the boundary tile.
        // Gizmos and bake share this helper so they stay in lock-step.
        // -------------------------------------------------------------------

        /// <summary>
        /// Iterates every tile whose center lies inside <paramref name="bounds"/> and invokes
        /// <paramref name="onTile"/> for each one. The candidate range is computed via
        /// <see cref="GridCoordinates.WorldToGrid"/> with a one-tile padding on each side to guard
        /// against rounding surprises (WorldToGrid uses round-to-nearest, which can over- or
        /// under-shoot by one when bounds align with tile edges); the per-tile half-open
        /// interval test inside the loop guarantees correctness.
        /// </summary>
        public static void RasterizeBoundsToTiles(Bounds bounds, System.Action<Vector2Int> onTile)
        {
            if (onTile == null) return;

            Vector2Int candidateMin = GridCoordinates.WorldToGrid(new Vector2(bounds.min.x, bounds.min.z));
            Vector2Int candidateMax = GridCoordinates.WorldToGrid(new Vector2(bounds.max.x, bounds.max.z));

            int xLo = candidateMin.x - 1;
            int xHi = candidateMax.x + 1;
            int yLo = candidateMin.y - 1;
            int yHi = candidateMax.y + 1;

            for (int x = xLo; x <= xHi; x++)
            {
                for (int y = yLo; y <= yHi; y++)
                {
                    Vector3 tileCenter = GridCoordinates.GridToWorld(new Vector2Int(x, y));
                    // Half-open interval on XZ: min inclusive, max exclusive. Y axis is implicitly
                    // satisfied because we test at bounds.center.y, which is always within Y range.
                    bool xIn = tileCenter.x >= bounds.min.x && tileCenter.x < bounds.max.x;
                    bool zIn = tileCenter.z >= bounds.min.z && tileCenter.z < bounds.max.z;
                    if (xIn && zIn)
                    {
                        onTile(new Vector2Int(x, y));
                    }
                }
            }
        }

        /// <summary>
        /// Computes the tight tile rectangle for a <see cref="Bounds"/> — the rectangle that
        /// exactly contains every tile whose center is inside the bounds. Returns false if no
        /// tile centers fall inside the bounds.
        /// </summary>
        public static bool TryGetTightTileRect(Bounds bounds, out Vector2Int minTile, out Vector2Int maxTile)
        {
            bool any = false;
            int minX = 0, maxX = 0, minY = 0, maxY = 0;

            RasterizeBoundsToTiles(bounds, t =>
            {
                if (!any)
                {
                    minX = maxX = t.x;
                    minY = maxY = t.y;
                    any = true;
                }
                else
                {
                    if (t.x < minX) minX = t.x;
                    if (t.x > maxX) maxX = t.x;
                    if (t.y < minY) minY = t.y;
                    if (t.y > maxY) maxY = t.y;
                }
            });

            if (!any)
            {
                minTile = Vector2Int.zero;
                maxTile = Vector2Int.zero;
                return false;
            }

            minTile = new Vector2Int(minX, minY);
            maxTile = new Vector2Int(maxX, maxY);
            return true;
        }

        /// <summary>
        /// Instance-side convenience: computes the tight tile rectangle for THIS volume's collider.
        /// </summary>
        protected bool TryGetTightTileRect(out Vector2Int minTile, out Vector2Int maxTile)
        {
            minTile = Vector2Int.zero;
            maxTile = Vector2Int.zero;
            var col = Collider;
            if (col == null) return false;
            return TryGetTightTileRect(col.bounds, out minTile, out maxTile);
        }

        /// <summary>
        /// Draws a translucent fill over the volume's tile coverage at the specified world Y.
        /// Uses per-tile flat cube gizmos (Option A from the gizmo design discussion).
        /// </summary>
        /// <param name="fillColor">Fully-opaque color; alpha will be applied via <paramref name="alpha"/>.</param>
        /// <param name="alpha">Alpha 0..1 applied to the fill.</param>
        /// <param name="yLevel">World Y at which to draw the fill. Stagger by volume type to avoid z-fighting.</param>
        protected void DrawTileCoverageFill(Color fillColor, float alpha, float yLevel)
        {
            if (!TryGetTightTileRect(out Vector2Int minTile, out Vector2Int maxTile)) return;

            // For a single BoxCollider, every tile in the tight rectangle is covered (the rect was
            // built from actually-covered tiles, and the covered set is always rectangular for a
            // BoxCollider's bounds). So we can iterate the rect directly without re-checking
            // bounds.Contains per tile.
            Color prev = Gizmos.color;
            Gizmos.color = PlayerColors.WithAlpha(fillColor, alpha);

            Vector3 tileSize = new Vector3(GridCoordinates.TILE_SIZE, 0.01f, GridCoordinates.TILE_SIZE);

            for (int x = minTile.x; x <= maxTile.x; x++)
            {
                for (int y = minTile.y; y <= maxTile.y; y++)
                {
                    Vector3 center = GridCoordinates.GridToWorld(new Vector2Int(x, y));
                    Vector3 drawCenter = new Vector3(center.x, yLevel, center.z);
                    Gizmos.DrawCube(drawCenter, tileSize);
                }
            }

            Gizmos.color = prev;
        }

        /// <summary>
        /// Draws an opaque rectangular perimeter outline around the volume's tile coverage at the
        /// specified world Y. For single-volume rasterization the coverage is always rectangular
        /// (a BoxCollider's tile rasterization can only produce a tile rectangle), so we draw the
        /// four edges of the tight tile rect.
        /// </summary>
        /// <param name="outlineColor">Outline color (alpha applied as supplied).</param>
        /// <param name="yLevel">World Y at which to draw the outline.</param>
        protected void DrawRectangularOutline(Color outlineColor, float yLevel)
        {
            if (!TryGetTightTileRect(out Vector2Int minTile, out Vector2Int maxTile)) return;

            // Convert tile-corner indices to world-space corners. A tile at (x, y) spans world XZ
            // from (x, y) to (x+1, y+1) (TILE_SIZE = 1, edge-aligned).
            float tileSize = GridCoordinates.TILE_SIZE;
            Vector3 swCorner = new Vector3(minTile.x * tileSize, yLevel, minTile.y * tileSize);
            Vector3 seCorner = new Vector3((maxTile.x + 1) * tileSize, yLevel, minTile.y * tileSize);
            Vector3 neCorner = new Vector3((maxTile.x + 1) * tileSize, yLevel, (maxTile.y + 1) * tileSize);
            Vector3 nwCorner = new Vector3(minTile.x * tileSize, yLevel, (maxTile.y + 1) * tileSize);

            Color prev = Gizmos.color;
            Gizmos.color = outlineColor;

            Gizmos.DrawLine(swCorner, seCorner);
            Gizmos.DrawLine(seCorner, neCorner);
            Gizmos.DrawLine(neCorner, nwCorner);
            Gizmos.DrawLine(nwCorner, swCorner);

            Gizmos.color = prev;
        }

        /// <summary>
        /// Draws a line+cone arrow from <paramref name="origin"/> in the given world-space
        /// direction. The arrow shaft is drawn with <see cref="Gizmos.DrawLine"/> and the
        /// arrowhead is built from four lines forming a small cone.
        /// </summary>
        /// <param name="origin">Arrow start point (world space).</param>
        /// <param name="direction">Unit-length direction the arrow points in.</param>
        /// <param name="length">Total arrow length in world units.</param>
        /// <param name="color">Arrow color (alpha applied as supplied).</param>
        protected void DrawArrow(Vector3 origin, Vector3 direction, float length, Color color)
        {
            if (direction.sqrMagnitude < 0.0001f) return;
            direction = direction.normalized;

            Vector3 tip = origin + direction * length;

            // Pick an "up" axis perpendicular to the arrow direction in the XZ plane. Since our
            // arrows are always horizontal (Y is fixed), we want the arrowhead to flare in the
            // horizontal plane. The right-vector relative to the direction does this:
            //   right = cross(direction, world up). If direction is parallel to world up
            // (vertical arrow, which we don't expect), fall back to world forward.
            Vector3 right = Vector3.Cross(direction, Vector3.up);
            if (right.sqrMagnitude < 0.0001f)
            {
                right = Vector3.Cross(direction, Vector3.forward);
            }
            right.Normalize();

            // Arrowhead size proportional to arrow length.
            float headLength = length * 0.25f;
            float headWidth = length * 0.15f;
            Vector3 headBase = tip - direction * headLength;
            Vector3 leftWing = headBase - right * headWidth;
            Vector3 rightWing = headBase + right * headWidth;

            Color prev = Gizmos.color;
            Gizmos.color = color;

            Gizmos.DrawLine(origin, tip);
            Gizmos.DrawLine(tip, leftWing);
            Gizmos.DrawLine(tip, rightWing);
            Gizmos.DrawLine(leftWing, rightWing); // close the head for visibility

            Gizmos.color = prev;
        }

        /// <summary>
        /// Converts a <see cref="Direction"/> enum to a unit world-space vector in the XZ plane.
        /// North = +Z, South = -Z, East = +X, West = -X (matches grid-y mapped to world-z).
        /// </summary>
        protected static Vector3 DirectionToWorld(Direction direction)
        {
            switch (direction)
            {
                case Direction.North: return new Vector3(0f, 0f, 1f);
                case Direction.South: return new Vector3(0f, 0f, -1f);
                case Direction.East: return new Vector3(1f, 0f, 0f);
                case Direction.West: return new Vector3(-1f, 0f, 0f);
                default: return Vector3.zero;
            }
        }

        /// <summary>
        /// Returns a human-readable label for a player slot, used by gizmo labels for consistency
        /// across volume types. Currently formatted as "Player N" (or "Player ?" for None / unknown).
        /// </summary>
        public static string FormatPlayerName(PlayerSlot slot)
        {
            if (slot == PlayerSlot.None) return "Player ?";
            return "Player " + ((byte)slot).ToString();
        }
    }
}