"""Shared render asset dataclasses and helpers used by multiple modules."""

from __future__ import annotations

import math
from dataclasses import dataclass

import pygame

from .colors import (
    BLACK,
    BLUE,
    DARK_RED,
    LIGHT_GRAY,
    ORANGE,
    STEEL_BEAM_COLOR,
    STEEL_BEAM_LINE_COLOR,
    YELLOW,
    EnvironmentPalette,
    get_environment_palette,
)
from .entities_constants import INTERNAL_WALL_BEVEL_DEPTH
from .render_constants import (
    ANGLE_BINS,
    BUDDY_COLOR,
    HAND_SPREAD_RAD,
    SPIKY_PLANT_BODY_COLOR,
    SPIKY_PLANT_SPIKE_COLOR,
    HUMANOID_OUTLINE_COLOR,
    HUMANOID_OUTLINE_WIDTH,
    PATROL_BOT_ARROW_COLOR,
    PATROL_BOT_BODY_COLOR,
    PATROL_BOT_OUTLINE_COLOR,
    SURVIVOR_COLOR,
    TRAPPED_OUTLINE_COLOR,
    ZOMBIE_BODY_COLOR,
    ZOMBIE_OUTLINE_COLOR,
    RenderAssets,
)


def _brighten_color(
    color: tuple[int, int, int], *, factor: float = 1.125
) -> tuple[int, int, int]:
    return tuple(min(255, int(c * factor + 0.5)) for c in color)


ANGLE_STEP = math.tau / ANGLE_BINS

_PLAYER_UPSCALE_FACTOR = 4
_CAR_UPSCALE_FACTOR = 4

_PLAYER_DIRECTIONAL_CACHE: dict[tuple[int, int], list[pygame.Surface]] = {}
_SURVIVOR_DIRECTIONAL_CACHE: dict[
    tuple[int, bool, bool, int], list[pygame.Surface]
] = {}
_ZOMBIE_DIRECTIONAL_CACHE: dict[tuple[int, bool, int], list[pygame.Surface]] = {}
_ZOMBIE_DOG_DIRECTIONAL_CACHE: dict[tuple[float, float, int], list[pygame.Surface]] = {}
_PATROL_BOT_DIRECTIONAL_CACHE: dict[tuple[int, int], list[pygame.Surface]] = {}
_RUBBLE_SURFACE_CACHE: dict[tuple, pygame.Surface] = {}
_HUMANOID_SURFACE_EXTENT_RATIO = 2.0

RUBBLE_ROTATION_DEG = 5.0
RUBBLE_OFFSET_RATIO = 0.06
RUBBLE_SCALE_RATIO = 0.9
RUBBLE_SHADOW_RATIO = 0.9


def _scale_color(color: tuple[int, int, int], *, ratio: float) -> tuple[int, int, int]:
    return tuple(max(0, min(255, int(c * ratio + 0.5))) for c in color)


def rubble_offset_for_size(size: int) -> int:
    return max(1, int(round(size * RUBBLE_OFFSET_RATIO)))


def angle_bin_from_vector(
    dx: float, dy: float, *, bins: int = ANGLE_BINS
) -> int | None:
    if dx == 0 and dy == 0:
        return None
    angle = math.atan2(dy, dx)
    if angle < 0:
        angle += math.tau
    step = math.tau / bins
    return int(round(angle / step)) % bins


def _hand_defaults(radius: int) -> tuple[int, int]:
    hand_radius = max(1, int(radius * 0.5))
    hand_distance = max(hand_radius + 1, int(radius * 1.0))
    return hand_radius, hand_distance


def _draw_capped_circle(
    surface: pygame.Surface,
    center: tuple[int, int],
    radius: int,
    base_color: tuple[int, int, int],
    cap_color: tuple[int, int, int],
    outline_color: tuple[int, int, int],
    outline_width: int,
    *,
    angle_rad: float = 0.0,
    hand_spread_rad: float = HAND_SPREAD_RAD,
    hand_radius: int | None = None,
    hand_distance: int | None = None,
    draw_hands: bool = True,
) -> None:
    if hand_radius is None or hand_distance is None:
        hand_radius, hand_distance = _hand_defaults(radius)
    if draw_hands:
        for direction in (-1, 1):
            hand_angle = angle_rad + (hand_spread_rad * direction)
            hand_x = int(round(center[0] + math.cos(hand_angle) * hand_distance))
            hand_y = int(round(center[1] + math.sin(hand_angle) * hand_distance))
            pygame.draw.circle(surface, base_color, (hand_x, hand_y), hand_radius)
    pygame.draw.circle(surface, cap_color, center, radius)
    if outline_width > 0:
        pygame.draw.circle(surface, outline_color, center, radius, width=outline_width)


def _build_capped_surface(
    radius: int,
    base_color: tuple[int, int, int],
    cap_color: tuple[int, int, int],
    angle_bin: int,
    *,
    outline_scale: int = 1,
    draw_hands: bool = True,
    outline_color: tuple[int, int, int] = HUMANOID_OUTLINE_COLOR,
) -> pygame.Surface:
    hand_radius, hand_distance = _hand_defaults(radius)
    max_extent = max(radius, int(round(radius * _HUMANOID_SURFACE_EXTENT_RATIO)))
    size = max_extent * 2 + 2
    surface = pygame.Surface((size, size), pygame.SRCALPHA)
    center = (max_extent + 1, max_extent + 1)
    angle_rad = (angle_bin % ANGLE_BINS) * ANGLE_STEP
    _draw_capped_circle(
        surface,
        center,
        radius,
        base_color,
        cap_color,
        outline_color,
        HUMANOID_OUTLINE_WIDTH * outline_scale,
        angle_rad=angle_rad,
        hand_radius=hand_radius,
        hand_distance=hand_distance,
        draw_hands=draw_hands,
    )
    return surface


@dataclass(frozen=True)
class PolygonSpec:
    size: tuple[int, int]
    polygons: list[list[tuple[int, int]]]


FUEL_CAN_SPEC = PolygonSpec(
    size=(13, 17),
    polygons=[
        [
            (1, 1),
            (7, 1),
            (12, 6),
            (12, 16),
            (1, 16),
        ],
        [
            (10, 1),
            (12, 3),
            (9, 4),
            (7, 3),
        ],
    ],
)

