"""
STARWASTE - Sprite Loader
Load and manage character sprites with directional animations
"""

import arcade
import math
from pathlib import Path
from typing import Dict, Optional, List, Tuple
from PIL import Image
import xml.etree.ElementTree as ET
from shared.utils import console


class DirectionalSprite(arcade.AnimatedWalkingSprite):
    """Sprite with 4 directional textures using Arcade's AnimatedWalkingSprite"""
    
    def __init__(self, character_name: str, scale: float = 1.0):
        super().__init__(scale=scale)
        
        self.character_name = character_name
        
        # Load textures
        self._load_textures()
    
    def _load_textures(self):
        """Load all 4 directional textures for standing and walking"""
        character_dir = Path("assets/characters") / self.character_name
        
        if not character_dir.exists():
            console.print(f"⚠️ Character directory not found: {character_dir}")
            return
        
        # Check if rotations are in subdirectory (PixelLab format)
        rotations_dir = character_dir / "rotations"
        if rotations_dir.exists():
            character_dir = rotations_dir
        
        # Direction mapping (PixelLab generates these names)
        direction_files = {
            "south": "south.png",
            "north": "north.png", 
            "east": "east.png",
            "west": "west.png"
        }
        
        # Load textures for each direction
        textures = {}
        for direction, filename in direction_files.items():
            file_path = character_dir / filename
            
            if file_path.exists():
                try:
                    texture = arcade.load_texture(str(file_path))
                    textures[direction] = texture
                    # console.print(f"   ✅ Loaded {direction}: {filename}")
                except Exception as e:
                    console.print(f"   ❌ Failed to load {direction}: {e}")
            else:
                console.print(f"   ⚠️ Missing {direction}: {filename}")
        
        # Try to load walk animations
        walk_animations = self._load_walk_animations()
        
        # Set up AnimatedWalkingSprite textures
        if "south" in textures:
            self.stand_down_textures = [textures["south"]]
            self.walk_down_textures = walk_animations.get("south", [textures["south"]])
        
        if "north" in textures:
            self.stand_up_textures = [textures["north"]]
            self.walk_up_textures = walk_animations.get("north", [textures["north"]])
        
        if "east" in textures:
            self.stand_right_textures = [textures["east"]]
            self.walk_right_textures = walk_animations.get("east", [textures["east"]])
        
        if "west" in textures:
            self.stand_left_textures = [textures["west"]]
            self.walk_left_textures = walk_animations.get("west", [textures["west"]])
        
        # Set initial texture
        if self.stand_down_textures:
            self.texture = self.stand_down_textures[0]
    
    def _load_walk_animations(self) -> dict:
        """Load walk animation frames for each direction"""
        character_dir = Path("assets/characters") / self.character_name
        
        # Check for animations/walk directory
        walk_dir = character_dir / "animations" / "walk"
        
        if not walk_dir.exists():
            # No walk animation available
            return {}
        
        animations = {}
        
        # Load frames for each direction
        for direction in ["south", "north", "east", "west"]:
            direction_dir = walk_dir / direction
            
            if not direction_dir.exists():
                continue
            
            # Load all frame_*.png files in order
            frame_files = sorted(direction_dir.glob("frame_*.png"))
            
            if frame_files:
                textures = []
                for frame_file in frame_files:
                    try:
                        texture = arcade.load_texture(str(frame_file))
                        textures.append(texture)
                    except Exception as e:
                        console.print(f"   ⚠️ Failed to load {direction} frame {frame_file.name}: {e}")
                
                if textures:
                    animations[direction] = textures
                    # console.print(f"   ✅ Loaded {direction} walk animation: {len(textures)} frames")
        
        return animations


