// Assets/_Project/Scripts/Gameplay/LevelLoader.cs
using System.Collections.Generic;
using UnityEngine;
using TD.Core;
using TD.Levels;

namespace TD.Gameplay
{
    /// <summary>
    /// Match-time loader for a baked <see cref="LevelData"/> asset. Owns the
    /// runtime representation of the level: the buildable-plane physics
    /// collider that click-to-tile raycasts target, and tile-query accessors
    /// for the rest of the gameplay code.
    /// </summary>
    /// <remarks>
    /// Plain <c>MonoBehaviour</c>, not <c>NetworkBehaviour</c>. The baked
    /// LevelData is identical on every peer (same asset, same content), so
    /// there's no value in syncing it through Netcode. Each peer loads
    /// locally on scene-load.
    ///
    /// Mutable runtime state (walkability changes as towers are placed) lives
    /// here for now. When server-authoritative tower placement is implemented,
    /// the mutable walkability grid will move to a dedicated
    /// <c>MatchState</c> NetworkBehaviour alongside lives, wave info, etc.
    /// Today's goal is the minimum loader surface area needed for everything
    /// that comes next.
    ///
    /// Singleton access: <see cref="Instance"/> is set in <c>Awake</c> and
    /// cleared in <c>OnDestroy</c>. Consumers null-check before use.
    /// </remarks>
    public class LevelLoader : MonoBehaviour
    {
        // ----- Singleton --------------------------------------------------

        /// <summary>
        /// The currently loaded LevelLoader. Null before any scene with a
        /// LevelLoader has Awake'd, and during scene transitions. Always
        /// null-check before use.
        /// </summary>
        public static LevelLoader Instance { get; private set; }

        // ----- Inspector fields -------------------------------------------

        [Header("Level")]

        [Tooltip("Baked LevelData asset to load. Required.")]
        [SerializeField] private LevelData level;

        [Tooltip("Name of the physics layer used by the buildable-plane collider. " +
                 "Click-to-tile raycasts should target this layer. Layer must exist " +
                 "in Project Settings → Tags and Layers.")]
        [SerializeField] private string buildablePlaneLayerName = "BuildablePlane";

        [Header("Debug Gizmos")]

        [Tooltip("Draw the grid's bounding rectangle (always visible).")]
        [SerializeField] private bool drawGridBounds = true;

        [Tooltip("Draw a translucent overlay showing walkable vs. non-walkable tiles.")]
        [SerializeField] private bool drawWalkability = true;

        [Tooltip("Draw the buildable-plane collider's footprint.")]
        [SerializeField] private bool drawBuildablePlane = true;

        [Tooltip("Draw owner-grid tile borders. Only visible when LevelLoader is selected.")]
        [SerializeField] private bool drawOwnerBorders = true;

        // ----- Runtime state ----------------------------------------------

        /// <summary>The loaded LevelData asset, or null if loading failed.</summary>
        public LevelData LevelData => level;

        /// <summary>True if the loader successfully initialized at match start.</summary>
        public bool IsLoaded { get; private set; }

        // The mutable walkability grid. Initialized from LevelData.WalkabilityGrid
        // and mutated by tower placement at runtime. Stays in lockstep with the
        // baked grid until towers are placed.
        //
        // Consumers query through IsWalkable(Vector2Int) and mutate through
        // SetWalkable(Vector2Int, bool), which hide the flat-array indexing.
        //
        // NOTE: This grid will move to MatchState when that NetworkBehaviour is
        // implemented, so server-authoritative mutation is co-located with other
        // match-wide state. For now it lives here because no consumer needed it
        // to live elsewhere.
        private bool[] runtimeWalkability;