FLASHLIGHT_SPEC = PolygonSpec(
    size=(12, 10),
    polygons=[
        [
            (1, 2),
            (8, 2),
            (8, 7),
            (1, 7),
        ],
        [
            (8, 1),
            (11, 1),
            (11, 8),
            (8, 8),
        ],
    ],
)

FUEL_STATION_SPEC = PolygonSpec(
    size=(14, 18),
    polygons=[
        [(1, 1), (8, 1), (8, 17), (1, 17)],
        [(8, 4), (13, 4), (13, 8), (8, 8)],
        [(8, 10), (12, 10), (12, 12), (8, 12)],
    ],
)

SHOES_SPEC = PolygonSpec(
    size=(14, 10),
    polygons=[
        [
            (1, 1),
            (7, 1),
            (8, 4),
            (13, 6),
            (13, 9),
            (1, 9),
        ],
    ],
)


def _scale_polygons(
    spec: PolygonSpec,
    dst_size: tuple[int, int],
) -> list[list[tuple[int, int]]]:
    src_w, src_h = spec.size
    dst_w, dst_h = dst_size
    scale_x = dst_w / max(1, src_w)
    scale_y = dst_h / max(1, src_h)
    scaled = []
    for poly in spec.polygons:
        scaled.append(
            [
                (
                    int(round(x * scale_x)),
                    int(round(y * scale_y)),
                )
                for x, y in poly
            ]
        )
    return scaled


def _draw_polygon_surface(
    width: int,
    height: int,
    spec: PolygonSpec,
    *,
    fill_color: tuple[int, int, int] = YELLOW,
    outline_color: tuple[int, int, int] = BLACK,
) -> pygame.Surface:
    surface = pygame.Surface((width, height), pygame.SRCALPHA)
    draw_polygons = spec.polygons
    if (width, height) != spec.size:
        draw_polygons = _scale_polygons(spec, (width, height))
    for poly in draw_polygons:
        pygame.draw.polygon(surface, fill_color, poly)
        pygame.draw.polygon(surface, outline_color, poly, width=1)
    return surface