class DirectionalSprite8Way(arcade.Sprite):
    """Sprite with 8-way directional movement (uses 4 textures intelligently)"""
    
    def __init__(self, character_name: str, scale: float = 1.0):
        super().__init__(scale=scale)
        
        self.character_name = character_name
        self.current_direction = "south"  # Default facing direction
        
        # Animation state
        self.is_moving = False
        self._move_frame_index = 0
        self._action_frame_index = 0
        self.frame_time = 0.0
        self.frame_duration = 0.1  # 10 FPS animation

        # Optional one-shot / action animation state
        self._active_action: Optional[str] = None
        self._action_loop: bool = False
        
        # Textures storage: direction -> [frames]
        self.stand_textures: Dict[str, list] = {}
        self.walk_textures: Dict[str, list] = {}

        # Action textures: action_key -> (direction -> [frames])
        self.action_textures: Dict[str, Dict[str, list]] = {}
        
        # Load textures
        self._load_textures()
        
        # Set initial texture
        self._update_texture()

    def play_action(self, action_key: str, *, loop: bool = False, restart: bool = True) -> bool:
        """Play an action animation (e.g. 'picking_up'). Returns True if started."""
        action = self.action_textures.get(action_key)
        if not action:
            return False

        # Ensure there are frames in at least one direction
        if not any(action.values()):
            return False

        self._active_action = action_key
        self._action_loop = bool(loop)
        if restart:
            self._action_frame_index = 0
            self.frame_time = 0.0
        self._update_texture()
        return True

    def stop_action(self):
        """Stop current action animation (if any)."""
        self._active_action = None
        self._action_frame_index = 0
        self.frame_time = 0.0
        self._update_texture()

    @property
    def is_action_playing(self) -> bool:
        return self._active_action is not None
    
    def _get_direction_from_velocity(self, vx: float, vy: float) -> str:
        """Convert velocity to 8-way direction"""
        if vx == 0 and vy == 0:
            return self.current_direction  # Keep current when stopped
        
        # Calculate angle in degrees (0 = right, 90 = up)
        angle = math.degrees(math.atan2(vy, vx))
        
        # Normalize to 0-360
        if angle < 0:
            angle += 360
        
        # Map to 8 directions (45° sectors)
        # E, NE, N, NW, W, SW, S, SE
        if 337.5 <= angle or angle < 22.5:
            return "east"  # E (0°)
        elif 22.5 <= angle < 67.5:
            return "north-east"  # NE (45°)
        elif 67.5 <= angle < 112.5:
            return "north"  # N (90°)
        elif 112.5 <= angle < 157.5:
            return "north-west"  # NW (135°)
        elif 157.5 <= angle < 202.5:
            return "west"  # W (180°)
        elif 202.5 <= angle < 247.5:
            return "south-west"  # SW (225°)
        elif 247.5 <= angle < 292.5:
            return "south"  # S (270°)
        else:  # 292.5 <= angle < 337.5
            return "south-east"  # SE (315°)
    
    def update_velocity(self, vx: float, vy: float):
        """Update sprite based on velocity"""
        # Determine if moving
        self.is_moving = (vx != 0 or vy != 0)
        
        # Update direction based on velocity
        if self.is_moving:
            new_direction = self._get_direction_from_velocity(vx, vy)
            if new_direction != self.current_direction:
                self.current_direction = new_direction
                self._move_frame_index = 0  # Reset movement animation
    
    def update_animation(self, delta_time: float = 1/60):
        """Update animation frame"""
        # Action animation has priority (one-shot by default)
        if self._active_action:
            self.frame_time += delta_time
            if self.frame_time >= self.frame_duration:
                self.frame_time = 0.0

                frames = self._get_action_frames(self._active_action)
                if frames:
                    self._action_frame_index += 1
                    if self._action_frame_index >= len(frames):
                        if self._action_loop:
                            self._action_frame_index = 0
                        else:
                            # End one-shot action and fall back to normal anim
                            self._active_action = None
                            self._action_frame_index = 0
                else:
                    # No frames for this action; cancel it
                    self._active_action = None
                    self._action_frame_index = 0

            self._update_texture()
            return

        if not self.is_moving:
            self._move_frame_index = 0
            self._update_texture()
            return

        # Advance movement frame timer
        self.frame_time += delta_time
        if self.frame_time >= self.frame_duration:
            self.frame_time = 0.0

            frames = self.walk_textures.get(self.current_direction, [])
            if frames:
                self._move_frame_index = (self._move_frame_index + 1) % len(frames)

        self._update_texture()
    
    def _update_texture(self):
        """Update sprite texture based on current state"""
        if self._active_action:
            frames = self._get_action_frames(self._active_action)
            if frames:
                self.texture = frames[self._action_frame_index % len(frames)]
                return
            # If action doesn't have frames for current setup, fall back gracefully
            self._active_action = None
            self._action_frame_index = 0

        if self.is_moving:
            frames = self.walk_textures.get(self.current_direction, [])
            frame_index = self._move_frame_index
        else:
            frames = self.stand_textures.get(self.current_direction, [])
            frame_index = 0
        
        # Fallback if direction not found (4-way character with 8-way movement)
        if not frames:
            fallback = self._get_fallback_direction(self.current_direction)
            if self.is_moving:
                frames = self.walk_textures.get(fallback, [])
            else:
                frames = self.stand_textures.get(fallback, [])
        
        if frames:
            self.texture = frames[frame_index % len(frames)]

    def _get_action_frames(self, action_key: str) -> list:
        """Get action frames for current direction, with 8->4 fallback."""
        action = self.action_textures.get(action_key) or {}
        frames = action.get(self.current_direction, [])
        if frames:
            return frames

        fallback = self._get_fallback_direction(self.current_direction)
        return action.get(fallback, [])
    
    def _get_fallback_direction(self, direction: str) -> str:
        """Get fallback direction for 4-way characters"""
        fallback_map = {
            "north-east": "north",
            "north-west": "north",
            "south-east": "south",
            "south-west": "south"
        }
        return fallback_map.get(direction, "south")
    
    def _load_textures(self):
        """Load 8 directional textures (PixelLab format)"""
        character_dir = Path("assets/characters") / self.character_name
        
        if not character_dir.exists():
            console.print(f"⚠️ Character directory not found: {character_dir}")
            return
        
        # Check if rotations are in subdirectory (PixelLab format)
        rotations_dir = character_dir / "rotations"
        if rotations_dir.exists():
            character_dir = rotations_dir
        
        # Direction mapping (PixelLab 8-way format with hyphens)
        direction_files = {
            "south": "south.png",
            "south-east": "south-east.png",
            "east": "east.png",
            "north-east": "north-east.png",
            "north": "north.png",
            "north-west": "north-west.png",
            "west": "west.png",
            "south-west": "south-west.png"
        }
        
        # Load standing textures
        loaded_count = 0
        for direction, filename in direction_files.items():
            file_path = character_dir / filename
            
            if file_path.exists():
                try:
                    texture = arcade.load_texture(str(file_path))
                    self.stand_textures[direction] = [texture]
                    loaded_count += 1
                    # console.print(f"   ✅ Loaded {direction} stand: {filename}")
                except Exception as e:
                    console.print(f"   ❌ Failed to load {direction}: {e}")
        
        # If we only got 4 directions, fall back to 4-way system
        if loaded_count == 4:
            pass # console.print(f"   ℹ️ Found 4-way character, using cardinal directions only")
        elif loaded_count == 8:
            pass # console.print(f"   ✅ Found 8-way character, full directional support enabled")
        
        # Load walk animations
        walk_animations = self._load_walk_animations()
        for direction, frames in walk_animations.items():
            self.walk_textures[direction] = frames

        # Load optional action animations (one-shot)
        # We accept multiple folder names for convenience.
        self._load_action_animation(
            action_key="picking_up",
            folder_candidates=[
                "picking-up",
                "picking_up",
                "picking up",
                "pickingup",
            ],
        )

        self._load_action_animation(
            action_key="throw_object",
            folder_candidates=[
                "throw-object",
                "throw_object",
                "throw object",
                "throwobject",
            ],
        )

    def _load_action_animation(self, *, action_key: str, folder_candidates: list[str]) -> None:
        """Load an action animation under assets/characters/<char>/animations/<folder>/<direction>/frame_*.png"""
        character_dir = Path("assets/characters") / self.character_name
        animations_root = character_dir / "animations"

        if not animations_root.exists():
            return

        found_any = False
        loaded: Dict[str, list] = {}

        for folder in folder_candidates:
            action_dir = animations_root / folder
            if not action_dir.exists() or not action_dir.is_dir():
                continue

            found_any = True

            directions_8way = [
                "south",
                "south-east",
                "east",
                "north-east",
                "north",
                "north-west",
                "west",
                "south-west",
            ]
            directions_4way = ["south", "north", "east", "west"]

            # Try load 8-way action frames first
            for direction in directions_8way:
                direction_dir = action_dir / direction
                if not direction_dir.exists():
                    continue
                frame_files = sorted(direction_dir.glob("frame_*.png"))
                if not frame_files:
                    continue

                textures = []
                for frame_file in frame_files:
                    try:
                        textures.append(arcade.load_texture(str(frame_file)))
                    except Exception as e:
                        console.print(f"   ⚠️ Failed to load {action_key} {direction} frame {frame_file.name}: {e}")
                if textures:
                    loaded[direction] = textures

            # If no 8-way directions were found, try 4-way
            if not loaded:
                for direction in directions_4way:
                    direction_dir = action_dir / direction
                    if not direction_dir.exists():
                        continue
                    frame_files = sorted(direction_dir.glob("frame_*.png"))
                    if not frame_files:
                        continue

                    textures = []
                    for frame_file in frame_files:
                        try:
                            textures.append(arcade.load_texture(str(frame_file)))
                        except Exception as e:
                            console.print(f"   ⚠️ Failed to load {action_key} {direction} frame {frame_file.name}: {e}")
                    if textures:
                        loaded[direction] = textures

            # We found a usable folder; stop scanning candidates.
            if loaded:
                break

        if found_any and loaded:
            self.action_textures[action_key] = loaded
    
    def _load_walk_animations(self) -> dict:
        """Load walk animation frames for each direction (supports both 'walk' and 'walking')"""
        character_dir = Path("assets/characters") / self.character_name
        
        # Check for animations/walk or animations/walking directory
        walk_dir = character_dir / "animations" / "walk"
        if not walk_dir.exists():
            walk_dir = character_dir / "animations" / "walking"
        
        if not walk_dir.exists():
            # No walk animation - copy stand textures as walk
            return {direction: frames[:] for direction, frames in self.stand_textures.items()}
        
        animations = {}
        
        # Try to load 8 directions first, fall back to 4 if not found
        directions_8way = ["south", "south-east", "east", "north-east", 
                          "north", "north-west", "west", "south-west"]
        directions_4way = ["south", "north", "east", "west"]
        
        # Try 8-way first
        for direction in directions_8way:
            direction_dir = walk_dir / direction
            
            if not direction_dir.exists():
                continue
            
            # Load all frame_*.png files in order
            frame_files = sorted(direction_dir.glob("frame_*.png"))
            
            if frame_files:
                textures = []
                for frame_file in frame_files:
                    try:
                        texture = arcade.load_texture(str(frame_file))
                        textures.append(texture)
                    except Exception as e:
                        console.print(f"   ⚠️ Failed to load {direction} frame {frame_file.name}: {e}")
                
                if textures:
                    animations[direction] = textures
                    # console.print(f"   ✅ Loaded {direction} walk animation: {len(textures)} frames")
        
        # If no 8-way animations found, try 4-way
        if not animations:
            for direction in directions_4way:
                direction_dir = walk_dir / direction
                
                if not direction_dir.exists():
                    continue
                
                frame_files = sorted(direction_dir.glob("frame_*.png"))
                
                if frame_files:
                    textures = []
                    for frame_file in frame_files:
                        try:
                            texture = arcade.load_texture(str(frame_file))
                            textures.append(texture)
                        except Exception as e:
                            console.print(f"   ⚠️ Failed to load {direction} frame {frame_file.name}: {e}")
                    
                    if textures:
                        animations[direction] = textures
                        # console.print(f"   ✅ Loaded {direction} walk animation: {len(textures)} frames")
        
        # Use stand textures as fallback for missing directions
        for direction in self.stand_textures:
            if direction not in animations:
                animations[direction] = self.stand_textures[direction][:]
        
        return animations





