"""space_invaders.py — a small space game in Python.

You need pygame first:

    pip3 install pygame

Then run the game:

    python3 space_invaders.py

Move with the LEFT and RIGHT arrow keys. Shoot with SPACE.
Press R to play again. Press ESC to stop.

Pictures are optional. Put the five .png files EITHER in a folder called
"images" next to this file, OR in the same folder as this file. The game
looks in both. If it finds nothing, it draws simple coloured boxes instead.
"""
import math
import random
from pathlib import Path

import pygame

# ─────────────────────────────────────────────────────────────
# Settings. These never change while the game runs.
# ─────────────────────────────────────────────────────────────
WIDTH = 960
HEIGHT = 640
FPS = 60

PICTURES = ["background.png", "player.png", "creep1.png", "creep2.png", "creep3.png"]

WHITE = (255, 255, 255)
YELLOW = (255, 220, 60)
RED = (255, 80, 80)
GREY = (120, 130, 150)
DARK = (10, 12, 30)


def find_image_dir():
    """Find the folder with the pictures.

    We look next to this file, in "images" first and then in the folder
    itself. Downloads often land next to the game, not in a subfolder.
    """
    here = Path(__file__).resolve().parent
    for folder in (here / "images", here):
        if (folder / "background.png").exists():
            return folder
    return here / "images"          # nothing found — report this one


IMAGE_DIR = find_image_dir()


def report_images():
    """Say what we found. A silent game with no pictures is confusing."""
    found = [n for n in PICTURES if (IMAGE_DIR / n).exists()]
    print(f"Looking for pictures in: {IMAGE_DIR}")
    print(f"Found {len(found)} of {len(PICTURES)}.")
    if len(found) < len(PICTURES):
        missing = [n for n in PICTURES if n not in found]
        print("Missing:", ", ".join(missing))
        print("The game still works. It draws coloured boxes instead.")
        print(f"To use the pictures, put the .png files here: {IMAGE_DIR}")


def load_image(name, size, colour):
    """Load a picture. If it is not there, make a coloured box instead.

    The game always works, with or without the pictures.
    """
    path = IMAGE_DIR / name
    if path.exists():
        try:
            picture = pygame.image.load(str(path)).convert_alpha()
            return pygame.transform.smoothscale(picture, size)
        except pygame.error as problem:
            print(f"Could not read {path}: {problem}")
    box = pygame.Surface(size, pygame.SRCALPHA)
    box.fill(colour)
    return box


# ─────────────────────────────────────────────────────────────
# A bullet. The smallest class in the game.
# ─────────────────────────────────────────────────────────────
class Bullet:
    def __init__(self, x, y, speed, colour):
        # __init__ runs once, when we make a new bullet.
        # "self" means "this one bullet".
        self.x = x
        self.y = y
        self.speed = speed          # negative goes up, positive goes down
        self.colour = colour

    def update(self):
        """Move the bullet. We call this once every frame."""
        self.y += self.speed

    def is_gone(self):
        """True when the bullet has left the screen."""
        return self.y < -20 or self.y > HEIGHT + 20

    def rect(self):
        """The box around the bullet. We use it to check for hits."""
        return pygame.Rect(self.x - 2, self.y - 8, 4, 16)

    def draw(self, screen):
        pygame.draw.rect(screen, self.colour, self.rect())


# ─────────────────────────────────────────────────────────────
# The player's ship.
# ─────────────────────────────────────────────────────────────
class Player:
    def __init__(self, image):
        self.image = image
        self.x = WIDTH // 2
        self.y = HEIGHT - 70
        self.speed = 6
        self.lives = 3
        self.score = 0
        self.cool_down = 0          # frames to wait before shooting again

    def update(self, keys):
        """Move the ship if an arrow key is down."""
        if keys[pygame.K_LEFT]:
            self.x -= self.speed
        if keys[pygame.K_RIGHT]:
            self.x += self.speed

        # Stay on the screen.
        half = self.image.get_width() // 2
        self.x = max(half, min(WIDTH - half, self.x))

        if self.cool_down > 0:
            self.cool_down -= 1

    def shoot(self):
        """Make a bullet, but only if the gun is ready."""
        if self.cool_down > 0:
            return None
        self.cool_down = 12
        return Bullet(self.x, self.y - 24, -10, YELLOW)

    def rect(self):
        box = self.image.get_rect()
        box.center = (self.x, self.y)
        return box

    def draw(self, screen):
        screen.blit(self.image, self.rect())