        // Per-tile tower occupancy. True when a tower's footprint covers this tile.
        // Distinct from runtimeWalkability because:
        //   (a) spawner and leak-exit tiles are walkable but can never be occupied
        //       by a tower — tracking them separately avoids ambiguity.
        //   (b) the placement ghost needs to detect tile conflicts independently
        //       of walkability (a tile is walkable until the tower is placed,
        //       but the ghost should turn red as soon as a prior placement
        //       reserves it).
        //
        // Initialized to all-false on Awake. Mutated via SetOccupied; queried
        // via IsOccupied. Will move to MatchState alongside runtimeWalkability.
        private bool[] runtimeOccupied;

        // The buildable-plane GameObject we instantiated as our child.
        // Cached for inspector debugging and future destruction.
        private GameObject buildablePlaneGO;
        private BoxCollider buildablePlaneCollider;

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

        private void Awake()
        {
            if (Instance != null && Instance != this)
            {
                // Two LevelLoaders in one scene is a setup error -- the bake
                // pipeline assumes one map per scene, and the singleton doesn't
                // make sense with multiples. Log loudly and let the second one
                // keep going inert (Instance still points at the first).
                Debug.LogError(
                    $"[LevelLoader] Multiple LevelLoader instances in scene. " +
                    $"Existing: '{Instance.gameObject.name}'. New: '{gameObject.name}'. " +
                    $"This loader will not initialize.");
                return;
            }
            Instance = this;

            if (!ValidateInputs())
            {
                IsLoaded = false;
                return;
            }

            InitializeRuntimeWalkability();
            InitializeRuntimeOccupied();
            SpawnBuildablePlane();

            IsLoaded = true;
            LogLoadSummary();
        }

        private void OnDestroy()
        {
            if (Instance == this)
            {
                Instance = null;
            }
            // The buildable plane GameObject is our child, so Unity will
            // destroy it automatically when this object is destroyed.
        }

        // ----- Loading steps ----------------------------------------------

        private bool ValidateInputs()
        {
            if (level == null)
            {
                Debug.LogError(
                    $"[LevelLoader] '{gameObject.name}': LevelData reference is not assigned. " +
                    $"Assign a baked LevelData asset in the inspector.");
                return false;
            }

            // Grid arrays must be present and consistent. An unbaked LevelData
            // has empty arrays; a corrupted one might have mismatched sizes.
            int expectedLength = level.GridSize.x * level.GridSize.y;
            if (level.GridSize.x <= 0 || level.GridSize.y <= 0 || expectedLength <= 0)
            {
                Debug.LogError(
                    $"[LevelLoader] '{level.name}' has invalid grid size {level.GridSize}. " +
                    $"Re-bake the level.");
                return false;
            }
            if (level.WalkabilityGrid == null || level.WalkabilityGrid.Length != expectedLength ||
                level.PlacementGrid == null || level.PlacementGrid.Length != expectedLength ||
                level.OwnerGrid == null || level.OwnerGrid.Length != expectedLength)
            {
                Debug.LogError(
                    $"[LevelLoader] '{level.name}' has inconsistent grid arrays. " +
                    $"Expected each grid to have {expectedLength} entries " +
                    $"(GridSize {level.GridSize.x}×{level.GridSize.y}). Re-bake the level.");
                return false;
            }

            // Physics layer must exist. NameToLayer returns -1 for unknown
            // names; that's the failure signal.
            int layerIndex = LayerMask.NameToLayer(buildablePlaneLayerName);
            if (layerIndex < 0)
            {
                Debug.LogError(
                    $"[LevelLoader] Physics layer '{buildablePlaneLayerName}' does not exist. " +
                    $"Create it in Project Settings → Tags and Layers, then assign it to the " +
                    $"buildablePlaneLayerName field if you used a different name.");
                return false;
            }

            return true;
        }