class SpriteManager:
    """Manage sprite loading and caching"""
    
    def __init__(self):
        self.characters_dir = Path("assets/characters")
        self.loaded_sprites: Dict[str, DirectionalSprite] = {}
        
        console.print("✅ SpriteManager initialized")
        console.print(f"   Characters directory: {self.characters_dir.absolute()}")
    
    def create_character_sprite(
        self,
        character_name: str,
        x: float = 0,
        y: float = 0,
        scale: float = 1.0
    ) -> DirectionalSprite:
        """
        Create a character sprite
        
        Args:
            character_name: Name of character folder
            x: Initial X position
            y: Initial Y position
            scale: Sprite scale
        
        Returns:
            DirectionalSprite instance
        """
        sprite = DirectionalSprite(character_name, scale)
        sprite.center_x = x
        sprite.center_y = y
        
        return sprite
    
    def list_available_characters(self) -> list[str]:
        """List all available character directories"""
        if not self.characters_dir.exists():
            return []
        
        characters = [
            d.name for d in self.characters_dir.iterdir()
            if d.is_dir() and not d.name.startswith('.')
        ]
        
        return characters
    
    def check_character_files(self, character_name: str) -> dict:
        """
        Check which direction files exist for a character
        
        Returns:
            dict with direction: exists mapping
        """
        character_dir = self.characters_dir / character_name
        
        directions = {
            "south": (character_dir / "south.png").exists(),
            "north": (character_dir / "north.png").exists(),
            "east": (character_dir / "east.png").exists(),
            "west": (character_dir / "west.png").exists()
        }
        
        return directions


