"""
STARWASTE - Server Collision System
Universal collision detection for all game objects
"""

import os
from typing import Dict, List, Tuple, Optional
from pathlib import Path
import pytiled_parser
from shared.utils import console

# 🔥 OPTIMIZED: Use Shapely (GEOS C++ backend) for 50-100× faster collision detection
try:
    from shapely.geometry import Point, Polygon as ShapelyPolygon
    from shapely.prepared import prep
    SHAPELY_AVAILABLE = True
except ImportError:
    SHAPELY_AVAILABLE = False
    console.print("⚠️ Shapely not available, using slower Python ray-casting")


class CollisionManager:
    """
    Manages collision data for all game objects across all biomes.
    Loads collision polygons from Tiled TSX files at startup.
    """
    
    def __init__(self):
        """Initialize collision manager"""
        self.collision_data: Dict[str, Dict[str, Dict[int, List[Tuple[float, float]]]]] = {}
        # Structure: {biome_id: {object_type: {tile_id: [(x,y), ...]}}}
        # Example: {"toxic_wasteland": {"trees": {1: [(0,0), (64,0), ...]}}}
        
        self.dimensions: Dict[str, Dict[str, Dict[int, Tuple[int, int]]]] = {}
        # Structure: {biome_id: {object_type: {tile_id: (width, height)}}}
        # Example: {"toxic_wasteland": {"rocks": {1: (48, 48), 5: (42, 42)}}}

        # 🔥 HOT PATH OPTIMIZATION:
        # Cache Shapely polygon + prepared geometry per tileset tile.
        # This avoids allocating a Polygon for every point-in-polygon test (which happens per-player per-tick).
        self._shapely_polygons: Dict[str, Dict[str, Dict[int, "ShapelyPolygon"]]] = {}
        self._shapely_prepared: Dict[str, Dict[str, Dict[int, "object"]]] = {}
    
    def load_all_biomes(self, assets_dir: str = None) -> None:
        """
        Load collision data for all biomes and object types at server startup.
        
        Args:
            assets_dir: Path to biomes directory (default: assets/biomes)
        """
        if assets_dir is None:
            assets_dir = os.path.join("assets", "biomes")
        biomes_path = Path(assets_dir)
        if not biomes_path.exists():
            console.print(f"⚠️  Collision: Biomes directory not found: {biomes_path}")
            return
        
        total_loaded = 0
        
        for biome_dir in biomes_path.iterdir():
            if not biome_dir.is_dir():
                continue
            
            biome_id = biome_dir.name
            self.collision_data[biome_id] = {}
            
            # Load trees
            trees_loaded = self._load_object_type(biome_id, "trees", biome_dir / "objects/trees/trees.tsx")
            if trees_loaded > 0:
                total_loaded += trees_loaded
            
            # Load rocks
            rocks_loaded = self._load_object_type(biome_id, "rocks", biome_dir / "objects/rocks/rocks.tsx")
            if rocks_loaded > 0:
                total_loaded += rocks_loaded
            # buildings_loaded = self._load_object_type(biome_id, "buildings", biome_dir / "objects/buildings/buildings.tsx")
        
        if total_loaded > 0:
            console.print(f"✅ Collision: Loaded {total_loaded} collision shapes across {len(self.collision_data)} biomes")
    
    def _load_object_type(self, biome_id: str, object_type: str, tsx_file: Path) -> int:
        """
        Load collision data for specific object type from TSX file.
        
        Args:
            biome_id: Biome identifier (e.g., 'toxic_wasteland')
            object_type: Object type (e.g., 'trees', 'rocks')
            tsx_file: Path to TSX tileset file
            
        Returns:
            Number of collision shapes loaded
        """
        if not tsx_file.exists():
            return 0
        
        try:
            tileset = pytiled_parser.parse_tileset(tsx_file)
            collision_shapes = {}
            tile_dimensions = {}
            
            for tile in tileset.tiles.values():
                tile_id = tile.id
                
                # Load tile dimensions from image attributes
                if tile.image and hasattr(tile, 'image_width') and hasattr(tile, 'image_height'):
                    tile_dimensions[tile_id] = (tile.image_width, tile.image_height)
                
                # Skip tiles without collision objects
                if not tile.objects:
                    continue
                
                # Get objects list (handle different pytiled_parser versions)
                if hasattr(tile.objects, 'tiled_objects'):
                    objects_list = tile.objects.tiled_objects
                else:
                    objects_list = list(tile.objects) if hasattr(tile.objects, '__iter__') else []
                
                # Process first collision shape (skip Point objects)
                for obj in objects_list:
                    obj_type = type(obj).__name__
                    if obj_type == 'Point':
                        continue  # Skip horizon markers
                    points = self._extract_polygon_points(obj)
                    if points:
                        collision_shapes[tile_id] = points
                        break  # Only use first collision shape
            
            if collision_shapes:
                self.collision_data[biome_id][object_type] = collision_shapes
                console.print(f"   📦 {biome_id}/{object_type}: {len(collision_shapes)} shapes")

                # Pre-build Shapely geometries in local TSX space (px, py).
                # We later transform world point -> local point and test contains() on the cached prepared geometry.
                if SHAPELY_AVAILABLE:
                    if biome_id not in self._shapely_polygons:
                        self._shapely_polygons[biome_id] = {}
                        self._shapely_prepared[biome_id] = {}
                    if object_type not in self._shapely_polygons[biome_id]:
                        self._shapely_polygons[biome_id][object_type] = {}
                        self._shapely_prepared[biome_id][object_type] = {}

                    for tile_id, points in collision_shapes.items():
                        try:
                            poly = ShapelyPolygon(points)
                            self._shapely_polygons[biome_id][object_type][tile_id] = poly
                            self._shapely_prepared[biome_id][object_type][tile_id] = prep(poly)
                        except Exception:
                            # If shapely fails for any polygon, we'll fall back to Python ray-casting for that tile.
                            pass
            
            if tile_dimensions:
                if biome_id not in self.dimensions:
                    self.dimensions[biome_id] = {}
                self.dimensions[biome_id][object_type] = tile_dimensions
            
            return len(collision_shapes)
            
        except Exception as e:
            console.print(f"   ⚠️  Failed to load {biome_id}/{object_type}: {e}")
        
        return 0
    
    def _extract_polygon_points(self, obj) -> Optional[List[Tuple[float, float]]]:
        """
        Extract polygon points from Tiled object (supports Polygon and Rectangle).
        
        Args:
            obj: Tiled object from pytiled_parser
            
        Returns:
            List of (x, y) points in local coordinates, or None if unsupported type
        """
        obj_type = type(obj).__name__
        
        # Handle Polygon objects
        if obj_type == 'Polygon' and hasattr(obj, 'points'):
            obj_x = obj.coordinates.x
            obj_y = obj.coordinates.y
            
            points = []
            for point in obj.points:
                points.append((obj_x + point.x, obj_y + point.y))
            return points
        
        # Handle Rectangle objects (convert to 4-point polygon)
        elif obj_type == 'Rectangle' and hasattr(obj, 'size'):
            obj_x = obj.coordinates.x
            obj_y = obj.coordinates.y
            width = obj.size.width
            height = obj.size.height
            
            # Clockwise from top-left
            return [
                (obj_x, obj_y),
                (obj_x + width, obj_y),
                (obj_x + width, obj_y + height),
                (obj_x, obj_y + height)
            ]
        
        return None
    
    def get_collision_polygon(self, biome_id: str, object_type: str, tile_id: int) -> Optional[List[Tuple[float, float]]]:
        """
        Get collision polygon for specific object.
        
        Args:
            biome_id: Biome identifier
            object_type: Object type (e.g., 'trees')
            tile_id: Tile ID within tileset
            
        Returns:
            List of (x, y) points, or None if not found
        """
        if biome_id not in self.collision_data:
            return None
        
        if object_type not in self.collision_data[biome_id]:
            return None
        
        return self.collision_data[biome_id][object_type].get(tile_id)
    
    @staticmethod
    def point_in_polygon(x: float, y: float, polygon: List[Tuple[float, float]]) -> bool:
        """
        Point-in-polygon test.
        
        🔥 OPTIMIZED: Uses Shapely (GEOS C++ backend) when available - 50-100× faster!
        Falls back to Python ray-casting if Shapely not installed.
        
        Args:
            x: Point X coordinate
            y: Point Y coordinate
            polygon: List of (x, y) polygon vertices
            
        Returns:
            True if point is inside polygon
        """
        # 🔥 Fast path: Use Shapely (C++ GEOS backend)
        if SHAPELY_AVAILABLE:
            try:
                point = Point(x, y)
                poly = ShapelyPolygon(polygon)
                return poly.contains(point)
            except Exception:
                pass  # Fall back to Python implementation
        
        # Fallback: Python ray-casting algorithm
        n = len(polygon)
        inside = False
        
        p1x, p1y = polygon[0]
        for i in range(1, n + 1):
            p2x, p2y = polygon[i % n]
            
            if y > min(p1y, p2y):
                if y <= max(p1y, p2y):
                    if x <= max(p1x, p2x):
                        if p1y != p2y:
                            xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
                        if p1x == p2x or x <= xinters:
                            inside = not inside
            
            p1x, p1y = p2x, p2y
        
        return inside
    
    def check_collision(self, x: float, y: float, biome_id: str, object_type: str, 
                       object_x: float, object_y: float, tile_id: int,
                       sprite_center: Tuple[float, float] = (64, 96),
                       sprite_height: float = 192) -> bool:
        """
        Check if point collides with object.
        
        Args:
            x: Point X coordinate (world space)
            y: Point Y coordinate (world space)
            biome_id: Object's biome
            object_type: Object type (e.g., 'trees')
            object_x: Object X position (world space)
            object_y: Object Y position (world space)
            tile_id: Object's tile ID
            sprite_center: Sprite center offset (default for 128x192 trees)
            sprite_height: Sprite height (for Y-axis flip, default 192)
            
        Returns:
            True if collision detected
        """
        # Get collision polygon
        polygon = self.get_collision_polygon(biome_id, object_type, tile_id)
        if not polygon:
            return False

        # Transform world point -> local TSX point (px, py).
        # Inverse of the previous world transform:
        #   world_x = object_x + px - center_x
        #   world_y = object_y + (sprite_height - py) - center_y
        center_x, center_y = sprite_center
        local_x = x - object_x + center_x
        local_y = sprite_height - (y - object_y + center_y)

        # 🔥 Fast path: prepared Shapely geometry (cached per tile)
        if SHAPELY_AVAILABLE:
            try:
                prepared = (
                    self._shapely_prepared
                    .get(biome_id, {})
                    .get(object_type, {})
                    .get(tile_id)
                )
                if prepared is not None:
                    return prepared.contains(Point(local_x, local_y))
            except Exception:
                pass

        # Fallback: Python ray-casting in local space
        return self.point_in_polygon(local_x, local_y, polygon)