        private void InitializeRuntimeWalkability()
        {
            // Copy the baked walkability into a runtime array. We don't reuse
            // level.WalkabilityGrid directly because (a) the runtime grid will
            // mutate as towers are placed, and we don't want to mutate the
            // ScriptableObject asset; (b) ScriptableObject array fields are
            // shared across the editor and runtime, so mutating it would persist
            // tower placements across Play sessions.
            runtimeWalkability = new bool[level.WalkabilityGrid.Length];
            System.Array.Copy(
                level.WalkabilityGrid, runtimeWalkability, level.WalkabilityGrid.Length);
        }

        private void InitializeRuntimeOccupied()
        {
            // All tiles start unoccupied. bool[] default-initializes to false,
            // so Array.Clear is redundant but makes intent explicit.
            runtimeOccupied = new bool[level.WalkabilityGrid.Length];
        }

        private void SpawnBuildablePlane()
        {
            // Compute the world-space center and size of the grid.
            //
            // The grid covers tiles from GridOriginTile (inclusive, SW corner)
            // to GridOriginTile + GridSize - (1,1) (inclusive, NE corner).
            // Each tile is TILE_SIZE wide and edge-aligned: tile N occupies world [N, N+1].
            //
            // World extent on X:
            //   left  = GridOriginTile.x * TILE_SIZE
            //   right = (GridOriginTile.x + GridSize.x) * TILE_SIZE
            //   width = GridSize.x * TILE_SIZE
            //   centerX = (left + right) / 2 = (GridOriginTile.x + GridSize.x / 2) * TILE_SIZE
            //
            // Same shape on Z (grid-y maps to world-z).
            float worldCenterX =
                (level.GridOriginTile.x + level.GridSize.x * 0.5f) * GridCoordinates.TILE_SIZE;
            float worldCenterZ =
                (level.GridOriginTile.y + level.GridSize.y * 0.5f) * GridCoordinates.TILE_SIZE;
            float worldSizeX = level.GridSize.x * GridCoordinates.TILE_SIZE;
            float worldSizeZ = level.GridSize.y * GridCoordinates.TILE_SIZE;

            buildablePlaneGO = new GameObject("__BuildablePlane");
            buildablePlaneGO.transform.SetParent(transform, worldPositionStays: false);
            buildablePlaneGO.transform.localPosition = Vector3.zero;
            buildablePlaneGO.transform.localRotation = Quaternion.identity;
            buildablePlaneGO.transform.localScale = Vector3.one;
            buildablePlaneGO.layer = LayerMask.NameToLayer(buildablePlaneLayerName);

            buildablePlaneCollider = buildablePlaneGO.AddComponent<BoxCollider>();
            // Position the collider center in WORLD space coords by setting
            // BoxCollider.center after the GameObject is at origin. Since the
            // GO transform is identity at origin, local == world.
            buildablePlaneCollider.center = new Vector3(
                worldCenterX, GridCoordinates.BUILDABLE_PLANE_Y, worldCenterZ);
            // Y size: a thin sliver. We give it a non-zero height so raycasts
            // from above and below both register, and so floating-point
            // imprecision in raycast origin doesn't make hits fail at the
            // exact Y=0 plane. 0.1 world units is plenty.
            buildablePlaneCollider.size = new Vector3(worldSizeX, 0.1f, worldSizeZ);
            // Default isTrigger=false; raycasts hit it as a solid collider.
            // If we ever want it to ignore physics simulation while still
            // being raycastable, isTrigger=true also works for Raycast (with
            // QueryTriggerInteraction.Collide).
        }

        private void LogLoadSummary()
        {
            Vector3 c = buildablePlaneCollider.center;
            Vector3 s = buildablePlaneCollider.size;
            Debug.Log(
                $"[LevelLoader] Loaded '{level.MapName}' " +
                $"(playerCount={level.PlayerCount}, " +
                $"grid {level.GridSize.x}×{level.GridSize.y} from origin {level.GridOriginTile}). " +
                $"Buildable plane at world ({c.x:F2}, {c.y:F2}, {c.z:F2}) " +
                $"size ({s.x:F2}, {s.y:F2}, {s.z:F2}) on layer '{buildablePlaneLayerName}'.");
        }