# Convenience functions
_sprite_manager = None

def get_sprite_manager() -> SpriteManager:
    """Get global sprite manager instance"""
    global _sprite_manager
    if _sprite_manager is None:
        _sprite_manager = SpriteManager()
    return _sprite_manager


def create_player_sprite(character_name: str = "player_orange", x: float = 0, y: float = 0, use_8way: bool = True) -> DirectionalSprite:
    """
    Create a player sprite (convenience function)
    
    Args:
        character_name: Name of character folder
        x: Initial X position
        y: Initial Y position
        use_8way: Use 8-way directional system (default True)
    """
    if use_8way:
        sprite = DirectionalSprite8Way(character_name, scale=1.0)
        sprite.center_x = x
        sprite.center_y = y
        return sprite
    else:
        manager = get_sprite_manager()
        return manager.create_character_sprite(character_name, x, y, scale=1.0)


def create_enemy_sprite(character_name: str = "mutant_rat", x: float = 0, y: float = 0, use_8way: bool = True) -> DirectionalSprite:
    """
    Create an enemy sprite (convenience function)
    
    Args:
        character_name: Name of character folder
        x: Initial X position
        y: Initial Y position
        use_8way: Use 8-way directional system (default True)
    """
    if use_8way:
        sprite = DirectionalSprite8Way(character_name, scale=1.0)
        sprite.center_x = x
        sprite.center_y = y
        return sprite
    else:
        manager = get_sprite_manager()
        return manager.create_character_sprite(character_name, x, y, scale=1.0)


