"""A compact PyGame basics demo.

- standard game loop
- delta-time based motion
- drawing on the screen
- keyboard control
- mouse interaction
- object-oriented structure
"""

from __future__ import annotations

from collections import deque
import math
from dataclasses import dataclass, field
from typing import Any, ClassVar

import pygame

# Window and rendering constants.
WIDTH = 900
HEIGHT = 600
BACKGROUND = (24, 28, 36)
PLAYER_COLOR = (80, 170, 255)
MARKER_COLOR = (255, 210, 80)
TEXT_COLOR = (240, 240, 240)

# Movement and object limits.
PLAYER_SPEED = 260.0
MAX_MARKERS = 10


class GameObject:
    # Global count of instantiated game object subclasses.
    game_objects_created: ClassVar[int] = 0

    def __init__(self):
        # Increment base-class counter for every created instance.
        GameObject.game_objects_created += 1

    def update(self, dt: float) -> None:
        # Common update interface for dynamic objects.
        pass

    def draw(self, surface: pygame.Surface) -> None:
        # Common draw interface for renderable objects.
        pass


@dataclass
class MarkerCircle(GameObject):
    """A short-lived circle created with the mouse-click."""

    # World position in screen coordinates.
    position: pygame.Vector2
    # Remaining lifetime in seconds.
    lifetime: float = 2.0
    radius: int = 10

    def __post_init__(self):
        # dataclass-generated __init__ does not call base __init__ automatically.
        super().__init__()

    def update(self, dt: float) -> None:
        # Lifetime countdown driven by delta time.
        self.lifetime -= dt

    @property
    def alive(self) -> bool:
        # Object is active while lifetime is positive.
        return self.lifetime > 0

    def draw(self, surface: pygame.Surface) -> None:
        # Alpha fades with lifetime, but stays above a minimum visibility threshold.
        alpha = max(40, min(255, int(255 * self.lifetime / 2.0)))
        # Temporary per-pixel-alpha surface avoids redrawing the whole screen with transparency.
        temp = pygame.Surface((2 * self.radius + 4, 2 * self.radius + 4), pygame.SRCALPHA)
        pygame.draw.circle(
            temp,
            (*MARKER_COLOR, alpha),
            (self.radius + 2, self.radius + 2),
            self.radius,
        )
        # Blit so that the marker is centered at self.position.
        surface.blit(temp, (self.position.x - self.radius - 2, self.position.y - self.radius - 2))


class MarkerSquare(MarkerCircle):

    def __init__(self, position: pygame.Vector2, lifetime: float = 5.0, radius: int = 20):
        # Reuse parent initialization and counting logic.
        super().__init__(position, lifetime, radius)

    def draw(self, surface: pygame.Surface) -> None:
        # Same fading logic as MarkerCircle, different primitive.
        alpha = max(40, min(255, int(255 * self.lifetime / 2.0)))
        temp = pygame.Surface((2 * self.radius + 4, 2 * self.radius + 4), pygame.SRCALPHA)
        pygame.draw.rect(
            temp,
            (*MARKER_COLOR, alpha),
            (self.radius + 2, self.radius + 2, self.radius, self.radius),
        )
        surface.blit(temp, (self.position.x - self.radius - 2, self.position.y - self.radius - 2))


@dataclass
class Player:
    # Start at screen center.
    position: pygame.Vector2 = field(default_factory=lambda: pygame.Vector2(WIDTH / 2, HEIGHT / 2))
    radius: int = 22
    # Facing direction in radians.
    angle: float = 0.0

    def update(self, dt: float) -> None:
        # Poll current keyboard state once per frame.
        keys = pygame.key.get_pressed()
        velocity = pygame.Vector2(0, 0)

        # Build movement direction from pressed keys.
        if keys[pygame.K_LEFT] or keys[pygame.K_a]:
            velocity.x -= 1
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            velocity.x += 1
        if keys[pygame.K_UP] or keys[pygame.K_w]:
            velocity.y -= 1
        if keys[pygame.K_DOWN] or keys[pygame.K_s]:
            velocity.y += 1

        if velocity.length_squared() > 0:
            # Normalize to prevent faster diagonal motion, then scale by speed.
            velocity = velocity.normalize() * PLAYER_SPEED
            # Store movement direction for rendering the facing indicator.
            self.angle = math.atan2(velocity.y, velocity.x)

        # Delta-time scaling makes movement frame-rate independent.
        self.position += velocity * dt

        # Clamp position so the player stays inside the window.
        self.position.x = max(self.radius, min(WIDTH - self.radius, self.position.x))
        self.position.y = max(self.radius, min(HEIGHT - self.radius, self.position.y))

    def draw(self, surface: pygame.Surface) -> None:
        # Main body.
        pygame.draw.circle(surface, PLAYER_COLOR, self.position, self.radius)
        # Short line indicating current facing direction.
        nose = self.position + pygame.Vector2(math.cos(self.angle), math.sin(self.angle)) * (self.radius + 8)
        pygame.draw.line(surface, (255, 255, 255), self.position, nose, 4)


class App:
    """The main PyGame program."""

    def __init__(self) -> None:
        # Initialize PyGame before creating window, clock, and font.
        pygame.init()
        pygame.display.set_caption("PyGame Basics Demo")
        self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
        self.clock = pygame.time.Clock()
        self.font = pygame.font.SysFont(None, 30)

        # Main application state.
        self.player = Player()
        self.markers = deque(maxlen=MAX_MARKERS)  # automatically discards oldest markers when full
        self.running = True

    def handle_events(self) -> None:
        # Event queue processing: quit, keyboard, mouse.
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
                self.running = False
            elif event.type == pygame.MOUSEBUTTONDOWN:
                if event.button == 1:
                    # Left click: short-lived circular marker.
                    self.markers.append(MarkerCircle(position=pygame.Vector2(event.pos)))
                elif event.button == 3:
                    # Right click: longer-lived square marker.
                    self.markers.append(MarkerSquare(position=pygame.Vector2(event.pos)))

    def update(self, dt: float) -> None:
        # Update all dynamic objects.
        self.player.update(dt)
        for marker in self.markers:
            marker.update(dt)

        # Remove expired markers.
        # tuple(...) creates a stable snapshot of the deque
        # so removal does not modify the "active iteration target".
        for marker in tuple(self.markers):
            if not marker.alive:
                self.markers.remove(marker)

    def draw(self) -> None:
        # Clear frame buffer.
        self.screen.fill(BACKGROUND)

        # Draw world objects.
        for marker in self.markers:
            marker.draw(self.screen)
        self.player.draw(self.screen)

        # Prepare small diagnostic / instruction overlay.
        lines = [
            "Move with arrows or WASD.",
            "Left click to place markers.",
            "Press ESC to quit.",
            f"FPS: {int(self.clock.get_fps())}",
            f"Game objects created: {GameObject.game_objects_created}",
            f"Active game objects: {len(self.markers)}",
        ]
        for index, text in enumerate(lines):
            surface = self.font.render(text, True, TEXT_COLOR)
            self.screen.blit(surface, (20, 20 + 28 * index))

        # Show rendered frame.
        pygame.display.flip()

    def run(self) -> None:
        # Standard game loop: time step, events, update, draw.
        while self.running:
            dt = self.clock.tick(60) / 1000.0  # frame duration in seconds
            self.handle_events()
            self.update(dt)
            self.draw()

        # Release PyGame resources on exit.
        pygame.quit()


if __name__ == "__main__":
    # Script entry point.
    App().run()