        // ----- Public tile queries ----------------------------------------
        //
        // All queries take WORLD-TILE coordinates (the same coordinates
        // GridCoordinates.WorldToGrid returns). Internally we translate by
        // GridOriginTile to index into the flat arrays. Consumers should
        // never see grid-array indices.

        /// <summary>True if <paramref name="tile"/> is within the loaded grid's bounds.</summary>
        public bool InBounds(Vector2Int tile)
        {
            if (!IsLoaded) return false;
            int x = tile.x - level.GridOriginTile.x;
            int y = tile.y - level.GridOriginTile.y;
            return x >= 0 && x < level.GridSize.x && y >= 0 && y < level.GridSize.y;
        }

        /// <summary>
        /// True if <paramref name="tile"/> is part of the playable map area (inside any
        /// MapAreaVolume at bake time). Returns false for out-of-bounds tiles and for in-bounds
        /// "void" tiles outside the map area. This is the outermost gate — gameplay queries
        /// (IsWalkable, GetPlacement, GetOwner) are only meaningful where IsInMap is true.
        ///
        /// Use this for: builder movement clamp, camera pan clamp, minimap rendering bounds.
        /// </summary>
        public bool IsInMap(Vector2Int tile)
        {
            if (!TryFlatIndex(tile, out int idx)) return false;
            // Defensive: existing maps that haven't been re-baked since MapAreaGrid was added
            // will have a null array. Treat that as "not in map" so callers don't false-positive.
            if (level.MapAreaGrid == null || level.MapAreaGrid.Length == 0) return false;
            return level.MapAreaGrid[idx];
        }

        /// <summary>
        /// True if <paramref name="tile"/> is currently walkable. Returns
        /// false for out-of-bounds tiles. Reflects the runtime walkability
        /// grid (which will mutate as towers are placed in future work).
        /// </summary>
        public bool IsWalkable(Vector2Int tile)
        {
            if (!TryFlatIndex(tile, out int idx)) return false;
            return runtimeWalkability[idx];
        }

        /// <summary>
        /// True if <paramref name="tile"/> is walkable in the BAKED terrain grid, ignoring
        /// any runtime tower placement. Returns false for out-of-bounds tiles.
        /// </summary>
        /// <remarks>
        /// Flying enemies path against this grid so they soar over towers (which only
        /// affect <see cref="IsWalkable"/>'s runtime grid) while still respecting
        /// impassable terrain and map bounds. Because the baked grid never changes at
        /// runtime, flying paths are computed once and never need invalidation.
        /// </remarks>
        public bool IsBaseWalkable(Vector2Int tile)
        {
            if (level == null || level.WalkabilityGrid == null) return false;
            if (!TryFlatIndex(tile, out int idx)) return false;
            return level.WalkabilityGrid[idx];
        }

        /// <summary>
        /// Placement state for <paramref name="tile"/>. Returns
        /// <see cref="PlacementState.Outside"/> for out-of-bounds tiles.
        /// Reflects the baked placement grid; this does NOT change at runtime.
        /// </summary>
        public PlacementState GetPlacement(Vector2Int tile)
        {
            if (!TryFlatIndex(tile, out int idx)) return PlacementState.Outside;
            return level.PlacementGrid[idx];
        }

        /// <summary>
        /// Owning player for <paramref name="tile"/>. Returns
        /// <see cref="PlayerSlot.None"/> for out-of-bounds tiles or tiles not
        /// inside any player zone.
        /// </summary>
        public PlayerSlot GetOwner(Vector2Int tile)
        {
            if (!TryFlatIndex(tile, out int idx)) return PlayerSlot.None;
            return level.OwnerGrid[idx];
        }