# Tree sprite cache (per biome)
_tree_textures_cache: Dict[str, Dict[int, arcade.Texture]] = {}

def load_tree_sprites(biome_id: str) -> Dict[int, arcade.Texture]:
    """
    Load all tree sprites for specified biome
    
    Args:
        biome_id: Biome identifier (e.g., 'toxic_wasteland', 'frozen_world')
    
    Returns:
        Dictionary mapping tree_id (1-10) to arcade.Texture
        Falls back to empty dict if trees not found for this biome
    """
    global _tree_textures_cache
    
    # Return cached textures if already loaded for this biome
    if biome_id in _tree_textures_cache:
        return _tree_textures_cache[biome_id]
    
    textures = {}
    trees_dir = Path("assets") / "biomes" / biome_id / "objects" / "trees"
    
    if not trees_dir.exists():
        console.print(f"⚠️ Trees directory not found for biome '{biome_id}': {trees_dir}")
        _tree_textures_cache[biome_id] = textures
        return textures
    
    # Load tree_01.png through tree_10.png
    for tree_id in range(1, 11):
        tree_file = trees_dir / f"tree_{tree_id:02d}.png"
        
        if tree_file.exists():
            try:
                texture = arcade.load_texture(str(tree_file))
                textures[tree_id] = texture
                # console.print(f"   ✅ Loaded tree_{tree_id:02d}.png from {biome_id}")
            except Exception as e:
                console.print(f"   ❌ Failed to load tree_{tree_id:02d}.png: {e}")
        else:
            console.print(f"   ⚠️ Missing tree_{tree_id:02d}.png in {biome_id}")
    
    console.print(f"🌲 Loaded {len(textures)} tree sprites for biome '{biome_id}'")
    _tree_textures_cache[biome_id] = textures
    return textures


# Tree collision data cache (per biome)
# Maps tree_id -> list of polygon points [(x, y), ...]
_tree_collision_cache: Dict[str, Dict[int, List[Tuple[float, float]]]] = {}

def load_tree_collision_data(biome_id: str) -> Dict[int, List[Tuple[float, float]]]:
    """
    Load collision polygon data from trees.tsx for specified biome using Arcade's tilemap API
    
    Args:
        biome_id: Biome identifier (e.g., 'toxic_wasteland', 'frozen_world')
    
    Returns:
        Dictionary mapping tree_id (1-10) to list of polygon points (absolute coordinates)
        Each polygon is a list of (x, y) tuples
    """
    global _tree_collision_cache
    
    # Return cached data if already loaded
    if biome_id in _tree_collision_cache:
        return _tree_collision_cache[biome_id]
    
    collision_data = {}
    tsx_file = Path("assets") / "biomes" / biome_id / "objects" / "trees" / "trees.tsx"
    
    if not tsx_file.exists():
        console.print(f"⚠️ trees.tsx not found for biome '{biome_id}': {tsx_file}")
        _tree_collision_cache[biome_id] = collision_data
        return collision_data
    
    try:
        # Use pytiled_parser to load tileset
        import pytiled_parser
        tileset = pytiled_parser.parse_tileset(tsx_file)
        
        # Iterate through all tiles in tileset
        for tile in tileset.tiles.values():
            tile_id = tile.id
            
            # Check if tile has objects (collision shapes)
            if not tile.objects:
                continue
            
            # Get objects from the ObjectLayer
            if hasattr(tile.objects, 'tiled_objects'):
                objects_list = tile.objects.tiled_objects
            else:
                # Try to iterate directly
                objects_list = list(tile.objects) if hasattr(tile.objects, '__iter__') else []
            
            # Get first collision object (skip Point objects like horizont)
            for obj in objects_list:
                obj_type = type(obj).__name__
                
                # Skip Point objects (e.g., horizont markers)
                if obj_type == 'Point':
                    continue
                
                points = []
                
                # Handle Polygon objects
                if obj_type == 'Polygon' and hasattr(obj, 'points'):
                    # Object position (origin of polygon)
                    obj_x = obj.coordinates.x
                    obj_y = obj.coordinates.y
                    
                    # Convert polygon points to absolute coordinates
                    for point in obj.points:
                        abs_x = obj_x + point.x
                        abs_y = obj_y + point.y
                        points.append((abs_x, abs_y))
                
                # Handle Rectangle objects (convert to 4-point polygon)
                elif obj_type == 'Rectangle' and hasattr(obj, 'size'):
                    # Rectangle is defined by top-left corner + width/height
                    obj_x = obj.coordinates.x
                    obj_y = obj.coordinates.y
                    width = obj.size.width
                    height = obj.size.height
                    
                    # Convert to 4 corners (clockwise from top-left)
                    points = [
                        (obj_x, obj_y),                    # Top-left
                        (obj_x + width, obj_y),            # Top-right
                        (obj_x + width, obj_y + height),   # Bottom-right
                        (obj_x, obj_y + height)            # Bottom-left
                    ]
                
                if points:
                    # Direct mapping: tile_id to tree_id
                    tree_id = tile_id
                    collision_data[tree_id] = points
                    # console.print(f"   ✅ Loaded collision {obj_type.lower()} for tree_{tree_id:02d} ({len(points)} points)")
                    break  # Only use first shape
        
        console.print(f"🔷 Loaded collision data for {len(collision_data)} trees in '{biome_id}'")
        
    except Exception as e:
        console.print(f"❌ Failed to parse trees.tsx for '{biome_id}': {e}")
        import traceback
        traceback.print_exc()
    
    _tree_collision_cache[biome_id] = collision_data
    return collision_data