# ─────────────────────────────────────────────────────────────
# A creep — the enemy. This is the important class.
#
# Every creep is made from the SAME class, but each one gets
# DIFFERENT numbers in __init__. That is why they all behave
# differently on the screen.
# ─────────────────────────────────────────────────────────────
class Creep:
    def __init__(self, image, y, speed, path, hp, fire_rate):
        self.image = image
        self.x = -50                # every creep starts off the left edge
        self.start_y = y            # its line across the screen
        self.y = y
        self.speed = speed          # how fast it flies right
        self.path = path            # "straight" or "curve"
        self.hp = hp                # hit points: 1, 2 or 3
        self.fire_rate = fire_rate  # shoot every N frames — small = often
        self.timer = random.randint(0, fire_rate)
        self.wave = 0.0             # only used by curved creeps

    def update(self):
        """Move the creep and count towards its next shot."""
        self.x += self.speed

        if self.path == "curve":
            self.wave += 0.04
            self.y = self.start_y + math.sin(self.wave) * 70

        self.timer += 1

    def wants_to_shoot(self):
        """True when this creep is ready to fire."""
        if self.timer >= self.fire_rate:
            self.timer = 0
            return True
        return False

    def is_gone(self):
        """True when the creep has flown off the right edge."""
        return self.x > WIDTH + 60

    def rect(self):
        box = self.image.get_rect()
        box.center = (self.x, self.y)
        return box

    def draw(self, screen):
        screen.blit(self.image, self.rect())

        # One small bar for each hit point still left.
        for i in range(self.hp):
            left = self.x - 12 + i * 9
            pygame.draw.rect(screen, RED, (left, self.y - 28, 7, 4))


def make_creep(images):
    """Make ONE creep with random numbers.

    This is where the same class becomes many different enemies.
    """
    hp = random.choice([1, 1, 2, 2, 3])
    image = images[hp]

    return Creep(
        image=image,
        y=random.randint(70, HEIGHT // 2),
        speed=random.uniform(1.0, 3.2),
        path=random.choice(["straight", "curve"]),
        hp=hp,
        fire_rate=random.randint(70, 200),
    )


def draw_text(screen, font, text, x, y, colour=WHITE):
    screen.blit(font.render(text, True, colour), (x, y))


# ─────────────────────────────────────────────────────────────
# The game itself.
# ─────────────────────────────────────────────────────────────
def main():
    report_images()

    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("Space Invaders — Krueng AI")
    clock = pygame.time.Clock()
    font = pygame.font.SysFont(None, 30)
    big_font = pygame.font.SysFont(None, 64)

    # Pictures must be loaded AFTER the window is made.
    background = load_image("background.png", (WIDTH, HEIGHT), DARK)
    player_image = load_image("player.png", (54, 54), WHITE)
    creep_images = {
        1: load_image("creep1.png", (48, 48), (90, 220, 230)),
        2: load_image("creep2.png", (56, 56), (240, 160, 60)),
        3: load_image("creep3.png", (64, 64), (190, 110, 230)),
    }

    player = Player(player_image)
    creeps = []
    my_bullets = []
    their_bullets = []
    spawn_timer = 0
    playing = True
    running = True

    while running:
        # ---- 1. events ----
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    running = False
                elif event.key == pygame.K_SPACE and playing:
                    bullet = player.shoot()
                    if bullet is not None:
                        my_bullets.append(bullet)
                elif event.key == pygame.K_r and not playing:
                    player = Player(player_image)
                    creeps = []
                    my_bullets = []
                    their_bullets = []
                    playing = True

        if playing:
            # ---- 2. move everything ----
            keys = pygame.key.get_pressed()
            player.update(keys)

            spawn_timer += 1
            if spawn_timer >= 45:
                spawn_timer = 0
                creeps.append(make_creep(creep_images))

            for creep in creeps:
                creep.update()
                if creep.wants_to_shoot():
                    their_bullets.append(Bullet(creep.x, creep.y + 20, 5, RED))

            for bullet in my_bullets + their_bullets:
                bullet.update()

            # ---- 3. check for hits ----
            for bullet in list(my_bullets):
                for creep in list(creeps):
                    if bullet.rect().colliderect(creep.rect()):
                        my_bullets.remove(bullet)
                        creep.hp -= 1           # take one hit point away
                        if creep.hp <= 0:
                            creeps.remove(creep)
                            player.score += 10
                        break

            for bullet in list(their_bullets):
                if bullet.rect().colliderect(player.rect()):
                    their_bullets.remove(bullet)
                    player.lives -= 1
                    if player.lives <= 0:
                        playing = False

            # ---- 4. tidy up ----
            creeps = [c for c in creeps if not c.is_gone()]
            my_bullets = [b for b in my_bullets if not b.is_gone()]
            their_bullets = [b for b in their_bullets if not b.is_gone()]

        # ---- 5. draw everything ----
        screen.blit(background, (0, 0))
        for creep in creeps:
            creep.draw(screen)
        for bullet in my_bullets + their_bullets:
            bullet.draw(screen)
        player.draw(screen)

        draw_text(screen, font, f"Score {player.score}", 16, 14)
        draw_text(screen, font, f"Lives {player.lives}", WIDTH - 110, 14)

        if not playing:
            draw_text(screen, big_font, "GAME OVER", WIDTH // 2 - 150, HEIGHT // 2 - 40)
            draw_text(screen, font, "Press R to play again", WIDTH // 2 - 110, HEIGHT // 2 + 30, GREY)

        pygame.display.flip()
        clock.tick(FPS)

    pygame.quit()


# ─────────────────────────────────────────────────────────────
# The entry point. Python runs main() only when you start THIS
# file. If another file imports this one, main() does not run.
# ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
    main()