        /// <summary>
        /// True if <paramref name="tile"/> is currently occupied by a placed tower footprint.
        /// Returns false for out-of-bounds tiles. Unlike <see cref="IsWalkable"/>, this grid
        /// starts all-false and only becomes true when a tower is successfully placed.
        /// </summary>
        /// <remarks>
        /// The placement ghost uses this (alongside <see cref="GetPlacement"/> and
        /// <see cref="GetOwner"/>) to decide whether to render as valid (white) or invalid (red).
        /// The server uses it as part of the tile-availability check before path validation.
        /// </remarks>
        public bool IsOccupied(Vector2Int tile)
        {
            if (!TryFlatIndex(tile, out int idx)) return false;
            return runtimeOccupied[idx];
        }

        // ----- Runtime mutators -------------------------------------------
        //
        // Called by TowerPlacementManager (server-side) when a tower placement
        // is accepted. Both mutators must be called for every tile in the tower's
        // footprint so the two grids stay in sync.
        //
        // These are not RPCs — LevelLoader is a plain MonoBehaviour, not a
        // NetworkBehaviour. The server calls these directly after authoritative
        // validation; clients learn about the change when the TowerInstance
        // NetworkObject spawns and its Start/OnNetworkSpawn stamps its own
        // footprint locally.

        /// <summary>
        /// Fired on every peer whenever <see cref="SetWalkable"/> changes a tile's
        /// walkability. <see cref="TD.Gameplay.PathfindingService"/> subscribes to
        /// invalidate cached paths so in-flight enemies reroute after a tower is
        /// placed or removed.
        /// </summary>
        public event System.Action OnWalkabilityChanged;

        /// <summary>
        /// Sets the runtime walkability of <paramref name="tile"/>. Called by
        /// <c>TowerPlacementManager</c> on the server when a tower is accepted (pass
        /// <c>false</c>) and when a tower is sold/destroyed (pass <c>true</c>).
        /// No-ops silently for out-of-bounds tiles.
        /// </summary>
        /// <remarks>
        /// Fires <see cref="OnWalkabilityChanged"/> once if the tile actually changed.
        /// For multi-tile stamps (a tower footprint), prefer
        /// <see cref="SetWalkableBatch"/> to fire the event ONCE for the whole batch
        /// instead of per-tile — every event triggers all enemy re-paths.
        /// </remarks>
        public void SetWalkable(Vector2Int tile, bool walkable)
        {
            if (!TryFlatIndex(tile, out int idx)) return;
            if (runtimeWalkability[idx] == walkable) return; // no change — don't fire event
            runtimeWalkability[idx] = walkable;
            OnWalkabilityChanged?.Invoke();
        }

        /// <summary>
        /// Batched variant of <see cref="SetWalkable"/>: updates every tile in
        /// <paramref name="tiles"/> to <paramref name="walkable"/> and fires
        /// <see cref="OnWalkabilityChanged"/> AT MOST ONCE for the entire batch
        /// (only if at least one tile actually changed). Use this for tower footprint
        /// stamps so a 2×2 placement triggers a single enemy re-path instead of four.
        /// Out-of-bounds tiles in the list are skipped silently.
        /// </summary>
        public void SetWalkableBatch(IList<Vector2Int> tiles, bool walkable)
        {
            if (tiles == null) return;
            bool anyChanged = false;
            for (int i = 0; i < tiles.Count; i++)
            {
                if (!TryFlatIndex(tiles[i], out int idx)) continue;
                if (runtimeWalkability[idx] == walkable) continue;
                runtimeWalkability[idx] = walkable;
                anyChanged = true;
            }
            if (anyChanged) OnWalkabilityChanged?.Invoke();
        }

        /// <summary>
        /// Sets the runtime occupancy of <paramref name="tile"/>. Called alongside
        /// <see cref="SetWalkable"/> — always update both grids together so they
        /// stay in sync. Pass <c>true</c> when a tower is placed, <c>false</c> when
        /// it is sold or destroyed.
        /// No-ops silently for out-of-bounds tiles.
        /// </summary>
        public void SetOccupied(Vector2Int tile, bool occupied)
        {
            if (!TryFlatIndex(tile, out int idx)) return;
            runtimeOccupied[idx] = occupied;
        }