# Rock sprite cache (per biome)
_rock_textures_cache: Dict[str, Dict[int, arcade.Texture]] = {}

def load_rock_sprites(biome_id: str) -> Dict[int, arcade.Texture]:
    """
    Load all rock sprites for specified biome
    
    Args:
        biome_id: Biome identifier (e.g., 'toxic_wasteland', 'frozen_world')
    
    Returns:
        Dictionary mapping rock_id (1-10) to arcade.Texture
        Falls back to empty dict if rocks not found for this biome
    """
    global _rock_textures_cache
    
    # Return cached textures if already loaded for this biome
    if biome_id in _rock_textures_cache:
        return _rock_textures_cache[biome_id]
    
    textures = {}
    rocks_dir = Path("assets") / "biomes" / biome_id / "objects" / "rocks"
    
    if not rocks_dir.exists():
        console.print(f"⚠️ Rocks directory not found for biome '{biome_id}': {rocks_dir}")
        _rock_textures_cache[biome_id] = textures
        return textures
    
    # Load rock_01.png through rock_10.png
    for rock_id in range(1, 11):
        rock_file = rocks_dir / f"rock_{rock_id:02d}.png"
        
        if rock_file.exists():
            try:
                texture = arcade.load_texture(str(rock_file))
                textures[rock_id] = texture
                # console.print(f"   ✅ Loaded rock_{rock_id:02d}.png from {biome_id}")
            except Exception as e:
                console.print(f"   ❌ Failed to load rock_{rock_id:02d}.png: {e}")
        else:
            console.print(f"   ⚠️ Missing rock_{rock_id:02d}.png in {biome_id}")
    
    console.print(f"🪨 Loaded {len(textures)} rock sprites for biome '{biome_id}'")
    _rock_textures_cache[biome_id] = textures
    return textures


# Rock collision data cache (per biome)
_rock_collision_cache: Dict[str, Dict[int, List[Tuple[float, float]]]] = {}

def load_rock_collision_data(biome_id: str) -> Dict[int, List[Tuple[float, float]]]:
    """
    Load collision polygon data from rocks.tsx for specified biome
    
    Args:
        biome_id: Biome identifier (e.g., 'toxic_wasteland', 'frozen_world')
    
    Returns:
        Dictionary mapping rock_id (1-10) to list of polygon points (absolute coordinates)
    """
    global _rock_collision_cache
    
    # Return cached data if already loaded
    if biome_id in _rock_collision_cache:
        return _rock_collision_cache[biome_id]
    
    collision_data = {}
    tsx_file = Path("assets") / "biomes" / biome_id / "objects" / "rocks" / "rocks.tsx"
    
    if not tsx_file.exists():
        console.print(f"⚠️ rocks.tsx not found for biome '{biome_id}': {tsx_file}")
        _rock_collision_cache[biome_id] = collision_data
        return collision_data
    
    try:
        import pytiled_parser
        tileset = pytiled_parser.parse_tileset(tsx_file)
        
        for tile in tileset.tiles.values():
            tile_id = tile.id
            
            if not tile.objects:
                continue
            
            if hasattr(tile.objects, 'tiled_objects'):
                objects_list = tile.objects.tiled_objects
            else:
                objects_list = list(tile.objects) if hasattr(tile.objects, '__iter__') else []
            
            for obj in objects_list:
                obj_type = type(obj).__name__
                
                # Skip Point objects (e.g., horizont markers)
                if obj_type == 'Point':
                    continue
                
                points = []
                
                if obj_type == 'Polygon' and hasattr(obj, 'points'):
                    obj_x = obj.coordinates.x
                    obj_y = obj.coordinates.y
                    
                    for point in obj.points:
                        abs_x = obj_x + point.x
                        abs_y = obj_y + point.y
                        points.append((abs_x, abs_y))
                
                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
                    
                    points = [
                        (obj_x, obj_y),
                        (obj_x + width, obj_y),
                        (obj_x + width, obj_y + height),
                        (obj_x, obj_y + height)
                    ]
                
                if points:
                    rock_id = tile_id
                    collision_data[rock_id] = points
                    # console.print(f"   ✅ Loaded collision {obj_type.lower()} for rock_{rock_id:02d} ({len(points)} points)")
                    break  # Only use first collision shape
        
        console.print(f"🔷 Loaded collision data for {len(collision_data)} rocks in '{biome_id}'")
        
    except Exception as e:
        console.print(f"❌ Failed to parse rocks.tsx for '{biome_id}': {e}")
        import traceback
        traceback.print_exc()
    
    _rock_collision_cache[biome_id] = collision_data
    return collision_data


