#!/usr/bin/env python3
"""
FreeExile Viewport Frustum Culling Utilities.
Calculates 2.5D isometric screen projection and 2D AABB-Frustum intersection tests.
Ensures only on-screen entities are dispatched to the render pipeline, protecting 120 FPS frame budget.
"""

from __future__ import annotations

from dataclasses import dataclass
import time
from typing import Any, Dict, List, Tuple


@dataclass(slots=True, frozen=True)
class AABB:
    """2D Axis-Aligned Bounding Box."""
    min_x: float
    min_y: float
    max_x: float
    max_y: float

    @property
    def width(self) -> float:
        return max(0.0, self.max_x - self.min_x)

    @property
    def height(self) -> float:
        return max(0.0, self.max_y - self.min_y)

    def contains_point(self, x: float, y: float) -> bool:
        return self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y

    def intersects(self, other: AABB) -> bool:
        return not (
            self.max_x < other.min_x
            or self.min_x > other.max_x
            or self.max_y < other.min_y
            or self.min_y > other.max_y
        )

    def expand(self, padding: float) -> AABB:
        return AABB(
            min_x=self.min_x - padding,
            min_y=self.min_y - padding,
            max_x=self.max_x + padding,
            max_y=self.max_y + padding,
        )


@dataclass(slots=True, frozen=True)
class Frustum2D:
    """2D Orthographic / Isometric Screen Frustum with optional margin padding."""
    min_x: float
    min_y: float
    max_x: float
    max_y: float
    padding: float = 0.0

    def to_aabb(self) -> AABB:
        return AABB(
            min_x=self.min_x - self.padding,
            min_y=self.min_y - self.padding,
            max_x=self.max_x + self.padding,
            max_y=self.max_y + self.padding,
        )

    def contains_point(self, x: float, y: float) -> bool:
        effective = self.to_aabb()
        return effective.contains_point(x, y)


@dataclass(slots=True, frozen=True)
class EntityBounds:
    """Spatial bounding definition for an entity in 2.5D world space."""
    entity_id: str
    world_x: float
    world_y: float
    world_z: float = 0.0
    screen_width: float = 64.0
    screen_height: float = 64.0


def project_iso(
    world_x: float,
    world_y: float,
    world_z: float = 0.0,
    tile_w: float = 64.0,
    tile_h: float = 32.0,
    z_scale: float = 24.0,
) -> Tuple[float, float]:
    """Projects 3D world coordinates into 2.5D isometric screen coordinates."""
    iso_x = (world_x - world_y) * (tile_w / 2.0)
    iso_y = (world_x + world_y) * (tile_h / 2.0) - (world_z * z_scale)
    return iso_x, iso_y


def intersects(frustum: Frustum2D, box: AABB) -> bool:
    """Returns True if the 2D frustum intersects the given AABB."""
    f_box = frustum.to_aabb()
    return f_box.intersects(box)


def cull_entities(
    camera_pos: Tuple[float, float],
    viewport_size: Tuple[float, float],
    entities: List[EntityBounds],
    padding: float = 32.0,
    tile_w: float = 64.0,
    tile_h: float = 32.0,
) -> List[EntityBounds]:
    """Filters a list of entities, returning only those intersecting the viewport frustum."""
    cam_x, cam_y = camera_pos
    vw, vh = viewport_size

    cam_iso_x, cam_iso_y = project_iso(cam_x, cam_y, 0.0, tile_w, tile_h)
    half_vw = vw / 2.0
    half_vh = vh / 2.0

    frustum = Frustum2D(
        min_x=cam_iso_x - half_vw,
        min_y=cam_iso_y - half_vh,
        max_x=cam_iso_x + half_vw,
        max_y=cam_iso_y + half_vh,
        padding=padding,
    )

    visible: List[EntityBounds] = []
    for ent in entities:
        ent_iso_x, ent_iso_y = project_iso(ent.world_x, ent.world_y, ent.world_z, tile_w, tile_h)
        hw = ent.screen_width / 2.0
        hh = ent.screen_height / 2.0
        ent_box = AABB(
            min_x=ent_iso_x - hw,
            min_y=ent_iso_y - hh,
            max_x=ent_iso_x + hw,
            max_y=ent_iso_y + hh,
        )
        if intersects(frustum, ent_box):
            visible.append(ent)

    return visible


def benchmark_culling(
    num_entities: int = 1000,
    viewport_size: Tuple[float, float] = (1920.0, 1080.0),
) -> Dict[str, Any]:
    """Measures execution time and culling efficiency over a synthetic distribution."""
    import random
    rng = random.Random(42)

    entities = [
        EntityBounds(
            entity_id=f"mob_{i}",
            world_x=rng.uniform(-50.0, 50.0),
            world_y=rng.uniform(-50.0, 50.0),
            world_z=0.0,
            screen_width=64.0,
            screen_height=64.0,
        )
        for i in range(num_entities)
    ]

    start = time.perf_counter()
    visible = cull_entities(
        camera_pos=(0.0, 0.0),
        viewport_size=viewport_size,
        entities=entities,
        padding=32.0,
    )
    elapsed_ms = (time.perf_counter() - start) * 1000.0

    culled_count = len(entities) - len(visible)
    culled_ratio = culled_count / max(1, len(entities))

    return {
        "total_entities": len(entities),
        "visible_entities": len(visible),
        "culled_entities": culled_count,
        "culled_percentage": round(culled_ratio * 100.0, 2),
        "elapsed_ms": round(elapsed_ms, 3),
        "per_entity_us": round((elapsed_ms * 1000.0) / max(1, len(entities)), 3),
    }