        // Translates world-tile coordinates to a flat-array index, returning
        // false if the tile is out of bounds. Used by all query methods.
        private bool TryFlatIndex(Vector2Int tile, out int idx)
        {
            idx = 0;
            if (!IsLoaded) return false;
            int x = tile.x - level.GridOriginTile.x;
            int y = tile.y - level.GridOriginTile.y;
            if (x < 0 || x >= level.GridSize.x || y < 0 || y >= level.GridSize.y) return false;
            idx = y * level.GridSize.x + x;
            return true;
        }

        // ----- Gizmos -----------------------------------------------------
        //
        // Gizmos run in both edit mode and play mode. In edit mode we use the
        // baked walkability/owner grids from the LevelData asset directly,
        // because runtimeWalkability hasn't been initialized yet. In play mode
        // we use runtimeWalkability so the visualization reflects tower stamps.
        // Owner and placement grids are immutable, so we read them from the
        // asset in both modes.

        private void OnDrawGizmos()
        {
            if (level == null) return;
            if (level.GridSize.x <= 0 || level.GridSize.y <= 0) return;

            if (drawGridBounds) DrawGridBoundsGizmo();
            if (drawBuildablePlane) DrawBuildablePlaneGizmo();
            if (drawWalkability) DrawWalkabilityGizmo();
        }

        private void OnDrawGizmosSelected()
        {
            if (level == null) return;
            if (level.GridSize.x <= 0 || level.GridSize.y <= 0) return;

            if (drawOwnerBorders) DrawOwnerBordersGizmo();
        }

        private void DrawGridBoundsGizmo()
        {
            // One outlined wire box covering the entire grid extent. Tile N spans world
            // [N, N+1], so the grid's SW corner is at GridOriginTile and its NE corner
            // is at GridOriginTile + GridSize.
            Vector3 sw = new Vector3(
                level.GridOriginTile.x * GridCoordinates.TILE_SIZE,
                GridCoordinates.BUILDABLE_PLANE_Y,
                level.GridOriginTile.y * GridCoordinates.TILE_SIZE);
            Vector3 ne = new Vector3(
                (level.GridOriginTile.x + level.GridSize.x) * GridCoordinates.TILE_SIZE,
                GridCoordinates.BUILDABLE_PLANE_Y,
                (level.GridOriginTile.y + level.GridSize.y) * GridCoordinates.TILE_SIZE);

            Gizmos.color = new Color(1f, 1f, 1f, 0.9f); // bright white outline
            Vector3 nw = new Vector3(sw.x, sw.y, ne.z);
            Vector3 se = new Vector3(ne.x, sw.y, sw.z);
            Gizmos.DrawLine(sw, nw);
            Gizmos.DrawLine(nw, ne);
            Gizmos.DrawLine(ne, se);
            Gizmos.DrawLine(se, sw);
        }

        private void DrawBuildablePlaneGizmo()
        {
            // In play mode the collider exists; draw it directly. In edit mode
            // we don't have a collider yet, but we can draw the rectangle that
            // the loader WOULD instantiate, so designers can preview it. Uses
            // the same formula as SpawnBuildablePlane (tiles are edge-aligned).
            float worldCenterX =
                (level.GridOriginTile.x + level.GridSize.x * 0.5f) * GridCoordinates.TILE_SIZE;
            float worldCenterZ =
                (level.GridOriginTile.y + level.GridSize.y * 0.5f) * GridCoordinates.TILE_SIZE;
            float worldSizeX = level.GridSize.x * GridCoordinates.TILE_SIZE;
            float worldSizeZ = level.GridSize.y * GridCoordinates.TILE_SIZE;

            Gizmos.color = new Color(0.3f, 0.6f, 1f, 0.10f); // translucent blue fill
            Gizmos.DrawCube(
                new Vector3(worldCenterX, GridCoordinates.BUILDABLE_PLANE_Y, worldCenterZ),
                new Vector3(worldSizeX, 0.02f, worldSizeZ));
        }

