"""
FreeExile 2D Spatial Grid Engine.
Authoritative 2D placement, collision detection, and auto-defragmentation for Inventory & Stashes.
"""

from typing import Dict, List, Optional, Tuple, Any
from .inventory_types import InventoryItem
from .spatial_grid_types import GridPlacement, ItemDimensions


class SpatialInventoryGrid:
    """Manages 2D multi-cell spatial grid layout for ARPG bags and chests."""

    def __init__(self, width: int = 12, height: int = 5) -> None:
        self.width = width
        self.height = height
        # Map: (x, y) -> item_uuid
        self._grid: Dict[Tuple[int, int], str] = {}
        # Map: item_uuid -> (InventoryItem, GridPlacement)
        self._items: Dict[str, Tuple[InventoryItem, GridPlacement]] = {}

    def can_place(
        self,
        x: int,
        y: int,
        width: int,
        height: int,
        ignore_uuid: Optional[str] = None
    ) -> bool:
        """Validates bounding box and cell occupancy."""
        if x < 0 or y < 0 or width <= 0 or height <= 0:
            return False
        if x + width > self.width or y + height > self.height:
            return False

        for cx in range(x, x + width):
            for cy in range(y, y + height):
                occupied_uuid = self._grid.get((cx, cy))
                if occupied_uuid is not None and occupied_uuid != ignore_uuid:
                    return False
        return True

    def place_item(
        self,
        item: InventoryItem,
        x: int,
        y: int,
        width: int,
        height: int
    ) -> bool:
        """Places a multi-cell item on the grid, updating internal occupancy."""
        if not self.can_place(x, y, width, height, ignore_uuid=item.item_uuid):
            return False

        # If already placed elsewhere on this grid, clean up old cells
        if item.item_uuid in self._items:
            self.remove_item(item.item_uuid)

        # Occupy cells
        for cx in range(x, x + width):
            for cy in range(y, y + height):
                self._grid[(cx, cy)] = item.item_uuid

        placement = GridPlacement(x=x, y=y, width=width, height=height)
        self._items[item.item_uuid] = (item, placement)

        # Store spatial coordinates in item metadata
        item.metadata["grid_x"] = x
        item.metadata["grid_y"] = y
        item.metadata["grid_w"] = width
        item.metadata["grid_h"] = height
        return True

    def remove_item(self, item_uuid: str) -> Optional[InventoryItem]:
        """Removes an item from the grid and releases all its occupied cells."""
        if item_uuid not in self._items:
            return None

        item, placement = self._items.pop(item_uuid)
        for cx in range(placement.x, placement.x + placement.width):
            for cy in range(placement.y, placement.y + placement.height):
                self._grid.pop((cx, cy), None)

        return item

    def find_first_fit(self, width: int, height: int) -> Optional[Tuple[int, int]]:
        """Scans from top-to-bottom, left-to-right to find first available location."""
        for cy in range(self.height - height + 1):
            for cx in range(self.width - width + 1):
                if self.can_place(cx, cy, width, height):
                    return cx, cy
        return None

    def is_cell_occupied(self, x: int, y: int) -> bool:
        return (x, y) in self._grid

    def get_item_at(self, x: int, y: int) -> Optional[InventoryItem]:
        item_uuid = self._grid.get((x, y))
        if item_uuid and item_uuid in self._items:
            return self._items[item_uuid][0]
        return None

    def get_item_placement(self, item_uuid: str) -> Optional[GridPlacement]:
        if item_uuid in self._items:
            return self._items[item_uuid][1]
        return None

    def get_all_items(self) -> List[Tuple[InventoryItem, GridPlacement]]:
        return list(self._items.values())

    def auto_compact(self) -> bool:
        """Defragments the grid using descending area bin-packing heuristic."""
        all_entries = list(self._items.values())
        if not all_entries:
            return True

        # Sort descending: area (w*h) first, then height
        sorted_entries = sorted(
            all_entries,
            key=lambda e: (e[1].width * e[1].height, e[1].height, e[1].width),
            reverse=True
        )

        # Clear current grid
        self._grid.clear()
        self._items.clear()

        # Repack
        for item, old_p in sorted_entries:
            target_pos = self.find_first_fit(old_p.width, old_p.height)
            if target_pos is None:
                # Rollback if packed items cannot fit
                return False
            self.place_item(item, target_pos[0], target_pos[1], old_p.width, old_p.height)

        return True
