from __future__ import annotations

from typing import TYPE_CHECKING, Literal
import rtree
import numpy
from shapely.prepared import prep
from shapely.strtree import STRtree
from shapely.geometry import box

if TYPE_CHECKING:
    from shapely.geometry import Polygon
    from shapely.prepared import PreparedGeometry
    from inire.geometry.primitives import Port
    from inire.geometry.components import ComponentResult


class CollisionEngine:
    """
    Manages spatial queries for collision detection with unified dilation logic.
    """
    __slots__ = (
        'clearance', 'max_net_width', 'safety_zone_radius',
        'static_index', 'static_geometries', 'static_dilated', 'static_prepared', 
        'static_is_rect', 'static_tree', 'static_obj_ids', 'static_safe_cache', 
        'static_grid', 'grid_cell_size', '_static_id_counter',
        'dynamic_index', 'dynamic_geometries', 'dynamic_dilated', 'dynamic_prepared', 
        'dynamic_tree', 'dynamic_obj_ids', 'dynamic_grid', '_dynamic_id_counter',
        'metrics'
    )

    clearance: float
    """ Minimum required distance between any two waveguides or obstacles """

    max_net_width: float
    """ Maximum width of any net in the session (used for pre-dilation) """

    safety_zone_radius: float
    """ Radius around ports where collisions are ignored """

    def __init__(
            self,
            clearance: float,
            max_net_width: float = 2.0,
            safety_zone_radius: float = 0.0021,
            ) -> None:
        """
        Initialize the Collision Engine.

        Args:
            clearance: Minimum required distance (um).
            max_net_width: Maximum net width (um).
            safety_zone_radius: Safety radius around ports (um).
        """
        self.clearance = clearance
        self.max_net_width = max_net_width
        self.safety_zone_radius = safety_zone_radius

        # Static obstacles
        self.static_index = rtree.index.Index()
        self.static_geometries: dict[int, Polygon] = {}  # ID -> Raw Polygon
        self.static_dilated: dict[int, Polygon] = {}     # ID -> Dilated Polygon (by clearance)
        self.static_prepared: dict[int, PreparedGeometry] = {}  # ID -> Prepared Dilated
        self.static_is_rect: dict[int, bool] = {} # Optimization for ray_cast
        self.static_tree: STRtree | None = None
        self.static_obj_ids: list[int] = [] # Mapping from tree index to obj_id
        self.static_safe_cache: set[tuple] = set() # Global cache for safe move-port combinations
        self.static_grid: dict[tuple[int, int], list[int]] = {}
        self.grid_cell_size = 50.0 # 50um grid cells for broad phase
        self._static_id_counter = 0

        # Dynamic paths for multi-net congestion
        self.dynamic_index = rtree.index.Index()
        self.dynamic_geometries: dict[int, tuple[str, Polygon]] = {}
        self.dynamic_dilated: dict[int, Polygon] = {}
        self.dynamic_prepared: dict[int, PreparedGeometry] = {}
        self.dynamic_tree: STRtree | None = None
        self.dynamic_obj_ids: list[int] = []
        self.dynamic_grid: dict[tuple[int, int], list[int]] = {}
        self._dynamic_id_counter = 0
        
        self.metrics = {
            'static_cache_hits': 0,
            'static_grid_skips': 0,
            'static_tree_queries': 0,
            'static_straight_fast': 0,
            'congestion_grid_skips': 0,
            'congestion_tree_queries': 0,
            'safety_zone_checks': 0
        }

    def reset_metrics(self) -> None:
        """ Reset all performance counters. """
        for k in self.metrics:
            self.metrics[k] = 0

    def get_metrics_summary(self) -> str:
        """ Return a human-readable summary of collision performance. """
        m = self.metrics
        total_static = m['static_cache_hits'] + m['static_grid_skips'] + m['static_tree_queries'] + m['static_straight_fast']
        static_eff = ((m['static_cache_hits'] + m['static_grid_skips'] + m['static_straight_fast']) / total_static * 100) if total_static > 0 else 0
        
        total_cong = m['congestion_grid_skips'] + m['congestion_tree_queries']
        cong_eff = (m['congestion_grid_skips'] / total_cong * 100) if total_cong > 0 else 0
        
        return (f"Collision Performance: \n"
                f"  Static: {total_static} checks, {static_eff:.1f}% bypassed STRtree\n"
                f"    (Cache={m['static_cache_hits']}, Grid={m['static_grid_skips']}, StraightFast={m['static_straight_fast']}, Tree={m['static_tree_queries']})\n"
                f"  Congestion: {total_cong} checks, {cong_eff:.1f}% bypassed STRtree\n"
                f"    (Grid={m['congestion_grid_skips']}, Tree={m['congestion_tree_queries']})\n"
                f"  Safety Zone: {m['safety_zone_checks']} full intersections performed")

    def add_static_obstacle(self, polygon: Polygon) -> None:
        """
        Add a static obstacle to the engine.

        Args:
            polygon: Raw obstacle geometry.
        """
        obj_id = self._static_id_counter
        self._static_id_counter += 1

        # Use MITRE join style to preserve rectangularity of boxes
        dilated = polygon.buffer(self.clearance, join_style=2)
        self.static_geometries[obj_id] = polygon
        self.static_dilated[obj_id] = dilated
        self.static_prepared[obj_id] = prep(dilated)
        self.static_index.insert(obj_id, dilated.bounds)
        
        # Invalidate higher-level spatial data
        self.static_tree = None
        self.static_grid = {} # Rebuild on demand
        
        # Check if it's an axis-aligned rectangle (approximately)
        # Dilated rectangle of an axis-aligned rectangle IS an axis-aligned rectangle.
        b = dilated.bounds
        area = (b[2] - b[0]) * (b[3] - b[1])
        if abs(dilated.area - area) < 1e-4:
             self.static_is_rect[obj_id] = True
        else:
             self.static_is_rect[obj_id] = False

    def _ensure_static_tree(self) -> None:
        if self.static_tree is None and self.static_dilated:
            ids = sorted(self.static_dilated.keys())
            geoms = [self.static_dilated[i] for i in ids]
            self.static_tree = STRtree(geoms)
            self.static_obj_ids = ids

    def _ensure_static_grid(self) -> None:
        if not self.static_grid and self.static_dilated:
            cs = self.grid_cell_size
            for obj_id, poly in self.static_dilated.items():
                b = poly.bounds
                min_gx, max_gx = int(b[0] / cs), int(b[2] / cs)
                min_gy, max_gy = int(b[1] / cs), int(b[3] / cs)
                for gx in range(min_gx, max_gx + 1):
                    for gy in range(min_gy, max_gy + 1):
                        cell = (gx, gy)
                        if cell not in self.static_grid:
                            self.static_grid[cell] = []
                        self.static_grid[cell].append(obj_id)

    def add_path(self, net_id: str, geometry: list[Polygon], dilated_geometry: list[Polygon] | None = None) -> None:
        """
        Add a net's routed path to the dynamic index.

        Args:
            net_id: Identifier for the net.
            geometry: List of raw polygons in the path.
            dilated_geometry: Optional list of pre-dilated polygons (by clearance/2).
        """
        dilation = self.clearance / 2.0
        for i, poly in enumerate(geometry):
            obj_id = self._dynamic_id_counter
            self._dynamic_id_counter += 1
            
            dil = dilated_geometry[i] if dilated_geometry else poly.buffer(dilation)
            
            self.dynamic_geometries[obj_id] = (net_id, poly)
            self.dynamic_dilated[obj_id] = dil
            self.dynamic_prepared[obj_id] = prep(dil)
            self.dynamic_index.insert(obj_id, dil.bounds)
        
        self.dynamic_tree = None
        self.dynamic_grid = {}

    def _ensure_dynamic_tree(self) -> None:
        if self.dynamic_tree is None and self.dynamic_dilated:
            ids = sorted(self.dynamic_dilated.keys())
            geoms = [self.dynamic_dilated[i] for i in ids]
            self.dynamic_tree = STRtree(geoms)
            self.dynamic_obj_ids = ids

    def _ensure_dynamic_grid(self) -> None:
        if not self.dynamic_grid and self.dynamic_dilated:
            cs = self.grid_cell_size
            for obj_id, poly in self.dynamic_dilated.items():
                b = poly.bounds
                min_gx, max_gx = int(b[0] / cs), int(b[2] / cs)
                min_gy, max_gy = int(b[1] / cs), int(b[3] / cs)
                for gx in range(min_gx, max_gx + 1):
                    for gy in range(min_gy, max_gy + 1):
                        cell = (gx, gy)
                        if cell not in self.dynamic_grid:
                            self.dynamic_grid[cell] = []
                        self.dynamic_grid[cell].append(obj_id)

    def remove_path(self, net_id: str) -> None:
        """
        Remove a net's path from the dynamic index.

        Args:
            net_id: Identifier for the net to remove.
        """
        to_remove = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id]
        for obj_id in to_remove:
            nid, poly = self.dynamic_geometries.pop(obj_id)
            dilated = self.dynamic_dilated.pop(obj_id)
            self.dynamic_prepared.pop(obj_id)
            self.dynamic_index.delete(obj_id, dilated.bounds)
        
        if to_remove:
            self.dynamic_tree = None
            self.dynamic_grid = {}

    def lock_net(self, net_id: str) -> None:
        """
        Move a net's dynamic path to static obstacles permanently.

        Args:
            net_id: Identifier for the net to lock.
        """
        to_move = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id]
        for obj_id in to_move:
            nid, poly = self.dynamic_geometries.pop(obj_id)
            dilated = self.dynamic_dilated.pop(obj_id)
            self.dynamic_prepared.pop(obj_id)
            self.dynamic_index.delete(obj_id, dilated.bounds)
            # Re-buffer for static clearance if necessary. 
            # Note: dynamic is clearance/2, static is clearance.
            self.add_static_obstacle(poly)

    def is_collision(
            self,
            geometry: Polygon,
            net_width: float = 2.0,
            start_port: Port | None = None,
            end_port: Port | None = None,
            ) -> bool:
        """
        Alias for check_collision(buffer_mode='static') for backward compatibility.
        """
        _ = net_width
        res = self.check_collision(geometry, 'default', buffer_mode='static', start_port=start_port, end_port=end_port)
        return bool(res)

    def count_congestion(self, geometry: Polygon, net_id: str) -> int:
        """
        Alias for check_collision(buffer_mode='congestion') for backward compatibility.
        """
        res = self.check_collision(geometry, net_id, buffer_mode='congestion')
        return int(res)

    def check_move_straight_static(
            self,
            origin: Port,
            length: float,
            ) -> bool:
        """
        Specialized fast static check for Straights.
        """
        self.metrics['static_straight_fast'] += 1
        # FAST PATH: Grid check
        self._ensure_static_grid()
        cs = self.grid_cell_size
        
        rad = numpy.radians(origin.orientation)
        dx = length * numpy.cos(rad)
        dy = length * numpy.sin(rad)
        
        # Move bounds
        xmin, xmax = sorted([origin.x, origin.x + dx])
        ymin, ymax = sorted([origin.y, origin.y + dy])
        
        # Inflate by clearance/2 for waveguide half-width? 
        # No, static obstacles are ALREADY inflated by full clearance.
        # So we just check if the centerline hits an inflated obstacle.
        
        min_gx, max_gx = int(xmin / cs), int(xmax / cs)
        min_gy, max_gy = int(ymin / cs), int(ymax / cs)
        
        static_grid = self.static_grid
        static_dilated = self.static_dilated
        static_is_rect = self.static_is_rect
        static_prepared = self.static_prepared
        
        inv_dx = 1.0/dx if abs(dx) > 1e-12 else 1e30
        inv_dy = 1.0/dy if abs(dy) > 1e-12 else 1e30

        checked_ids = set()
        for gx in range(min_gx, max_gx + 1):
            for gy in range(min_gy, max_gy + 1):
                if (gx, gy) in static_grid:
                    for obj_id in static_grid[(gx, gy)]:
                        if obj_id in checked_ids: continue
                        checked_ids.add(obj_id)
                        
                        b = static_dilated[obj_id].bounds
                        # Slab Method
                        if abs(dx) < 1e-12:
                            if origin.x < b[0] or origin.x > b[2]: continue
                            tx_min, tx_max = -1e30, 1e30
                        else:
                            tx_min = (b[0] - origin.x) * inv_dx
                            tx_max = (b[2] - origin.x) * inv_dx
                            if tx_min > tx_max: tx_min, tx_max = tx_max, tx_min
                            
                        if abs(dy) < 1e-12:
                            if origin.y < b[1] or origin.y > b[3]: continue
                            ty_min, ty_max = -1e30, 1e30
                        else:
                            ty_min = (b[1] - origin.y) * inv_dy
                            ty_max = (b[3] - origin.y) * inv_dy
                            if ty_min > ty_max: ty_min, ty_max = ty_max, ty_min
                            
                        t_min = max(tx_min, ty_min)
                        t_max = min(tx_max, ty_max)
                        
                        if t_max < 0 or t_min > t_max or t_min > 1.0:
                            continue
                            
                        # If rectangle, slab is exact
                        if static_is_rect[obj_id]:
                            return True
                            
                        # Fallback for complex obstacles
                        # (We could still use ray_cast here but we want exact)
                        # For now, if hits AABB, check prepared
                        from shapely.geometry import LineString
                        line = LineString([(origin.x, origin.y), (origin.x+dx, origin.y+dy)])
                        if static_prepared[obj_id].intersects(line):
                            return True
        return False

    def check_move_static(
            self,
            result: ComponentResult,
            start_port: Port | None = None,
            end_port: Port | None = None,
            ) -> bool:
        """
        Check if a move (ComponentResult) hits any static obstacles.
        """
        # FAST PATH 1: Safety cache check
        cache_key = (result.move_type, 
                     round(start_port.x, 3) if start_port else 0, 
                     round(start_port.y, 3) if start_port else 0,
                     round(end_port.x, 3) if end_port else 0, 
                     round(end_port.y, 3) if end_port else 0)
        
        if cache_key in self.static_safe_cache:
            self.metrics['static_cache_hits'] += 1
            return False

        # FAST PATH 2: Spatial grid check (bypasses STRtree for empty areas)
        self._ensure_static_grid()
        cs = self.grid_cell_size
        b = result.total_bounds
        min_gx, max_gx = int(b[0] / cs), int(b[2] / cs)
        min_gy, max_gy = int(b[1] / cs), int(b[3] / cs)
        
        any_candidates = False
        static_grid = self.static_grid
        for gx in range(min_gx, max_gx + 1):
            for gy in range(min_gy, max_gy + 1):
                if (gx, gy) in static_grid:
                    any_candidates = True
                    break
            if any_candidates: break
            
        if not any_candidates:
            self.metrics['static_grid_skips'] += 1
            self.static_safe_cache.add(cache_key)
            return False

        self.metrics['static_tree_queries'] += 1
        self._ensure_static_tree()
        if self.static_tree is None:
            return False

        # Vectorized Broad phase + Narrow phase
        # Pass all polygons in the move at once
        res_indices, tree_indices = self.static_tree.query(result.geometry, predicate='intersects')
        if tree_indices.size == 0:
            self.static_safe_cache.add(cache_key)
            return False

        # If we have hits, we must check safety zones
        static_obj_ids = self.static_obj_ids
        for i in range(tree_indices.size):
            poly_idx = res_indices[i]
            hit_idx = tree_indices[i]
            obj_id = static_obj_ids[hit_idx]
            poly = result.geometry[poly_idx]
            if self._is_in_safety_zone(poly, obj_id, start_port, end_port):
                continue
            return True
        
        self.static_safe_cache.add(cache_key)
        return False

    def check_move_congestion(
            self,
            result: ComponentResult,
            net_id: str,
            ) -> int:
        """
        Count overlaps of a move with other dynamic paths.
        """
        if result.total_dilated_bounds_box is None:
             return 0 

        # FAST PATH: Grid check
        self._ensure_dynamic_grid()
        if not self.dynamic_grid:
            return 0
            
        cs = self.grid_cell_size
        b = result.total_dilated_bounds
        min_gx, max_gx = int(b[0] / cs), int(b[2] / cs)
        min_gy, max_gy = int(b[1] / cs), int(b[3] / cs)
        
        any_candidates = False
        dynamic_grid = self.dynamic_grid
        dynamic_geometries = self.dynamic_geometries
        for gx in range(min_gx, max_gx + 1):
            for gy in range(min_gy, max_gy + 1):
                cell = (gx, gy)
                if cell in dynamic_grid:
                    # Check if any obj_id in this cell belongs to another net
                    for obj_id in dynamic_grid[cell]:
                        other_net_id, _ = dynamic_geometries[obj_id]
                        if other_net_id != net_id:
                            any_candidates = True
                            break
                if any_candidates: break
            if any_candidates: break
            
        if not any_candidates:
            self.metrics['congestion_grid_skips'] += 1
            return 0

        # SLOW PATH: STRtree
        self.metrics['congestion_tree_queries'] += 1
        self._ensure_dynamic_tree()
        if self.dynamic_tree is None:
            return 0

        # Vectorized query: pass the whole list of polygons
        # result.dilated_geometry is list[Polygon]
        # query() returns (2, M) array of [geometry_indices, tree_indices]
        res_indices, tree_indices = self.dynamic_tree.query(result.dilated_geometry, predicate='intersects')
        if tree_indices.size == 0:
            return 0

        count = 0
        dynamic_geometries = self.dynamic_geometries
        dynamic_obj_ids = self.dynamic_obj_ids
        
        # We need to filter by net_id and count UNIQUE overlaps?
        # Actually, if a single move polygon hits multiple other net polygons, it's multiple overlaps.
        # But if multiple move polygons hit the SAME other net polygon, is it multiple overlaps?
        # Usually, yes, because cost is proportional to volume of overlap.
        for hit_idx in tree_indices:
            obj_id = dynamic_obj_ids[hit_idx]
            other_net_id, _ = dynamic_geometries[obj_id]
            if other_net_id != net_id:
                count += 1
        return count

    def _is_in_safety_zone(self, geometry: Polygon, obj_id: int, start_port: Port | None, end_port: Port | None) -> bool:
        """ Helper to check if an intersection is within a port safety zone. """
        sz = self.safety_zone_radius
        static_dilated = self.static_dilated
        
        # Optimization: Skip expensive intersection if neither port is near the obstacle's bounds
        is_near_port = False
        b = static_dilated[obj_id].bounds
        if start_port:
            if (b[0] - sz <= start_port.x <= b[2] + sz and 
                b[1] - sz <= start_port.y <= b[3] + sz):
                is_near_port = True
        if not is_near_port and end_port:
            if (b[0] - sz <= end_port.x <= b[2] + sz and 
                b[1] - sz <= end_port.y <= b[3] + sz):
                is_near_port = True
        
        if not is_near_port:
            return False # Collision is NOT in safety zone

        # Only if near port, do the expensive check
        self.metrics['safety_zone_checks'] += 1
        raw_obstacle = self.static_geometries[obj_id]
        intersection = geometry.intersection(raw_obstacle)
        if intersection.is_empty:
            return True # Not actually hitting the RAW obstacle (only the buffer)
            
        ix_bounds = intersection.bounds
        # Check start port
        if start_port:
            if (abs(ix_bounds[0] - start_port.x) < sz and
                abs(ix_bounds[2] - start_port.x) < sz and
                abs(ix_bounds[1] - start_port.y) < sz and
                abs(ix_bounds[3] - start_port.y) < sz):
                return True # Is safe
        # Check end port
        if end_port:
            if (abs(ix_bounds[0] - end_port.x) < sz and
                abs(ix_bounds[2] - end_port.x) < sz and
                abs(ix_bounds[1] - end_port.y) < sz and
                abs(ix_bounds[3] - end_port.y) < sz):
                return True # Is safe
                
        return False

    def check_congestion(
            self,
            geometry: Polygon,
            net_id: str,
            dilated_geometry: Polygon | None = None,
            ) -> int:
        """
        Alias for check_collision(buffer_mode='congestion') for backward compatibility.
        """
        res = self.check_collision(geometry, net_id, buffer_mode='congestion', dilated_geometry=dilated_geometry)
        return int(res)
    def check_collision(
            self,
            geometry: Polygon,
            net_id: str,
            buffer_mode: Literal['static', 'congestion'] = 'static',
            start_port: Port | None = None,
            end_port: Port | None = None,
            dilated_geometry: Polygon | None = None,
            bounds: tuple[float, float, float, float] | None = None,
            ) -> bool | int:
        """
        Check for collisions using unified dilation logic.
        """
        if buffer_mode == 'static':
            self._ensure_static_tree()
            if self.static_tree is None:
                return False
                
            hits = self.static_tree.query(geometry, predicate='intersects')
            static_obj_ids = self.static_obj_ids
            for hit_idx in hits:
                obj_id = static_obj_ids[hit_idx]
                if self._is_in_safety_zone(geometry, obj_id, start_port, end_port):
                    continue
                return True
            return False

        # buffer_mode == 'congestion'
        self._ensure_dynamic_tree()
        if self.dynamic_tree is None:
            return 0
            
        dilation = self.clearance / 2.0
        test_poly = dilated_geometry if dilated_geometry else geometry.buffer(dilation)
        
        hits = self.dynamic_tree.query(test_poly, predicate='intersects')
        count = 0
        dynamic_geometries = self.dynamic_geometries
        dynamic_obj_ids = self.dynamic_obj_ids
        
        for hit_idx in hits:
            obj_id = dynamic_obj_ids[hit_idx]
            other_net_id, _ = dynamic_geometries[obj_id]
            if other_net_id != net_id:
                count += 1
        return count

    def ray_cast(self, origin: Port, angle_deg: float, max_dist: float = 2000.0) -> float:
        """
        Cast a ray and find the distance to the nearest static obstacle.

        Args:
            origin: Starting port (x, y).
            angle_deg: Ray direction in degrees.
            max_dist: Maximum lookahead distance.

        Returns:
            Distance to first collision, or max_dist if clear.
        """
        import numpy
        from shapely.geometry import LineString

        rad = numpy.radians(angle_deg)
        cos_val = numpy.cos(rad)
        sin_val = numpy.sin(rad)
        dx = max_dist * cos_val
        dy = max_dist * sin_val
        
        # 1. Pre-calculate ray direction inverses for fast slab intersection
        # Use a small epsilon to avoid divide by zero, but handle zero dx/dy properly.
        if abs(dx) < 1e-12:
            inv_dx = 1e30 # Represent infinity
        else:
            inv_dx = 1.0 / dx
            
        if abs(dy) < 1e-12:
            inv_dy = 1e30 # Represent infinity
        else:
            inv_dy = 1.0 / dy
        
        # Ray AABB for initial R-Tree query
        min_x, max_x = sorted([origin.x, origin.x + dx])
        min_y, max_y = sorted([origin.y, origin.y + dy])
        
        # 1. Query R-Tree
        candidates = list(self.static_index.intersection((min_x, min_y, max_x, max_y)))
        if not candidates:
            return max_dist
        
        min_dist = max_dist
        
        # 2. Check Intersections
        # Note: We intersect with DILATED obstacles to account for clearance
        static_dilated = self.static_dilated
        static_prepared = self.static_prepared
        
        # Optimization: Sort candidates by approximate distance to origin
        # (Using a simpler distance measure for speed)
        def approx_dist_sq(obj_id):
            b = static_dilated[obj_id].bounds
            return (b[0] - origin.x)**2 + (b[1] - origin.y)**2
            
        candidates.sort(key=approx_dist_sq)

        ray_line = None # Lazy creation

        for obj_id in candidates:
            b = static_dilated[obj_id].bounds
            
            # Fast Ray-Box intersection (Slab Method)
            # Correctly handle potential for dx=0 or dy=0
            if abs(dx) < 1e-12:
                if origin.x < b[0] or origin.x > b[2]:
                    continue
                tx_min, tx_max = -1e30, 1e30
            else:
                tx_min = (b[0] - origin.x) * inv_dx
                tx_max = (b[2] - origin.x) * inv_dx
                if tx_min > tx_max: tx_min, tx_max = tx_max, tx_min
            
            if abs(dy) < 1e-12:
                if origin.y < b[1] or origin.y > b[3]:
                    continue
                ty_min, ty_max = -1e30, 1e30
            else:
                ty_min = (b[1] - origin.y) * inv_dy
                ty_max = (b[3] - origin.y) * inv_dy
                if ty_min > ty_max: ty_min, ty_max = ty_max, ty_min
            
            t_min = max(tx_min, ty_min)
            t_max = min(tx_max, ty_max)
            
            # Intersection if [t_min, t_max] intersects [0, 1]
            if t_max < 0 or t_min > t_max or t_min >= (min_dist / max_dist) or t_min > 1.0:
                continue
            
            # Optimization: If it's a rectangle, the slab result is exact!
            if self.static_is_rect[obj_id]:
                min_dist = max(0.0, t_min * max_dist)
                continue

            # If we are here, the ray hits the AABB. Now check the actual polygon.
            if ray_line is None:
                ray_line = LineString([(origin.x, origin.y), (origin.x + dx, origin.y + dy)])

            if static_prepared[obj_id].intersects(ray_line):
                # Calculate exact intersection distance
                intersection = ray_line.intersection(static_dilated[obj_id])
                if intersection.is_empty:
                    continue
                
                # Intersection could be MultiLineString or LineString or Point
                def get_dist(geom):
                    if hasattr(geom, 'geoms'): # Multi-part
                        return min(get_dist(g) for g in geom.geoms)
                    # For line string, the intersection is the segment INSIDE the obstacle.
                    coords = geom.coords
                    p1 = coords[0]
                    return numpy.sqrt((p1[0] - origin.x)**2 + (p1[1] - origin.y)**2)

                try:
                    d = get_dist(intersection)
                    if d < min_dist:
                        min_dist = d
                        # Update ray_line for more aggressive pruning? 
                        # Actually just update min_dist and we use it in the t_min check.
                except Exception:
                    pass

        return min_dist