# Object dimensions cache (per biome, per object type)
_dimensions_cache: Dict[str, Dict[str, Dict[int, Tuple[int, int]]]] = {}

def load_object_dimensions(biome_id: str, object_type: str) -> Dict[int, Tuple[int, int]]:
    """
    Universal function to load sprite dimensions from TSX file
    
    Args:
        biome_id: Biome identifier (e.g., 'toxic_wasteland')
        object_type: Object type (e.g., 'trees', 'rocks')
    
    Returns:
        Dictionary mapping tile_id to (width, height) tuple
    """
    global _dimensions_cache
    
    # Initialize cache structure
    if biome_id not in _dimensions_cache:
        _dimensions_cache[biome_id] = {}
    
    if object_type in _dimensions_cache[biome_id]:
        return _dimensions_cache[biome_id][object_type]
    
    dimensions = {}
    tsx_file = Path("assets") / "biomes" / biome_id / "objects" / object_type / f"{object_type}.tsx"
    
    if not tsx_file.exists():
        _dimensions_cache[biome_id][object_type] = dimensions
        return dimensions
    
    try:
        import pytiled_parser
        tileset = pytiled_parser.parse_tileset(tsx_file)
        
        for tile in tileset.tiles.values():
            if tile.image and hasattr(tile, 'image_width') and hasattr(tile, 'image_height'):
                dimensions[tile.id] = (tile.image_width, tile.image_height)
        
        console.print(f"📐 Loaded dimensions for {len(dimensions)} {object_type} in '{biome_id}'")
        
    except Exception as e:
        console.print(f"❌ Failed to load {object_type} dimensions for '{biome_id}': {e}")
    
    _dimensions_cache[biome_id][object_type] = dimensions
    return dimensions


def load_rock_dimensions(biome_id: str) -> Dict[int, Tuple[int, int]]:
    """Load rock sprite dimensions (wrapper for backward compatibility)"""
    return load_object_dimensions(biome_id, "rocks")


def load_tree_dimensions(biome_id: str) -> Dict[int, Tuple[int, int]]:
    """Load tree sprite dimensions (wrapper for backward compatibility)"""
    return load_object_dimensions(biome_id, "trees")


# Horizon point cache (per biome, per object type)
# Maps object_id -> (x, y) horizon point
_tree_horizon_cache: Dict[str, Dict[int, Tuple[float, float]]] = {}
_rock_horizon_cache: Dict[str, Dict[int, Tuple[float, float]]] = {}


def load_tree_horizon_data(biome_id: str) -> Dict[int, Tuple[float, float]]:
    """
    Load horizon point data from trees.tsx for Y-sorting
    
    Args:
        biome_id: Biome identifier (e.g., 'toxic_wasteland', 'frozen_world')
    
    Returns:
        Dictionary mapping tree_id to (x, y) horizon point
    """
    global _tree_horizon_cache
    
    if biome_id in _tree_horizon_cache:
        return _tree_horizon_cache[biome_id]
    
    horizon_data = {}
    tsx_file = Path("assets") / "biomes" / biome_id / "objects" / "trees" / "trees.tsx"
    
    if not tsx_file.exists():
        _tree_horizon_cache[biome_id] = horizon_data
        return horizon_data
    
    try:
        import pytiled_parser
        tileset = pytiled_parser.parse_tileset(tsx_file)
        
        for tile in tileset.tiles.values():
            tile_id = tile.id
            
            if not tile.objects:
                continue
            
            if hasattr(tile.objects, 'tiled_objects'):
                objects_list = tile.objects.tiled_objects
            else:
                objects_list = list(tile.objects) if hasattr(tile.objects, '__iter__') else []
            
            # Look for object named "horizont" or "horizon"
            for obj in objects_list:
                obj_name = getattr(obj, 'name', '').lower()
                obj_type = type(obj).__name__
                
                if obj_name in ['horizont', 'horizon'] and obj_type == 'Point':
                    horizon_x = obj.coordinates.x
                    horizon_y = obj.coordinates.y
                    horizon_data[tile_id] = (horizon_x, horizon_y)
                    # console.print(f"   ✅ Loaded horizon for tree_{tile_id:02d} at ({horizon_x:.1f}, {horizon_y:.1f})")
                    break
        
        if horizon_data:
            console.print(f"🎯 Loaded {len(horizon_data)} tree horizon points for '{biome_id}'")
        
    except Exception as e:
        console.print(f"❌ Failed to load tree horizons for '{biome_id}': {e}")
    
    _tree_horizon_cache[biome_id] = horizon_data
    return horizon_data