        private void DrawWalkabilityGizmo()
        {
            // Per-tile translucent fill: green tint for walkable, red tint
            // for non-walkable. Edit mode reads from the baked asset; play
            // mode reads from the runtime grid.
            bool[] walk = (Application.isPlaying && runtimeWalkability != null)
                ? runtimeWalkability
                : level.WalkabilityGrid;
            if (walk == null || walk.Length != level.GridSize.x * level.GridSize.y) return;

            Color walkable = new Color(0.2f, 0.9f, 0.2f, 0.10f);
            Color blocked = new Color(0.9f, 0.2f, 0.2f, 0.20f);

            float tile = GridCoordinates.TILE_SIZE;
            // Slight Y offset so this sits above the buildable-plane gizmo
            // rather than z-fighting with it.
            float drawY = GridCoordinates.BUILDABLE_PLANE_Y + 0.005f;
            Vector3 size = new Vector3(tile * 0.95f, 0.001f, tile * 0.95f);

            for (int y = 0; y < level.GridSize.y; y++)
            {
                for (int x = 0; x < level.GridSize.x; x++)
                {
                    int idx = y * level.GridSize.x + x;
                    Gizmos.color = walk[idx] ? walkable : blocked;
                    // Use GridToWorld so tile centers stay consistent with the convention
                    // (tile (N, N) center at world (N+0.5, N+0.5)).
                    Vector3 c = GridCoordinates.GridToWorld(
                        new Vector2Int(level.GridOriginTile.x + x, level.GridOriginTile.y + y));
                    c.y = drawY;
                    Gizmos.DrawCube(c, size);
                }
            }
        }

        private void DrawOwnerBordersGizmo()
        {
            // One thin outlined square per tile that has an owner, colored
            // with that owner's player color. Drawn only when LevelLoader is
            // selected to keep the scene view from getting too noisy.
            if (level.OwnerGrid == null ||
                level.OwnerGrid.Length != level.GridSize.x * level.GridSize.y) return;

            float tile = GridCoordinates.TILE_SIZE;
            float drawY = GridCoordinates.BUILDABLE_PLANE_Y + 0.010f;

            for (int y = 0; y < level.GridSize.y; y++)
            {
                for (int x = 0; x < level.GridSize.x; x++)
                {
                    int idx = y * level.GridSize.x + x;
                    PlayerSlot owner = level.OwnerGrid[idx];
                    if (owner == PlayerSlot.None) continue;

                    // Tile (gx, gy) spans world XZ from (gx, gy) to (gx+1, gy+1) (edge-aligned).
                    int gx = level.GridOriginTile.x + x;
                    int gy = level.GridOriginTile.y + y;
                    float wMinX = gx * tile;
                    float wMaxX = (gx + 1) * tile;
                    float wMinZ = gy * tile;
                    float wMaxZ = (gy + 1) * tile;

                    Gizmos.color = PlayerColors.Get(owner);
                    // Draw four edges as a wire square. We could DrawWireCube
                    // but it would also draw vertical edges we don't want.
                    Vector3 sw = new Vector3(wMinX, drawY, wMinZ);
                    Vector3 nw = new Vector3(wMinX, drawY, wMaxZ);
                    Vector3 ne = new Vector3(wMaxX, drawY, wMaxZ);
                    Vector3 se = new Vector3(wMaxX, drawY, wMinZ);
                    Gizmos.DrawLine(sw, nw);
                    Gizmos.DrawLine(nw, ne);
                    Gizmos.DrawLine(ne, se);
                    Gizmos.DrawLine(se, sw);
                }
            }
        }
    }
}