def build_beveled_polygon(
    width: int,
    height: int,
    depth: int,
    bevels: tuple[bool, bool, bool, bool],
) -> list[tuple[int, int]]:
    d = max(0, min(depth, width // 2, height // 2))
    if d == 0 or not any(bevels):
        return [(0, 0), (width, 0), (width, height), (0, height)]

    segments = 4
    tl, tr, br, bl = bevels
    points: list[tuple[int, int]] = []

    def _add_point(x: float, y: float) -> None:
        point = (int(round(x)), int(round(y)))
        if not points or points[-1] != point:
            points.append(point)

    def _add_arc(
        center_x: float,
        center_y: float,
        radius: float,
        start_deg: float,
        end_deg: float,
        *,
        skip_first: bool = False,
        skip_last: bool = False,
    ) -> None:
        for i in range(segments + 1):
            if skip_first and i == 0:
                continue
            if skip_last and i == segments:
                continue
            t = i / segments
            angle = math.radians(start_deg + (end_deg - start_deg) * t)
            _add_point(
                center_x + radius * math.cos(angle),
                center_y + radius * math.sin(angle),
            )

    _add_point(d if tl else 0, 0)
    if tr:
        _add_point(width - d, 0)
        _add_arc(width - d, d, d, -90, 0, skip_first=True)
    else:
        _add_point(width, 0)
    if br:
        _add_point(width, height - d)
        _add_arc(width - d, height - d, d, 0, 90, skip_first=True)
    else:
        _add_point(width, height)
    if bl:
        _add_point(d, height)
        _add_arc(d, height - d, d, 90, 180, skip_first=True)
    else:
        _add_point(0, height)
    if tl:
        _add_point(0, d)
        _add_arc(d, d, d, 180, 270, skip_first=True, skip_last=True)
    return points


def resolve_wall_colors(
    *,
    health_ratio: float,
    palette_category: str,
    palette: EnvironmentPalette | None,
) -> tuple[tuple[int, int, int], tuple[int, int, int]]:
    if palette is None:
        palette = get_environment_palette(None)
    if palette_category == "outer_wall":
        base_color = palette.outer_wall
        border_base_color = palette.outer_wall_border
    else:
        base_color = palette.inner_wall
        border_base_color = palette.inner_wall_border

    if health_ratio <= 0:
        fill_color = (40, 40, 40)
        ratio = 0.0
    else:
        ratio = max(0.0, min(1.0, health_ratio))
        mix = 0.6 + 0.4 * ratio
        fill_color = (
            int(base_color[0] * mix),
            int(base_color[1] * mix),
            int(base_color[2] * mix),
        )
    border_mix = 0.6 + 0.4 * ratio
    border_color = (
        int(border_base_color[0] * border_mix),
        int(border_base_color[1] * border_mix),
        int(border_base_color[2] * border_mix),
    )
    return fill_color, border_color


_CAR_COLOR_SCHEMES: dict[str, dict[str, tuple[int, int, int]]] = {
    "default": {
        "healthy": YELLOW,
        "damaged": ORANGE,
        "critical": DARK_RED,
    },
    "disabled": {
        "healthy": (185, 185, 185),
        "damaged": (150, 150, 150),
        "critical": (110, 110, 110),
    },
}


def resolve_car_color(
    *,
    health_ratio: float,
    appearance: str,
    palette: EnvironmentPalette | None = None,
) -> tuple[int, int, int]:
    palette = _CAR_COLOR_SCHEMES.get(appearance, _CAR_COLOR_SCHEMES["default"])
    color = palette["healthy"]
    if health_ratio < 0.6:
        color = palette["damaged"]
    if health_ratio < 0.3:
        color = palette["critical"]
    return color


def resolve_steel_beam_colors(
    *,
    health_ratio: float,
    palette: EnvironmentPalette | None = None,
) -> tuple[tuple[int, int, int], tuple[int, int, int]]:
    _ = health_ratio, palette
    return STEEL_BEAM_COLOR, STEEL_BEAM_LINE_COLOR


def build_player_directional_surfaces(
    radius: int, *, bins: int = ANGLE_BINS
) -> list[pygame.Surface]:
    cache_key = (radius, bins)
    if cache_key in _PLAYER_DIRECTIONAL_CACHE:
        return _PLAYER_DIRECTIONAL_CACHE[cache_key]
    surfaces = _build_humanoid_directional_surfaces(
        radius,
        base_color=BLUE,
        cap_color=_brighten_color(BLUE),
        bins=bins,
        outline_color=HUMANOID_OUTLINE_COLOR,
    )
    _PLAYER_DIRECTIONAL_CACHE[cache_key] = surfaces
    return surfaces


def _build_humanoid_directional_surfaces(
    radius: int,
    *,
    base_color: tuple[int, int, int],
    cap_color: tuple[int, int, int],
    bins: int = ANGLE_BINS,
    draw_hands: bool = True,
    outline_color: tuple[int, int, int],
) -> list[pygame.Surface]:
    base_radius = radius * _PLAYER_UPSCALE_FACTOR
    base_surface = _build_capped_surface(
        base_radius,
        base_color,
        cap_color,
        0,
        outline_scale=_PLAYER_UPSCALE_FACTOR,
        draw_hands=draw_hands,
        outline_color=outline_color,
    )
    target_surface = _build_capped_surface(
        radius,
        base_color,
        cap_color,
        0,
        draw_hands=draw_hands,
        outline_color=outline_color,
    )
    target_size = target_surface.get_size()
    scale = target_size[0] / base_surface.get_width()
    half_step_deg = 360.0 / (bins * 5)
    surfaces: list[pygame.Surface] = []
    for idx in range(bins):
        rotation_deg = -(idx * 360.0 / bins - half_step_deg)
        rotated = pygame.transform.rotozoom(base_surface, rotation_deg, scale)
        framed = pygame.Surface(target_size, pygame.SRCALPHA)
        framed.blit(rotated, rotated.get_rect(center=framed.get_rect().center))
        surfaces.append(framed)
    return surfaces


def draw_humanoid_hand(
    surface: pygame.Surface,
    *,
    radius: int,
    angle_rad: float,
    color: tuple[int, int, int],
    hand_radius: int | None = None,
    hand_distance: int | None = None,
) -> None:
    if hand_radius is None or hand_distance is None:
        hand_radius, hand_distance = _hand_defaults(radius)
    center_x, center_y = surface.get_rect().center
    hand_x = int(round(center_x + math.cos(angle_rad) * hand_distance))
    hand_y = int(round(center_y + math.sin(angle_rad) * hand_distance))
    pygame.draw.circle(surface, color, (hand_x, hand_y), hand_radius)


def draw_humanoid_nose(
    surface: pygame.Surface,
    *,
    radius: int,
    angle_rad: float,
    color: tuple[int, int, int],
) -> None:
    center_x, center_y = surface.get_rect().center
    nose_length = max(2, int(radius * 0.45))
    nose_offset = max(1, int(radius * 0.35))
    start_x = center_x + math.cos(angle_rad) * nose_offset
    start_y = center_y + math.sin(angle_rad) * nose_offset
    end_x = center_x + math.cos(angle_rad) * (nose_offset + nose_length)
    end_y = center_y + math.sin(angle_rad) * (nose_offset + nose_length)
    pygame.draw.line(
        surface,
        color,
        (int(start_x), int(start_y)),
        (int(end_x), int(end_y)),
        width=2,
    )


def draw_lineformer_direction_arm(
    surface: pygame.Surface,
    *,
    radius: int,
    angle_rad: float,
    color: tuple[int, int, int],
) -> None:
    center_x, center_y = surface.get_rect().center
    forward_x = math.cos(angle_rad)
    forward_y = math.sin(angle_rad)
    right_x = -forward_y
    right_y = forward_x
    # Place the curved arm on the zombie's right side (relative to facing),
    # keep the inner edge near the body, and bias it slightly forward.
    arc_center_offset = radius * 0.45
    arc_radius_right = max(2.0, radius * 0.8)
    arc_radius_forward = max(2.0, radius * 0.5)
    arc_forward_offset = radius * 0.8
    sweep_rad = math.radians(65)
    points: list[tuple[int, int]] = []
    for step in range(9):
        t = -sweep_rad + (2.0 * sweep_rad * step / 9.0)
        local_right = arc_radius_right * math.cos(t)
        local_forward = arc_radius_forward * math.sin(t)
        px = (
            center_x
            + right_x * (arc_center_offset + local_right)
            + forward_x * (arc_forward_offset + local_forward)
        )
        py = (
            center_y
            + right_y * (arc_center_offset + local_right)
            + forward_y * (arc_forward_offset + local_forward)
        )
        points.append((int(round(px)), int(round(py))))
    pygame.draw.lines(
        surface,
        color,
        False,
        points,
        width=2,
    )


def draw_lightning_marker(
    surface: pygame.Surface,
    *,
    center: tuple[int, int],
    size: int,
    color: tuple[int, int, int],
    width: int = 2,
) -> None:
    """Draw a small lightning bolt marker centered at the given point."""
    half = max(2, int(size * 0.5))
    quarter = max(1, int(size * 0.25))
    x, y = center
    points = [
        (x - quarter, y - half),
        (x + quarter, y - quarter),
        (x - quarter, y),
        (x + quarter, y + half),
    ]
    pygame.draw.lines(surface, color, False, points, width=width)


def build_survivor_directional_surfaces(
    radius: int,
    *,
    is_buddy: bool,
    bins: int = ANGLE_BINS,
    draw_hands: bool = True,
) -> list[pygame.Surface]:
    cache_key = (radius, is_buddy, draw_hands, bins)
    if cache_key in _SURVIVOR_DIRECTIONAL_CACHE:
        return _SURVIVOR_DIRECTIONAL_CACHE[cache_key]
    fill_color = BUDDY_COLOR if is_buddy else SURVIVOR_COLOR
    surfaces = _build_humanoid_directional_surfaces(
        radius,
        base_color=fill_color,
        cap_color=_brighten_color(fill_color),
        bins=bins,
        draw_hands=draw_hands,
        outline_color=HUMANOID_OUTLINE_COLOR,
    )
    _SURVIVOR_DIRECTIONAL_CACHE[cache_key] = surfaces
    return surfaces


def build_zombie_directional_surfaces(
    radius: int,
    *,
    bins: int = ANGLE_BINS,
    draw_hands: bool = True,
    is_trapped: bool = False,
) -> list[pygame.Surface]:
    cache_key = (radius, draw_hands, bins, is_trapped)
    if cache_key in _ZOMBIE_DIRECTIONAL_CACHE:
        return _ZOMBIE_DIRECTIONAL_CACHE[cache_key]

    outline_color = TRAPPED_OUTLINE_COLOR if is_trapped else ZOMBIE_OUTLINE_COLOR

    surfaces = _build_humanoid_directional_surfaces(
        radius,
        base_color=ZOMBIE_BODY_COLOR,
        cap_color=_brighten_color(ZOMBIE_BODY_COLOR),
        bins=bins,
        draw_hands=draw_hands,
        outline_color=outline_color,
    )
    _ZOMBIE_DIRECTIONAL_CACHE[cache_key] = surfaces
    return surfaces


def build_zombie_dog_directional_surfaces(
    long_axis: float,
    short_axis: float,
    *,
    bins: int = ANGLE_BINS,
    is_trapped: bool = False,
) -> list[pygame.Surface]:

    cache_key = (float(long_axis), float(short_axis), bins, is_trapped)

    if cache_key in _ZOMBIE_DOG_DIRECTIONAL_CACHE:
        return _ZOMBIE_DOG_DIRECTIONAL_CACHE[cache_key]

    outline_color = TRAPPED_OUTLINE_COLOR if is_trapped else ZOMBIE_OUTLINE_COLOR

    half_long = long_axis * 0.5

    half_short = short_axis * 0.5

    width = int(math.ceil(long_axis + HUMANOID_OUTLINE_WIDTH * 2))

    height = int(math.ceil(long_axis + HUMANOID_OUTLINE_WIDTH * 2))

    center = (width / 2.0, height / 2.0)

    def _lemon_points(angle_rad: float) -> list[tuple[int, int]]:

        taper = 0.4

        waist = 0.9

        local_points = [
            (half_long, 0.0),
            (half_long * taper, half_short),
            (-half_long * taper, half_short * waist),
            (-half_long, 0.0),
            (-half_long * taper, -half_short * waist),
            (half_long * taper, -half_short),
        ]

        cos_a = math.cos(angle_rad)

        sin_a = math.sin(angle_rad)

        points = []

        for px, py in local_points:
            rx = px * cos_a - py * sin_a + center[0]

            ry = px * sin_a + py * cos_a + center[1]

            points.append((int(round(rx)), int(round(ry))))

        return points

    surfaces: list[pygame.Surface] = []

    for bin_idx in range(bins):
        angle_rad = bin_idx * ANGLE_STEP

        surface = pygame.Surface((width, height), pygame.SRCALPHA)

        points = _lemon_points(angle_rad)

        pygame.draw.polygon(surface, ZOMBIE_BODY_COLOR, points)

        center_x, center_y = center

        highlight_dir = (math.cos(angle_rad) * 0.25, math.sin(angle_rad) * 0.25)

        highlight_points = [
            (
                int(round((px - center_x) * 0.82 + center_x + highlight_dir[0])),
                int(round((py - center_y) * 0.82 + center_y + highlight_dir[1])),
            )
            for px, py in points
        ]

        pygame.draw.polygon(
            surface,
            _brighten_color(ZOMBIE_BODY_COLOR),
            highlight_points,
        )

        if HUMANOID_OUTLINE_WIDTH > 0:
            pygame.draw.polygon(
                surface,
                outline_color,
                points,
                width=HUMANOID_OUTLINE_WIDTH,
            )

        surfaces.append(surface)

    _ZOMBIE_DOG_DIRECTIONAL_CACHE[cache_key] = surfaces

    return surfaces


def build_patrol_bot_directional_surfaces(
    size: int,
    *,
    arrow_scale: float = 1.0,
    marker_mode: str = "single",
    bins: int = ANGLE_BINS,
) -> list[pygame.Surface]:
    cache_key = (int(size), round(float(arrow_scale), 3), marker_mode, bins)
    if cache_key in _PATROL_BOT_DIRECTIONAL_CACHE:
        return _PATROL_BOT_DIRECTIONAL_CACHE[cache_key]
    base_surface = pygame.Surface((size, size), pygame.SRCALPHA)
    center = (size // 2, size // 2)
    radius = max(1, size // 2)
    pygame.draw.circle(base_surface, PATROL_BOT_BODY_COLOR, center, radius)
    pygame.draw.circle(base_surface, PATROL_BOT_OUTLINE_COLOR, center, radius, width=2)
    if marker_mode == "diamond":
        marker_color = tuple(
            min(255, int(c * 0.7 + 255 * 0.3)) for c in PATROL_BOT_ARROW_COLOR
        )
    else:
        marker_color = PATROL_BOT_ARROW_COLOR
    marker = _build_patrol_bot_marker_surface(
        size,
        arrow_scale,
        marker_mode=marker_mode,
        marker_color=marker_color,
    )
    surfaces: list[pygame.Surface] = []
    for idx in range(bins):
        framed = base_surface.copy()
        if marker_mode == "diamond":
            framed.blit(marker, marker.get_rect(center=center))
        else:
            angle_rad = idx * ANGLE_STEP
            offset_radius = max(1.0, (size * 0.3) - (size * 0.22 * arrow_scale))
            eye_x = int(round(center[0] + math.cos(angle_rad) * offset_radius))
            eye_y = int(round(center[1] + math.sin(angle_rad) * offset_radius))
            rotated_marker = pygame.transform.rotozoom(
                marker, -math.degrees(angle_rad), 1.0
            )
            framed.blit(rotated_marker, rotated_marker.get_rect(center=(eye_x, eye_y)))
        surfaces.append(framed)
    _PATROL_BOT_DIRECTIONAL_CACHE[cache_key] = surfaces
    return surfaces


def _build_patrol_bot_marker_surface(
    size: int,
    marker_scale: float,
    *,
    marker_mode: str = "single",
    marker_color: tuple[int, int, int] = PATROL_BOT_ARROW_COLOR,
) -> pygame.Surface:
    surface = pygame.Surface((size, size), pygame.SRCALPHA)
    center = surface.get_rect().center
    marker_size = max(4, int(size * 0.2 * marker_scale))
    if marker_mode == "diamond":
        points = [
            (center[0], center[1] - marker_size),
            (center[0] + marker_size, center[1]),
            (center[0], center[1] + marker_size),
            (center[0] - marker_size, center[1]),
        ]
        pygame.draw.polygon(surface, marker_color, points)
    else:
        points = [
            (center[0] + marker_size, center[1]),
            (center[0], center[1] + marker_size),
            (center[0], center[1] - marker_size),
        ]
        pygame.draw.polygon(surface, marker_color, points)
    return surface


def build_car_surface(width: int, height: int) -> pygame.Surface:
    return pygame.Surface((width, height), pygame.SRCALPHA)


def paint_car_surface(
    surface: pygame.Surface,
    *,
    width: int,
    height: int,
    color: tuple[int, int, int],
) -> None:
    upscale = _CAR_UPSCALE_FACTOR
    if upscale > 1:
        up_width = width * upscale
        up_height = height * upscale
        up_surface = pygame.Surface((up_width, up_height), pygame.SRCALPHA)
        _paint_car_surface_base(
            up_surface, width=up_width, height=up_height, color=color
        )
        scaled = pygame.transform.smoothscale(up_surface, (width, height))
        surface.fill((0, 0, 0, 0))
        surface.blit(scaled, (0, 0))
        return
    _paint_car_surface_base(surface, width=width, height=height, color=color)


def _paint_car_surface_base(
    surface: pygame.Surface,
    *,
    width: int,
    height: int,
    color: tuple[int, int, int],
) -> None:
    surface.fill((0, 0, 0, 0))

    trim_color = tuple(int(c * 0.6) for c in color)
    body_color = tuple(min(255, int(c * 1.15)) for c in color)
    tail_light_color = (255, 80, 50)
    headlight_color = (200, 200, 200)

    base_width = 150.0
    base_height = 210.0
    scale_x = width / base_width
    scale_y = height / base_height

    def _rect(x: float, y: float, w: float, h: float) -> pygame.Rect:
        return pygame.Rect(
            int(round(x * scale_x)),
            int(round(y * scale_y)),
            max(1, int(round(w * scale_x))),
            max(1, int(round(h * scale_y))),
        )

    def _radius(value: float) -> int:
        return max(1, int(round(value * min(scale_x, scale_y))))

    body_top = _rect(0, 0, 150, 140)
    body_bottom = _rect(0, 70, 150, 140)
    rear_bed = _rect(16, 98, 118, 88)

    pygame.draw.rect(surface, trim_color, body_top, border_radius=_radius(50))
    pygame.draw.rect(surface, trim_color, body_bottom, border_radius=_radius(37))
    pygame.draw.rect(surface, body_color, rear_bed)

    tail_left = _rect(30, 190, 30, 20)
    tail_right = _rect(90, 190, 30, 20)
    pygame.draw.rect(surface, tail_light_color, tail_left)
    pygame.draw.rect(surface, tail_light_color, tail_right)

    headlight_left = _rect(15, 7, 40, 20)
    headlight_right = _rect(95, 7, 40, 20)
    pygame.draw.ellipse(surface, headlight_color, headlight_left)
    pygame.draw.ellipse(surface, headlight_color, headlight_right)


def build_car_directional_surfaces(
    base_surface: pygame.Surface, *, bins: int = ANGLE_BINS
) -> list[pygame.Surface]:
    """Return pre-rotated car surfaces matching angle_bin_from_vector bins."""
    surfaces: list[pygame.Surface] = []
    upscale = _CAR_UPSCALE_FACTOR
    if upscale > 1:
        src_size = base_surface.get_size()
        upscale_surface = pygame.transform.scale(
            base_surface,
            (src_size[0] * upscale, src_size[1] * upscale),
        )
    else:
        upscale_surface = base_surface
    for idx in range(bins):
        angle_rad = idx * ANGLE_STEP
        rotation_deg = -math.degrees(angle_rad) - 90
        rotated = pygame.transform.rotate(upscale_surface, rotation_deg)
        if upscale > 1:
            scaled = pygame.transform.smoothscale(
                rotated,
                (
                    max(1, rotated.get_width() // upscale),
                    max(1, rotated.get_height() // upscale),
                ),
            )
            surfaces.append(scaled)
        else:
            surfaces.append(rotated)
    return surfaces


def paint_wall_surface(
    surface: pygame.Surface,
    *,
    fill_color: tuple[int, int, int],
    border_color: tuple[int, int, int],
    bevel_depth: int,
    bevel_mask: tuple[bool, bool, bool, bool],
    draw_bottom_side: bool,
    bottom_side_ratio: float,
    side_shade_ratio: float,
) -> None:
    surface.fill((0, 0, 0, 0))
    rect_obj = surface.get_rect()
    side_height = 0
    if draw_bottom_side:
        side_height = max(1, int(rect_obj.height * bottom_side_ratio))

    def _draw_face(
        target: pygame.Surface,
        *,
        face_size: tuple[int, int] | None = None,
    ) -> None:
        face_width, face_height = face_size or target.get_size()
        if bevel_depth > 0 and any(bevel_mask):
            face_polygon = build_beveled_polygon(
                face_width, face_height, bevel_depth, bevel_mask
            )
            pygame.draw.polygon(target, border_color, face_polygon)
        else:
            target.fill(border_color)
        border_width = 18
        inner_rect = target.get_rect().inflate(-border_width, -border_width)
        if inner_rect.width > 0 and inner_rect.height > 0:
            inner_depth = max(0, bevel_depth - border_width)
            if inner_depth > 0 and any(bevel_mask):
                inner_polygon = build_beveled_polygon(
                    inner_rect.width, inner_rect.height, inner_depth, bevel_mask
                )
                inner_offset_polygon = [
                    (
                        int(point[0] + inner_rect.left),
                        int(point[1] + inner_rect.top),
                    )
                    for point in inner_polygon
                ]
                pygame.draw.polygon(target, fill_color, inner_offset_polygon)
            else:
                pygame.draw.rect(target, fill_color, inner_rect)

    if draw_bottom_side:
        extra_height = max(0, int(bevel_depth / 2))
        side_draw_height = min(rect_obj.height, side_height + extra_height)
        top_rect = pygame.Rect(
            rect_obj.left,
            rect_obj.top,
            rect_obj.width,
            rect_obj.height - side_height,
        )
        side_rect = pygame.Rect(
            rect_obj.left,
            rect_obj.bottom - side_draw_height,
            rect_obj.width,
            side_draw_height,
        )
        side_color = tuple(int(c * side_shade_ratio) for c in fill_color)
        side_surface = pygame.Surface(rect_obj.size, pygame.SRCALPHA)
        if bevel_depth > 0 and any(bevel_mask):
            side_polygon = build_beveled_polygon(
                rect_obj.width, rect_obj.height, bevel_depth, bevel_mask
            )
            pygame.draw.polygon(side_surface, side_color, side_polygon)
        else:
            pygame.draw.rect(side_surface, side_color, rect_obj)
        surface.blit(side_surface, side_rect.topleft, area=side_rect)

        top_height = max(0, rect_obj.height - side_height)
        top_surface = pygame.Surface((rect_obj.width, top_height), pygame.SRCALPHA)
        _draw_face(
            top_surface,
            face_size=(rect_obj.width, top_height),
        )
        if top_rect.height > 0:
            surface.blit(top_surface, top_rect.topleft, area=top_rect)
    else:
        _draw_face(surface)


def build_rubble_wall_surface(
    size: int,
    *,
    fill_color: tuple[int, int, int],
    border_color: tuple[int, int, int],
    angle_deg: float,
    offset_px: int | None = None,
    scale_ratio: float = RUBBLE_SCALE_RATIO,
    shadow_ratio: float = RUBBLE_SHADOW_RATIO,
    bevel_depth: int = INTERNAL_WALL_BEVEL_DEPTH,
) -> pygame.Surface:
    offset_px = offset_px if offset_px is not None else rubble_offset_for_size(size)
    safe_size = max(1, size)
    base_size = max(1, int(round(safe_size * scale_ratio)))
    tuned_bevel = min(bevel_depth, max(1, base_size // 2))
    cache_key = (
        safe_size,
        fill_color,
        border_color,
        angle_deg,
        offset_px,
        scale_ratio,
        shadow_ratio,
        tuned_bevel,
    )
    cached = _RUBBLE_SURFACE_CACHE.get(cache_key)
    if cached is not None:
        return cached

    top_surface = pygame.Surface((base_size, base_size), pygame.SRCALPHA)
    paint_wall_surface(
        top_surface,
        fill_color=fill_color,
        border_color=border_color,
        bevel_depth=tuned_bevel,
        bevel_mask=(False, False, False, False),
        draw_bottom_side=False,
        bottom_side_ratio=0.1,
        side_shade_ratio=0.9,
    )

    shadow_fill = _scale_color(fill_color, ratio=shadow_ratio)
    shadow_border = _scale_color(border_color, ratio=shadow_ratio)
    shadow_surface = pygame.Surface((base_size, base_size), pygame.SRCALPHA)
    paint_wall_surface(
        shadow_surface,
        fill_color=shadow_fill,
        border_color=shadow_border,
        bevel_depth=tuned_bevel,
        bevel_mask=(False, False, False, False),
        draw_bottom_side=False,
        bottom_side_ratio=0.1,
        side_shade_ratio=0.9,
    )

    if angle_deg:
        top_surface = pygame.transform.rotate(top_surface, angle_deg)
        shadow_surface = pygame.transform.rotate(shadow_surface, angle_deg)

    final_surface = pygame.Surface((safe_size, safe_size), pygame.SRCALPHA)
    center = final_surface.get_rect().center

    shadow_rect = shadow_surface.get_rect(
        center=(center[0] + offset_px, center[1] + offset_px)
    )
    final_surface.blit(shadow_surface, shadow_rect.topleft)

    top_rect = top_surface.get_rect(center=center)
    final_surface.blit(top_surface, top_rect.topleft)

    _RUBBLE_SURFACE_CACHE[cache_key] = final_surface
    return final_surface


def paint_steel_beam_surface(
    surface: pygame.Surface,
    *,
    base_color: tuple[int, int, int],
    line_color: tuple[int, int, int],
    health_ratio: float,
) -> None:
    surface.fill((0, 0, 0, 0))
    fill_mix = 0.55 + 0.45 * health_ratio
    fill_color = tuple(int(c * fill_mix) for c in base_color)
    rect_obj = surface.get_rect()
    side_height = max(1, int(rect_obj.height * 0.1))
    top_rect = pygame.Rect(
        rect_obj.left,
        rect_obj.top,
        rect_obj.width,
        rect_obj.height - side_height,
    )
    side_mix = 0.45 + 0.35 * health_ratio
    side_color = tuple(int(c * side_mix * 0.9) for c in base_color)
    side_rect = pygame.Rect(
        rect_obj.left,
        rect_obj.bottom - side_height,
        rect_obj.width,
        side_height,
    )
    pygame.draw.rect(surface, side_color, side_rect)
    line_mix = 0.7 + 0.3 * health_ratio
    tuned_line_color = tuple(int(c * line_mix) for c in line_color)
    top_surface = pygame.Surface(top_rect.size, pygame.SRCALPHA)
    local_rect = top_surface.get_rect()
    pygame.draw.rect(top_surface, fill_color, local_rect)
    pygame.draw.rect(top_surface, tuned_line_color, local_rect, width=6)
    pygame.draw.line(
        top_surface,
        tuned_line_color,
        local_rect.topleft,
        local_rect.bottomright,
        width=6,
    )
    pygame.draw.line(
        top_surface,
        tuned_line_color,
        local_rect.topright,
        local_rect.bottomleft,
        width=6,
    )
    surface.blit(top_surface, top_rect.topleft)


def build_fuel_can_surface(width: int, height: int) -> pygame.Surface:
    surface = pygame.Surface((width, height), pygame.SRCALPHA)
    _draw_fuel_can_body(
        surface,
        width,
        height,
        fill_color=YELLOW,
        emboss_color=(185, 155, 20),
    )
    _draw_fuel_can_cap(surface, width, height, fill_color=BLACK)
    return surface


def build_empty_fuel_can_surface(width: int, height: int) -> pygame.Surface:
    surface = pygame.Surface((width, height), pygame.SRCALPHA)
    _draw_fuel_can_body(surface, width, height, fill_color=LIGHT_GRAY)
    # Keep empty-can look but add a yellow cap accent to match item color language.
    _draw_fuel_can_cap(surface, width, height, fill_color=YELLOW)
    return surface


def _draw_fuel_can_body(
    surface: pygame.Surface,
    width: int,
    height: int,
    *,
    fill_color: tuple[int, int, int],
    emboss_color: tuple[int, int, int] | None = None,
) -> None:
    x1 = max(1, int(round(width * 0.10)))
    y1 = max(1, int(round(height * 0.10)))
    # Keep a small top-right cut so the can still reads as a mostly-rect body.
    x2 = min(width - 2, int(round(width * 0.78)))
    y2 = max(y1 + 1, int(round(height * 0.14)))
    x3 = min(width - 2, int(round(width * 0.86)))
    y3 = min(height - 2, int(round(height * 0.23)))
    x4 = x3
    y4 = min(height - 2, int(round(height * 0.92)))
    x5 = x1
    y5 = y4
    body_points = [
        (x1, y1),
        (x2, y2),
        (x3, y3),
        (x4, y4),
        (x5, y5),
    ]
    pygame.draw.polygon(surface, fill_color, body_points)
    pygame.draw.polygon(surface, BLACK, body_points, width=1)
    # Add a subtle embossed "X" highlight like a jerrycan rib.
    x_left = x1 + max(2, int(round((x3 - x1) * 0.22)))
    x_right = x3 - max(2, int(round((x3 - x1) * 0.22)))
    y_top = y1 + max(3, int(round((y4 - y1) * 0.24)))
    y_bottom = y4 - max(3, int(round((y4 - y1) * 0.20)))
    if x_right > x_left and y_bottom > y_top:
        highlight = emboss_color or tuple(min(255, c + 36) for c in fill_color)
        pygame.draw.line(
            surface, highlight, (x_left, y_top), (x_right, y_bottom), width=1
        )
        pygame.draw.line(
            surface, highlight, (x_left, y_bottom), (x_right, y_top), width=1
        )


def _draw_fuel_can_cap(
    surface: pygame.Surface,
    width: int,
    height: int,
    *,
    fill_color: tuple[int, int, int],
) -> None:
    cap = pygame.Rect(
        max(0, int(round(width * 0.66))),
        max(0, int(round(height * 0.06))),
        max(3, int(round(width * 0.30))),
        max(2, int(round(height * 0.22))),
    )
    cap.clamp_ip(surface.get_rect())
    pygame.draw.rect(surface, fill_color, cap, border_radius=1)
    pygame.draw.rect(surface, BLACK, cap, width=1, border_radius=1)


def build_fuel_station_surface(width: int, height: int) -> pygame.Surface:
    surface = pygame.Surface((width, height), pygame.SRCALPHA)

    def sx(px: int) -> int:
        return int(round(px * width / 14))

    def sy(py: int) -> int:
        return int(round(py * height / 18))

    # Base pedestal
    base = pygame.Rect(sx(0), sy(15), max(1, sx(9) - sx(0)), max(1, sy(18) - sy(15)))
    pygame.draw.rect(surface, YELLOW, base)
    pygame.draw.rect(surface, BLACK, base, width=1)

    # Main body
    body = pygame.Rect(sx(1), sy(1), max(1, sx(8) - sx(1)), max(1, sy(16) - sy(1)))
    pygame.draw.rect(surface, YELLOW, body)
    pygame.draw.rect(surface, BLACK, body, width=1)

    # Display panel
    panel = pygame.Rect(sx(2), sy(3), max(1, sx(7) - sx(2)), max(1, sy(7) - sy(3)))
    pygame.draw.rect(surface, BLACK, panel)

    # Hose support arm (leave a small gap from body to nozzle side)
    arm = pygame.Rect(sx(9), sy(4), max(1, sx(10) - sx(9)), max(1, sy(5) - sy(4)))
    pygame.draw.rect(surface, BLACK, arm)

    # Nozzle block
    nozzle = pygame.Rect(sx(10), sy(4), max(1, sx(13) - sx(10)), max(1, sy(7) - sy(4)))
    pygame.draw.rect(surface, YELLOW, nozzle)
    pygame.draw.rect(surface, BLACK, nozzle, width=1)

    # Hose loop
    hose_w = max(1, sx(11) - sx(10))
    hose_h = max(2, sy(14) - sy(7))
    hose = pygame.Rect(sx(10), sy(7), hose_w, hose_h)
    pygame.draw.rect(surface, BLACK, hose, width=1)

    # Short hose tip back toward the pump
    tip = pygame.Rect(sx(9), sy(13), max(1, sx(10) - sx(9)), max(1, sy(14) - sy(13)))
    pygame.draw.rect(surface, BLACK, tip)

    # Extend lower hose so it visibly connects back into the pump body.
    lower_hose = pygame.Rect(
        sx(9),
        sy(14),
        max(1, sx(10) - sx(9)),
        max(1, sy(16) - sy(14)),
    )
    pygame.draw.rect(surface, BLACK, lower_hose)
    body_connector = pygame.Rect(
        sx(8),
        sy(15),
        max(1, sx(9) - sx(8)),
        max(1, sy(16) - sy(15)),
    )
    pygame.draw.rect(surface, BLACK, body_connector)

    return surface


def build_flashlight_surface(width: int, height: int) -> pygame.Surface:
    return _draw_polygon_surface(width, height, FLASHLIGHT_SPEC)


def build_shoes_surface(width: int, height: int) -> pygame.Surface:
    return _draw_polygon_surface(width, height, SHOES_SPEC)


def get_character_icon(kind: str, size: int) -> pygame.Surface:
    """Return a single icon Surface for the given character kind and base size.

    'size' is generally treated as the radius for humanoids/bots,
    or a base scale for others.
    """
    if kind == "player":
        return build_player_directional_surfaces(size)[0]
    elif kind == "buddy":
        return build_survivor_directional_surfaces(
            size, is_buddy=True, draw_hands=True
        )[0]
    elif kind == "survivor":
        return build_survivor_directional_surfaces(
            size, is_buddy=False, draw_hands=False
        )[0]
    elif kind == "zombie":
        return build_zombie_directional_surfaces(size, draw_hands=False)[0]
    elif kind == "zombie_dog":
        # Dog uses size*2 as long axis, and a slightly thinner short axis.
        return build_zombie_dog_directional_surfaces(
            float(size * 2.4), float(size * 1.6)
        )[0]
    elif kind == "patrol_bot":
        # Patrol bot size in build_patrol_bot_directional_surfaces is total diameter.
        # Humanoids use 'size' as radius, so their diameter is ~2*size.
        # Robot is significantly larger than humanoids in-game, so we use 4*size.
        return build_patrol_bot_directional_surfaces(size * 4)[0]
    elif kind == "car":
        # Create a small car icon.
        width = size * 2
        height = int(width * 1.4)
        surf = pygame.Surface((width, height), pygame.SRCALPHA)
        paint_car_surface(
            surf,
            width=width,
            height=height,
            color=resolve_car_color(health_ratio=1.0, appearance="default"),
        )
        return surf
    elif kind == "fuel_can":
        scaled_size = int(size * 3.2)
        return build_fuel_can_surface(scaled_size, scaled_size)
    elif kind == "empty_fuel_can":
        scaled_size = int(size * 3.2)
        return build_empty_fuel_can_surface(scaled_size, scaled_size)
    elif kind == "shoes":
        scaled_size = int(size * 3.2)
        return build_shoes_surface(scaled_size, scaled_size)

    # Fallback
    fallback = pygame.Surface((size * 2, size * 2), pygame.SRCALPHA)
    pygame.draw.circle(fallback, (128, 128, 128), (size, size), size)
    return fallback


def get_tile_icon(kind: str, size: int) -> pygame.Surface:
    """Return a single icon Surface for special tile types."""
    # Use size*3.2 (approx 10px) to balance with humanoid icons
    scaled_size = int(size * 3.2)
    surf = pygame.Surface((scaled_size, scaled_size), pygame.SRCALPHA)
    rect = surf.get_rect()

    if kind == "pitfall":
        # Dark abyss with a slight metallic top edge
        pygame.draw.rect(surf, (21, 20, 20), rect)
        pygame.draw.rect(surf, (110, 110, 115), (0, 0, rect.width, 2))
    elif kind == "fall_spawn":
        # Orange-brown floor color
        pygame.draw.rect(surf, (84, 48, 29), rect)
        # Add a small dot to indicate it's a spawn point?
        pygame.draw.circle(surf, (180, 0, 0), rect.center, size // 2)
    elif kind == "moving_floor":
        # Gray tile with a simple ">" marker.
        pygame.draw.rect(surf, (90, 90, 90), rect)
        arrow_color = (130, 130, 130)
        cx, cy = rect.center
        half_h = max(2, rect.height // 4)
        left_x = max(1, cx - (rect.width // 5))
        right_x = min(rect.right - 2, cx + (rect.width // 5))
        pygame.draw.line(
            surf,
            arrow_color,
            (left_x, cy - half_h),
            (right_x, cy),
            width=2,
        )
        pygame.draw.line(
            surf,
            arrow_color,
            (left_x, cy + half_h),
            (right_x, cy),
            width=2,
        )
    elif kind == "fire_floor":
        # Dark red tile with a bright diamond cutout.
        base = (55, 20, 18)
        diamond = (170, 36, 36)
        pygame.draw.rect(surf, base, rect)
        inset = max(1, int(round(rect.width * 0.22)))
        cx, cy = rect.center
        points = [
            (cx, rect.top + inset),
            (rect.right - inset, cy),
            (cx, rect.bottom - inset),
            (rect.left + inset, cy),
        ]
        pygame.draw.polygon(surf, diamond, points)
    elif kind == "puddle":
        # Transparent water effect: single ring only, slightly larger for visibility.
        ring_color = (95, 135, 185)
        ring_rect = rect.inflate(-1, -1)
        # Enlarge by 10% while keeping center.
        inflate_w = max(1, int(round(ring_rect.width * 0.1)))
        inflate_h = max(1, int(round(ring_rect.height * 0.1)))
        ring_rect = ring_rect.inflate(inflate_w, inflate_h)
        if ring_rect.width > 0 and ring_rect.height > 0:
            pygame.draw.ellipse(surf, ring_color, ring_rect, width=1)
    elif kind == "spiky_plant":
        # Simple marker: body + four spikes.
        center = rect.center
        body_radius = max(2, scaled_size // 4)
        pygame.draw.circle(surf, SPIKY_PLANT_BODY_COLOR, center, max(1, body_radius - 1))
        spike_inner = max(1, body_radius)
        spike_outer = max(spike_inner + 1, body_radius + 1)
        for i in range(4):
            angle = i * 90
            direction = pygame.Vector2(1, 0).rotate(angle)
            start = (
                int(center[0] + direction.x * spike_inner),
                int(center[1] + direction.y * spike_inner),
            )
            end = (
                int(center[0] + direction.x * spike_outer),
                int(center[1] + direction.y * spike_outer),
            )
            pygame.draw.line(surf, SPIKY_PLANT_SPIKE_COLOR, start, end, width=1)
    else:
        pygame.draw.rect(surf, (128, 128, 128), rect, width=1)

    return surf


__all__ = [
    "angle_bin_from_vector",
    "EnvironmentPalette",
    "RenderAssets",
    "build_beveled_polygon",
    "resolve_wall_colors",
    "resolve_car_color",
    "resolve_steel_beam_colors",
    "build_player_directional_surfaces",
    "draw_humanoid_hand",
    "draw_humanoid_nose",
    "draw_lineformer_direction_arm",
    "draw_lightning_marker",
    "build_survivor_directional_surfaces",
    "build_zombie_directional_surfaces",
    "build_zombie_dog_directional_surfaces",
    "build_patrol_bot_directional_surfaces",
    "build_car_surface",
    "build_car_directional_surfaces",
    "paint_car_surface",
    "paint_wall_surface",
    "build_rubble_wall_surface",
    "rubble_offset_for_size",
    "RUBBLE_ROTATION_DEG",
    "paint_steel_beam_surface",
    "build_fuel_can_surface",
    "build_empty_fuel_can_surface",
    "build_fuel_station_surface",
    "build_flashlight_surface",
    "build_shoes_surface",
    "get_character_icon",
    "get_tile_icon",
]