def load_rock_horizon_data(biome_id: str) -> Dict[int, Tuple[float, float]]:
    """
    Load horizon point data from rocks.tsx for Y-sorting
    
    Args:
        biome_id: Biome identifier (e.g., 'toxic_wasteland', 'frozen_world')
    
    Returns:
        Dictionary mapping rock_id to (x, y) horizon point
    """
    global _rock_horizon_cache
    
    if biome_id in _rock_horizon_cache:
        return _rock_horizon_cache[biome_id]
    
    horizon_data = {}
    tsx_file = Path("assets") / "biomes" / biome_id / "objects" / "rocks" / "rocks.tsx"
    
    if not tsx_file.exists():
        _rock_horizon_cache[biome_id] = horizon_data
        return horizon_data
    
    try:
        import pytiled_parser
        tileset = pytiled_parser.parse_tileset(tsx_file)
        
        for tile in tileset.tiles.values():
            tile_id = tile.id
            
            if not tile.objects:
                continue
            
            if hasattr(tile.objects, 'tiled_objects'):
                objects_list = tile.objects.tiled_objects
            else:
                objects_list = list(tile.objects) if hasattr(tile.objects, '__iter__') else []
            
            # Look for object named "horizont" or "horizon"
            for obj in objects_list:
                obj_name = getattr(obj, 'name', '').lower()
                obj_type = type(obj).__name__
                
                if obj_name in ['horizont', 'horizon'] and obj_type == 'Point':
                    horizon_x = obj.coordinates.x
                    horizon_y = obj.coordinates.y
                    horizon_data[tile_id] = (horizon_x, horizon_y)
                    # console.print(f"   ✅ Loaded horizon for rock_{tile_id:02d} at ({horizon_x:.1f}, {horizon_y:.1f})")
                    break
        
        if horizon_data:
            console.print(f"🎯 Loaded {len(horizon_data)} rock horizon points for '{biome_id}'")
        
    except Exception as e:
        console.print(f"❌ Failed to load rock horizons for '{biome_id}': {e}")
    
    _rock_horizon_cache[biome_id] = horizon_data
    return horizon_data


# ===============================================================================
# UNIFIED RESOURCE LOADERS (replacing separate tree/rock functions)
# ===============================================================================

def load_resource_sprites(biome_id: str, resource_type: str) -> Dict[int, arcade.Texture]:
    """
    Unified loader for resource sprites (trees, rocks, etc.)
    
    Args:
        biome_id: Biome identifier
        resource_type: Type of resource ("tree" or "rock")
    
    Returns:
        Dictionary mapping variant_id to texture
    """
    if resource_type == "tree":
        return load_tree_sprites(biome_id)
    elif resource_type == "rock":
        return load_rock_sprites(biome_id)
    else:
        console.print(f"⚠️ Unknown resource type: {resource_type}")
        return {}


def load_resource_collision_data(biome_id: str, resource_type: str) -> Dict[int, List[Tuple[float, float]]]:
    """Unified loader for resource collision data"""
    if resource_type == "tree":
        return load_tree_collision_data(biome_id)
    elif resource_type == "rock":
        return load_rock_collision_data(biome_id)
    else:
        return {}


def load_resource_horizon_data(biome_id: str, resource_type: str) -> Dict[int, Tuple[float, float]]:
    """Unified loader for resource horizon points"""
    if resource_type == "tree":
        return load_tree_horizon_data(biome_id)
    elif resource_type == "rock":
        return load_rock_horizon_data(biome_id)
    else:
        return {}


def load_resource_dimensions(biome_id: str, resource_type: str) -> Dict[int, Tuple[int, int]]:
    """Unified loader for resource dimensions"""
    if resource_type == "tree":
        return load_tree_dimensions(biome_id)
    elif resource_type == "rock":
        return load_rock_dimensions(biome_id)
    else:
        return {}
