import math
import random
from dataclasses import dataclass, field

from js import document, window
from pyodide.ffi import create_proxy


WIDTH = 960
HEIGHT = 540
ROAD_TOP = 318
ROAD_BOTTOM = 512
PLAYER_X = 150
GAME_LENGTH = 90.0
MUSIC_VOLUME = 0.18

COLORS = {
    "cream": "#f7f3e8",
    "green": "#08331f",
    "leaf": "#159c5b",
    "sage": "#76a968",
    "mint": "#dff3e4",
    "sky": "#bfe6ef",
    "grass": "#87bf72",
    "road": "#555b5d",
    "road_dark": "#3e4345",
    "line": "#f6e4a8",
    "white": "#ffffff",
    "red": "#cf5447",
    "orange": "#e58a3b",
    "yellow": "#f0ca4d",
    "blue": "#438ca3",
    "brown": "#8b6040",
    "wood": "#b98455",
    "dark": "#17231c",
    "muted": "#527a48",
    "shadow": "rgba(8, 51, 31, 0.18)",
}

REPAIRS = [
    {"key": "roof", "name": "Roofing", "short": "ROOF", "color": "#cf5447", "symbol": "▲"},
    {"key": "window", "name": "Windows", "short": "WINDOW", "color": "#438ca3", "symbol": "▦"},
    {"key": "paint", "name": "Painting", "short": "PAINT", "color": "#e58a3b", "symbol": "▣"},
    {"key": "deck", "name": "Decking", "short": "DECK", "color": "#8b6040", "symbol": "▬"},
]

RANKS = [
    (0, "Freshest Rookie"),
    (750, "Project Starter"),
    (1500, "Neighborhood Fixer"),
    (2600, "Renovation Pro"),
    (4000, "Freshest Home Hero"),
    (5600, "Freshest Contractor in Town"),
]

WRONG_LINES = [
    "THAT IS NOT HOW ROOFING WORKS",
    "THE HOA JUST FELT A DISTURBANCE",
    "YOU PAINTED THE PROBLEM",
    "BOLD CHOICE. WRONG CHOICE.",
    "THE HOMEOWNER HAS QUESTIONS",
    "THAT WILL NOT PASS INSPECTION",
]

HIT_LINES = [
    "GNOME AMBUSH",
    "WHEELBARROWED",
    "GOOSE INCIDENT",
    "TRASH DAY WINS",
    "SPRINKLER SURPRISE",
    "LUMBER HAPPENS",
]


@dataclass
class House:
    x: float
    style: int
    repair_index: int
    width: float = 172.0
    repaired: bool = False
    locked: bool = False
    flash: float = 0.0
    gag: float = 0.0
    missed: bool = False


@dataclass
class Obstacle:
    x: float
    y: float
    kind: str
    size: float
    hit: bool = False
    bob: float = 0.0


@dataclass
class PowerUp:
    x: float
    y: float
    kind: str
    size: float = 34.0
    collected: bool = False
    bob: float = 0.0


@dataclass
class Projectile:
    x: float
    y: float
    repair_index: int
    target: House | None
    target_x: float
    target_y: float
    speed: float = 760.0
    alive: bool = True
    angle: float = 0.0


@dataclass
class Particle:
    x: float
    y: float
    vx: float
    vy: float
    life: float
    color: str
    size: float
    gravity: float = 280.0


@dataclass
class Popup:
    text: str
    x: float
    y: float
    life: float
    color: str
    size: int = 24


@dataclass
class Game:
    canvas: object
    ctx: object
    state: str = "intro"
    score: int = 0
    combo: int = 0
    best_combo: int = 0
    health: int = 3
    time_left: float = GAME_LENGTH
    elapsed: float = 0.0
    selected_repair: int = 0
    player_y: float = 411.0
    player_target_y: float = 411.0
    wheel_angle: float = 0.0
    road_offset: float = 0.0
    house_timer: float = 0.5
    obstacle_timer: float = 2.8
    power_timer: float = 9.0
    invulnerable: float = 0.0
    stumble: float = 0.0
    permit_time: float = 0.0
    boost_time: float = 0.0
    shield_time: float = 0.0
    repair_cooldown: float = 0.0
    keys: set = field(default_factory=set)
    houses: list = field(default_factory=list)
    obstacles: list = field(default_factory=list)
    powerups: list = field(default_factory=list)
    projectiles: list = field(default_factory=list)
    particles: list = field(default_factory=list)
    popups: list = field(default_factory=list)
    touch_up: bool = False
    touch_down: bool = False
    muted: bool = False
    audio_context: object | None = None
    music_element: object | None = None
    last_time: float = 0.0
    pointer_id: int | None = None

    def reset(self):
        self.state = "playing"
        self.score = 0
        self.combo = 0
        self.best_combo = 0
        self.health = 3
        self.time_left = GAME_LENGTH
        self.elapsed = 0.0
        self.selected_repair = 0
        self.player_y = 411.0
        self.player_target_y = 411.0
        self.wheel_angle = 0.0
        self.road_offset = 0.0
        self.house_timer = 0.7
        self.obstacle_timer = 3.2
        self.power_timer = 10.0
        self.invulnerable = 0.0
        self.stumble = 0.0
        self.permit_time = 0.0
        self.boost_time = 0.0
        self.shield_time = 0.0
        self.repair_cooldown = 0.0
        self.houses = []
        self.obstacles = []
        self.powerups = []
        self.projectiles = []
        self.particles = []
        self.popups = []
        self.touch_up = False
        self.touch_down = False
        self.spawn_house(1010)
        self.canvas.focus()
        self.start_music(restart=True)
        self.play_tone(520, 0.08, 0.035)

    def play_tone(self, frequency, duration, volume=0.04):
        if self.muted:
            return
        try:
            if self.audio_context is None:
                audio_constructor = getattr(window, "AudioContext", None) or getattr(window, "webkitAudioContext", None)
                if audio_constructor is None:
                    return
                self.audio_context = audio_constructor.new()
            if self.audio_context.state == "suspended":
                self.audio_context.resume()
            oscillator = self.audio_context.createOscillator()
            gain = self.audio_context.createGain()
            now = self.audio_context.currentTime
            oscillator.type = "square"
            oscillator.frequency.setValueAtTime(frequency, now)
            gain.gain.setValueAtTime(volume, now)
            gain.gain.exponentialRampToValueAtTime(0.001, now + duration)
            oscillator.connect(gain)
            gain.connect(self.audio_context.destination)
            oscillator.start(now)
            oscillator.stop(now + duration)
        except Exception:
            self.audio_context = None

    def ensure_music(self):
        if self.music_element is not None:
            return self.music_element
        try:
            self.music_element = document.getElementById("game-music")
            if self.music_element is not None:
                self.music_element.volume = MUSIC_VOLUME
                self.music_element.loop = True
                self.music_element.muted = self.muted
        except Exception:
            self.music_element = None
        return self.music_element

    def start_music(self, restart=False):
        music = self.ensure_music()
        if music is None:
            return
        try:
            music.muted = self.muted
            if restart:
                music.currentTime = 0
            if not self.muted:
                music.play()
        except Exception:
            pass

    def pause_music(self):
        music = self.ensure_music()
        if music is None:
            return
        try:
            music.pause()
        except Exception:
            pass

    def toggle_mute(self):
        self.muted = not self.muted
        music = self.ensure_music()
        if music is not None:
            try:
                music.muted = self.muted
            except Exception:
                pass
        if not self.muted and self.state == "playing":
            self.start_music()

    def pause_game(self):
        if self.state == "playing":
            self.state = "paused"
            self.pause_music()

    def resume_game(self):
        if self.state == "paused":
            self.state = "playing"
            self.start_music()

    def world_speed(self):
        base = 195.0 + min(65.0, self.elapsed * 0.85)
        if self.stumble > 0:
            base *= 0.62
        return base

    def spawn_house(self, x=1060):
        available = list(range(len(REPAIRS)))
        if self.houses:
            last = self.houses[-1].repair_index
            if last in available and len(available) > 1:
                available.remove(last)
        self.houses.append(House(x=float(x), style=random.randint(0, 4), repair_index=random.choice(available)))

    def spawn_obstacle(self):
        kinds = ["gnome", "trash", "goose", "sprinkler", "lumber", "wheelbarrow"]
        lane = random.choice([365.0, 415.0, 466.0])
        self.obstacles.append(Obstacle(1040.0, lane, random.choice(kinds), random.choice([30.0, 34.0, 38.0])))

    def spawn_powerup(self):
        kinds = ["permit", "boost", "shield"]
        lane = random.choice([365.0, 415.0, 466.0])
        self.powerups.append(PowerUp(1040.0, lane, random.choice(kinds)))

    def update(self, dt):
        if self.state != "playing":
            return

        self.elapsed += dt
        self.time_left = max(0.0, self.time_left - dt)
        self.invulnerable = max(0.0, self.invulnerable - dt)
        self.stumble = max(0.0, self.stumble - dt)
        self.permit_time = max(0.0, self.permit_time - dt)
        self.boost_time = max(0.0, self.boost_time - dt)
        self.shield_time = max(0.0, self.shield_time - dt)
        self.repair_cooldown = max(0.0, self.repair_cooldown - dt)

        if self.time_left <= 0 or self.health <= 0:
            self.finish_game()
            return

        direction = 0
        if "ArrowUp" in self.keys or "w" in self.keys or "W" in self.keys or self.touch_up:
            direction -= 1
        if "ArrowDown" in self.keys or "s" in self.keys or "S" in self.keys or self.touch_down:
            direction += 1

        self.player_target_y += direction * 210.0 * dt
        self.player_target_y = clamp(self.player_target_y, 352.0, 471.0)
        self.player_y += (self.player_target_y - self.player_y) * min(1.0, dt * 10.0)

        speed = self.world_speed()
        self.road_offset = (self.road_offset + speed * dt) % 92.0
        self.wheel_angle += speed * dt * 0.055

        self.house_timer -= dt
        if self.house_timer <= 0:
            self.spawn_house()
            self.house_timer = random.uniform(2.35, 3.10) * max(0.80, 1.0 - self.elapsed / 300.0)

        self.obstacle_timer -= dt
        if self.obstacle_timer <= 0:
            self.spawn_obstacle()
            self.obstacle_timer = random.uniform(1.85, 2.70) * max(0.78, 1.0 - self.elapsed / 260.0)

        self.power_timer -= dt
        if self.power_timer <= 0:
            self.spawn_powerup()
            self.power_timer = random.uniform(12.0, 16.0)

        for house in self.houses:
            house.x -= speed * dt
            house.flash = max(0.0, house.flash - dt)
            house.gag = max(0.0, house.gag - dt)
            if house.x + house.width < 0 and not house.repaired and not house.missed:
                house.missed = True
                self.combo = 0
                self.add_popup("MISSED JOB", 160, 278, COLORS["muted"], 20)

        for obstacle in self.obstacles:
            obstacle.x -= speed * dt
            obstacle.bob += dt * 5.5
            if not obstacle.hit and self.player_hits(obstacle.x, obstacle.y, obstacle.size):
                self.hit_obstacle(obstacle)

        for powerup in self.powerups:
            powerup.x -= speed * dt
            powerup.bob += dt * 4.0
            if not powerup.collected and self.player_hits(powerup.x, powerup.y, powerup.size):
                self.collect_powerup(powerup)

        for projectile in self.projectiles:
            self.update_projectile(projectile, dt)

        for particle in self.particles:
            particle.life -= dt
            particle.vy += particle.gravity * dt
            particle.x += particle.vx * dt
            particle.y += particle.vy * dt

        for popup in self.popups:
            popup.life -= dt
            popup.y -= dt * 28.0

        self.houses = [h for h in self.houses if h.x + h.width > -80]
        self.obstacles = [o for o in self.obstacles if o.x > -100 and not (o.hit and o.x < -30)]
        self.powerups = [p for p in self.powerups if p.x > -80 and not p.collected]
        self.projectiles = [p for p in self.projectiles if p.alive]
        self.particles = [p for p in self.particles if p.life > 0]
        self.popups = [p for p in self.popups if p.life > 0]

    def player_hits(self, x, y, size):
        px = PLAYER_X - 58
        py = self.player_y - 28
        pw = 116
        ph = 58
        return rects_overlap(px, py, pw, ph, x - size / 2, y - size / 2, size, size)

    def hit_obstacle(self, obstacle):
        obstacle.hit = True
        self.combo = 0
        if self.shield_time > 0:
            self.shield_time = 0.0
            self.add_popup("SAFETY GEAR SAVED IT", PLAYER_X + 20, self.player_y - 62, COLORS["leaf"], 20)
            self.burst(PLAYER_X, self.player_y, COLORS["leaf"], 18)
            self.play_tone(720, 0.08, 0.03)
            return
        if self.invulnerable > 0:
            return
        self.health -= 1
        self.invulnerable = 1.5
        self.stumble = 0.9
        self.score = max(0, self.score - 75)
        self.add_popup(random.choice(HIT_LINES), PLAYER_X + 30, self.player_y - 70, COLORS["red"], 22)
        self.burst(PLAYER_X, self.player_y, COLORS["red"], 24)
        self.play_tone(115, 0.22, 0.055)

    def collect_powerup(self, powerup):
        powerup.collected = True
        if powerup.kind == "permit":
            self.permit_time = 7.0
            label = "PERMIT APPROVED"
            color = COLORS["blue"]
        elif powerup.kind == "boost":
            self.boost_time = 7.0
            label = "FRESHEST BOOST ×2"
            color = COLORS["orange"]
        else:
            self.shield_time = 9.0
            label = "SAFETY GEAR"
            color = COLORS["leaf"]
        self.score += 50
        self.add_popup(label, PLAYER_X + 35, self.player_y - 72, color, 22)
        self.burst(powerup.x, powerup.y, color, 22)
        self.play_tone(780, 0.12, 0.035)

    def throw_repair(self):
        if self.state != "playing" or self.repair_cooldown > 0:
            return
        self.repair_cooldown = 0.34
        candidates = [h for h in self.houses if not h.repaired and not h.locked and 240 < h.x < 860]
        target = min(candidates, key=lambda h: h.x) if candidates else None
        if target:
            target.locked = True
            target_x = target.x + target.width * 0.5
            target_y = 147.0
        else:
            target_x = 760.0
            target_y = 175.0
        self.play_tone(330, 0.05, 0.022)
        self.projectiles.append(
            Projectile(
                x=PLAYER_X + 48,
                y=self.player_y - 20,
                repair_index=self.selected_repair,
                target=target,
                target_x=target_x,
                target_y=target_y,
            )
        )

    def update_projectile(self, projectile, dt):
        if not projectile.alive:
            return
        if projectile.target and projectile.target in self.houses:
            projectile.target_x = projectile.target.x + projectile.target.width * 0.5
            projectile.target_y = 147.0
        dx = projectile.target_x - projectile.x
        dy = projectile.target_y - projectile.y
        distance = math.hypot(dx, dy)
        if distance <= projectile.speed * dt or distance < 8:
            projectile.x = projectile.target_x
            projectile.y = projectile.target_y
            projectile.alive = False
            if projectile.target and projectile.target in self.houses:
                self.resolve_repair(projectile.target, projectile.repair_index)
            else:
                self.add_popup("AIR MAIL", projectile.x, projectile.y, COLORS["muted"], 18)
            return
        projectile.x += dx / distance * projectile.speed * dt
        projectile.y += dy / distance * projectile.speed * dt
        projectile.angle += dt * 9.0

    def resolve_repair(self, house, repair_index):
        house.locked = False
        correct = repair_index == house.repair_index or self.permit_time > 0
        if correct:
            house.repaired = True
            house.flash = 0.75
            self.combo += 1
            self.best_combo = max(self.best_combo, self.combo)
            multiplier = 2 if self.boost_time > 0 else 1
            combo_bonus = min(250, max(0, self.combo - 1) * 25)
            points = (100 + combo_bonus) * multiplier
            self.score += points
            label = f"+{points}  REPAIRED"
            if self.combo >= 3:
                label = f"+{points}  {self.combo}× COMBO"
            self.add_popup(label, house.x + house.width / 2, 150, COLORS["leaf"], 22)
            self.burst(house.x + house.width / 2, 190, REPAIRS[house.repair_index]["color"], 28)
            self.play_tone(620 + min(260, self.combo * 24), 0.09, 0.032)
        else:
            house.gag = 1.25
            house.flash = 0.55
            self.combo = 0
            self.score = max(0, self.score - 50)
            self.add_popup(random.choice(WRONG_LINES), house.x + house.width / 2, 145, COLORS["red"], 19)
            self.burst(house.x + house.width / 2, 190, REPAIRS[repair_index]["color"], 20)
            self.play_tone(165, 0.16, 0.045)

    def cycle_repair(self, direction):
        self.selected_repair = (self.selected_repair + direction) % len(REPAIRS)
        self.play_tone(440 + self.selected_repair * 35, 0.035, 0.014)

    def add_popup(self, text, x, y, color, size=24):
        self.popups.append(Popup(text, x, y, 1.15, color, size))

    def burst(self, x, y, color, count):
        for _ in range(count):
            angle = random.uniform(-math.pi, 0)
            speed = random.uniform(90, 260)
            self.particles.append(
                Particle(
                    x=x,
                    y=y,
                    vx=math.cos(angle) * speed,
                    vy=math.sin(angle) * speed,
                    life=random.uniform(0.45, 0.9),
                    color=color,
                    size=random.uniform(3.0, 8.0),
                )
            )

    def finish_game(self):
        self.state = "game_over"
        self.touch_up = False
        self.touch_down = False
        self.pause_music()

    def rank(self):
        result = RANKS[0][1]
        for threshold, name in RANKS:
            if self.score >= threshold:
                result = name
        return result

    def draw(self):
        self.ctx.clearRect(0, 0, WIDTH, HEIGHT)
        self.draw_world()
        if self.state == "intro":
            self.draw_intro()
        elif self.state == "playing":
            self.draw_hud()
            self.draw_controls()
        elif self.state == "paused":
            self.draw_hud()
            self.draw_controls()
            self.draw_overlay("PAUSED", "Press P or tap Continue", "CONTINUE")
        elif self.state == "game_over":
            self.draw_game_over()

    def draw_world(self):
        ctx = self.ctx
        ctx.fillStyle = COLORS["sky"]
        ctx.fillRect(0, 0, WIDTH, ROAD_TOP)

        self.draw_cloud(120 - (self.road_offset * 0.12), 72, 1.0)
        self.draw_cloud(510 - (self.road_offset * 0.07), 92, 0.78)
        self.draw_cloud(820 - (self.road_offset * 0.1), 55, 0.92)

        ctx.fillStyle = COLORS["grass"]
        ctx.fillRect(0, 246, WIDTH, ROAD_TOP - 246)

        fence_offset = (self.road_offset * 0.55) % 82
        for x in range(-100, WIDTH + 100, 82):
            fx = x - fence_offset
            ctx.fillStyle = "#e7d2a9"
            ctx.fillRect(fx, 267, 8, 50)
            ctx.fillRect(fx - 2, 264, 12, 7)
            ctx.fillRect(fx - 24, 280, 58, 7)
            ctx.fillRect(fx - 24, 301, 58, 7)

        for house in self.houses:
            self.draw_house(house)

        ctx.fillStyle = COLORS["road_dark"]
        ctx.fillRect(0, ROAD_TOP, WIDTH, ROAD_BOTTOM - ROAD_TOP)
        ctx.fillStyle = COLORS["road"]
        ctx.fillRect(0, ROAD_TOP + 8, WIDTH, ROAD_BOTTOM - ROAD_TOP - 16)

        for x in range(-100, WIDTH + 120, 92):
            line_x = x - self.road_offset
            ctx.fillStyle = COLORS["line"]
            rounded_rect(ctx, line_x, 411, 52, 8, 4, True, False)

        ctx.fillStyle = "#ded8c8"
        ctx.fillRect(0, ROAD_TOP, WIDTH, 8)
        ctx.fillRect(0, ROAD_BOTTOM - 8, WIDTH, 8)

        for powerup in self.powerups:
            self.draw_powerup(powerup)
        for obstacle in self.obstacles:
            self.draw_obstacle(obstacle)
        for projectile in self.projectiles:
            self.draw_projectile(projectile)
        self.draw_player()
        for particle in self.particles:
            self.ctx.globalAlpha = clamp(particle.life * 1.8, 0, 1)
            self.ctx.fillStyle = particle.color
            self.ctx.fillRect(particle.x, particle.y, particle.size, particle.size)
        self.ctx.globalAlpha = 1.0
        for popup in self.popups:
            self.ctx.globalAlpha = clamp(popup.life * 1.5, 0, 1)
            self.draw_text(popup.text, popup.x, popup.y, popup.size, popup.color, "center", 800)
        self.ctx.globalAlpha = 1.0

    def draw_cloud(self, x, y, scale):
        ctx = self.ctx
        ctx.fillStyle = "rgba(255,255,255,0.78)"
        ctx.beginPath()
        ctx.arc(x, y, 25 * scale, 0, math.pi * 2)
        ctx.arc(x + 28 * scale, y - 10 * scale, 31 * scale, 0, math.pi * 2)
        ctx.arc(x + 63 * scale, y, 24 * scale, 0, math.pi * 2)
        ctx.fill()

    def draw_house(self, house):
        ctx = self.ctx
        x = house.x
        y = 142
        palettes = [
            ("#f5d7a1", "#a94f42", "#fff8e9"),
            ("#d7e9cf", "#4e7855", "#f9f5e8"),
            ("#d7e6ed", "#4a7081", "#fbf8ed"),
            ("#eed0c0", "#8c5145", "#fff7ec"),
            ("#e4d9ef", "#6d5b82", "#fff8ee"),
        ]
        body, roof, trim = palettes[house.style % len(palettes)]

        if house.flash > 0:
            ctx.globalAlpha = 0.72 + math.sin(house.flash * 28) * 0.18

        ctx.fillStyle = "rgba(8,51,31,0.14)"
        rounded_rect(ctx, x + 10, y + 92, house.width, 82, 12, True, False)

        ctx.fillStyle = body
        rounded_rect(ctx, x, y + 42, house.width, 118, 5, True, False)

        ctx.fillStyle = roof
        ctx.beginPath()
        ctx.moveTo(x - 12, y + 49)
        ctx.lineTo(x + house.width / 2, y - 4)
        ctx.lineTo(x + house.width + 12, y + 49)
        ctx.closePath()
        ctx.fill()

        ctx.fillStyle = trim
        ctx.fillRect(x + 19, y + 78, 38, 38)
        ctx.fillRect(x + house.width - 57, y + 78, 38, 38)
        ctx.fillStyle = "#76a6b5"
        ctx.fillRect(x + 24, y + 83, 28, 28)
        ctx.fillRect(x + house.width - 52, y + 83, 28, 28)
        ctx.strokeStyle = trim
        ctx.lineWidth = 3
        ctx.beginPath()
        ctx.moveTo(x + 38, y + 83)
        ctx.lineTo(x + 38, y + 111)
        ctx.moveTo(x + house.width - 38, y + 83)
        ctx.lineTo(x + house.width - 38, y + 111)
        ctx.stroke()

        ctx.fillStyle = "#7a563e"
        rounded_rect(ctx, x + house.width / 2 - 17, y + 103, 34, 57, 4, True, False)
        ctx.fillStyle = "#f0ca4d"
        ctx.beginPath()
        ctx.arc(x + house.width / 2 + 10, y + 133, 2.8, 0, math.pi * 2)
        ctx.fill()

        if house.repaired:
            ctx.fillStyle = "#ffffff"
            ctx.beginPath()
            ctx.arc(x + house.width / 2, y + 5, 27, 0, math.pi * 2)
            ctx.fill()
            self.draw_text("✓", x + house.width / 2, y + 15, 31, COLORS["leaf"], "center", 900)
            self.draw_text("FIXED", x + house.width / 2, y + 37, 12, COLORS["green"], "center", 900)
        else:
            repair = REPAIRS[house.repair_index]
            ctx.fillStyle = COLORS["cream"]
            ctx.strokeStyle = repair["color"]
            ctx.lineWidth = 5
            ctx.beginPath()
            ctx.arc(x + house.width / 2, y + 5, 29, 0, math.pi * 2)
            ctx.fill()
            ctx.stroke()
            self.draw_text(repair["symbol"], x + house.width / 2, y + 13, 26, repair["color"], "center", 900)
            self.draw_text(repair["short"], x + house.width / 2, y + 38, 11, COLORS["green"], "center", 900)

        if house.gag > 0:
            selected = REPAIRS[self.selected_repair]
            ctx.fillStyle = selected["color"]
            ctx.globalAlpha = 0.78
            ctx.beginPath()
            ctx.arc(x + 34, y + 70, 18, 0, math.pi * 2)
            ctx.arc(x + 78, y + 91, 25, 0, math.pi * 2)
            ctx.arc(x + 133, y + 67, 21, 0, math.pi * 2)
            ctx.fill()
            ctx.globalAlpha = 1.0

        ctx.globalAlpha = 1.0

    def draw_player(self):
        ctx = self.ctx
        x = PLAYER_X
        y = self.player_y
        blink = self.invulnerable > 0 and int(self.invulnerable * 12) % 2 == 0
        if blink:
            ctx.globalAlpha = 0.35

        ctx.fillStyle = "rgba(8,51,31,0.22)"
        ctx.beginPath()
        ctx.ellipse(x, y + 31, 67, 14, 0, 0, math.pi * 2)
        ctx.fill()

        if self.shield_time > 0:
            ctx.strokeStyle = COLORS["leaf"]
            ctx.lineWidth = 6
            ctx.globalAlpha = 0.55 + math.sin(self.elapsed * 8) * 0.15
            ctx.beginPath()
            ctx.arc(x, y, 78, 0, math.pi * 2)
            ctx.stroke()
            ctx.globalAlpha = 1.0

        ctx.fillStyle = COLORS["green"]
        rounded_rect(ctx, x - 61, y - 31, 124, 55, 11, True, False)
        ctx.fillStyle = COLORS["leaf"]
        rounded_rect(ctx, x - 34, y - 54, 75, 31, 8, True, False)
        ctx.fillStyle = "#d9f2f4"
        rounded_rect(ctx, x - 24, y - 48, 27, 20, 4, True, False)
        rounded_rect(ctx, x + 8, y - 48, 25, 20, 4, True, False)
        ctx.fillStyle = COLORS["cream"]
        ctx.fillRect(x - 58, y - 15, 54, 23)
        self.draw_text("FRESHEST.COM", x - 31, y, 7, COLORS["green"], "center", 900)

        for wheel_x in [x - 38, x + 39]:
            ctx.save()
            ctx.translate(wheel_x, y + 23)
            ctx.rotate(self.wheel_angle)
            ctx.fillStyle = COLORS["dark"]
            ctx.beginPath()
            ctx.arc(0, 0, 15, 0, math.pi * 2)
            ctx.fill()
            ctx.strokeStyle = "#b8c2bc"
            ctx.lineWidth = 3
            ctx.beginPath()
            ctx.moveTo(-9, 0)
            ctx.lineTo(9, 0)
            ctx.moveTo(0, -9)
            ctx.lineTo(0, 9)
            ctx.stroke()
            ctx.restore()

        if self.stumble > 0:
            self.draw_text("!", x + 76, y - 28, 35, COLORS["red"], "center", 900)

        ctx.globalAlpha = 1.0

    def draw_obstacle(self, obstacle):
        if obstacle.hit:
            self.ctx.globalAlpha = 0.42
        x = obstacle.x
        y = obstacle.y + math.sin(obstacle.bob) * 2
        ctx = self.ctx
        kind = obstacle.kind

        if kind == "gnome":
            ctx.fillStyle = COLORS["red"]
            ctx.beginPath()
            ctx.moveTo(x, y - 29)
            ctx.lineTo(x - 15, y - 3)
            ctx.lineTo(x + 15, y - 3)
            ctx.closePath()
            ctx.fill()
            ctx.fillStyle = "#f4d1b4"
            ctx.beginPath()
            ctx.arc(x, y + 4, 11, 0, math.pi * 2)
            ctx.fill()
            ctx.fillStyle = COLORS["blue"]
            ctx.fillRect(x - 13, y + 13, 26, 20)
        elif kind == "trash":
            ctx.fillStyle = "#687276"
            rounded_rect(ctx, x - 18, y - 22, 36, 46, 5, True, False)
            ctx.fillStyle = "#3e4548"
            ctx.fillRect(x - 22, y - 27, 44, 7)
            ctx.fillRect(x - 5, y - 34, 10, 7)
        elif kind == "goose":
            ctx.fillStyle = COLORS["white"]
            ctx.beginPath()
            ctx.ellipse(x, y + 7, 24, 15, 0, 0, math.pi * 2)
            ctx.fill()
            ctx.strokeStyle = COLORS["white"]
            ctx.lineWidth = 10
            ctx.beginPath()
            ctx.moveTo(x + 15, y)
            ctx.quadraticCurveTo(x + 18, y - 25, x + 33, y - 23)
            ctx.stroke()
            ctx.fillStyle = COLORS["orange"]
            ctx.beginPath()
            ctx.moveTo(x + 38, y - 24)
            ctx.lineTo(x + 50, y - 20)
            ctx.lineTo(x + 38, y - 16)
            ctx.closePath()
            ctx.fill()
        elif kind == "sprinkler":
            ctx.fillStyle = "#385f4d"
            ctx.fillRect(x - 5, y - 2, 10, 25)
            ctx.fillRect(x - 18, y - 7, 36, 8)
            ctx.strokeStyle = COLORS["blue"]
            ctx.lineWidth = 4
            ctx.beginPath()
            ctx.arc(x - 17, y - 5, 28, math.pi, math.pi * 1.55)
            ctx.arc(x + 17, y - 5, 28, math.pi * 1.45, math.pi * 2)
            ctx.stroke()
        elif kind == "lumber":
            ctx.fillStyle = COLORS["wood"]
            for i in range(3):
                rounded_rect(ctx, x - 31 + i * 5, y - 15 + i * 10, 62, 9, 3, True, False)
        else:
            ctx.fillStyle = COLORS["wood"]
            ctx.beginPath()
            ctx.moveTo(x - 28, y - 22)
            ctx.lineTo(x + 22, y - 22)
            ctx.lineTo(x + 12, y + 11)
            ctx.lineTo(x - 18, y + 11)
            ctx.closePath()
            ctx.fill()
            ctx.strokeStyle = COLORS["dark"]
            ctx.lineWidth = 5
            ctx.beginPath()
            ctx.moveTo(x + 19, y - 20)
            ctx.lineTo(x + 36, y - 34)
            ctx.stroke()
            ctx.fillStyle = COLORS["dark"]
            ctx.beginPath()
            ctx.arc(x - 13, y + 18, 9, 0, math.pi * 2)
            ctx.fill()

        self.ctx.globalAlpha = 1.0

    def draw_powerup(self, powerup):
        x = powerup.x
        y = powerup.y + math.sin(powerup.bob) * 6
        if powerup.kind == "permit":
            color = COLORS["blue"]
            symbol = "P"
        elif powerup.kind == "boost":
            color = COLORS["orange"]
            symbol = "×2"
        else:
            color = COLORS["leaf"]
            symbol = "S"
        self.ctx.fillStyle = "rgba(255,255,255,0.88)"
        self.ctx.strokeStyle = color
        self.ctx.lineWidth = 5
        self.ctx.beginPath()
        self.ctx.arc(x, y, 24, 0, math.pi * 2)
        self.ctx.fill()
        self.ctx.stroke()
        self.draw_text(symbol, x, y + 7, 19, color, "center", 900)

    def draw_projectile(self, projectile):
        repair = REPAIRS[projectile.repair_index]
        ctx = self.ctx
        ctx.save()
        ctx.translate(projectile.x, projectile.y)
        ctx.rotate(projectile.angle)
        ctx.fillStyle = repair["color"]
        rounded_rect(ctx, -14, -11, 28, 22, 5, True, False)
        self.draw_text(repair["symbol"], 0, 7, 17, COLORS["white"], "center", 900)
        ctx.restore()

    def draw_hud(self):
        ctx = self.ctx
        ctx.fillStyle = "rgba(247,243,232,0.94)"
        rounded_rect(ctx, 18, 16, 460, 68, 18, True, False)
        self.draw_text("SCORE", 40, 39, 12, COLORS["muted"], "left", 900)
        self.draw_text(f"{self.score:,}", 40, 68, 27, COLORS["green"], "left", 900)
        self.draw_text("TIME", 190, 39, 12, COLORS["muted"], "left", 900)
        self.draw_text(f"{math.ceil(self.time_left):02d}", 190, 68, 27, COLORS["green"], "left", 900)
        self.draw_text("COMBO", 288, 39, 12, COLORS["muted"], "left", 900)
        self.draw_text(f"{self.combo}×", 288, 68, 27, COLORS["green"], "left", 900)
        self.draw_text("VAN", 390, 39, 12, COLORS["muted"], "left", 900)
        for index in range(3):
            color = COLORS["red"] if index < self.health else "#d8d4c9"
            self.draw_heart(400 + index * 24, 61, 10, color)

        effects = []
        if self.permit_time > 0:
            effects.append(("PERMIT", self.permit_time, COLORS["blue"]))
        if self.boost_time > 0:
            effects.append(("×2", self.boost_time, COLORS["orange"]))
        if self.shield_time > 0:
            effects.append(("SAFE", self.shield_time, COLORS["leaf"]))
        x = 497
        for name, duration, color in effects:
            ctx.fillStyle = "rgba(247,243,232,0.94)"
            rounded_rect(ctx, x, 23, 82, 34, 14, True, False)
            self.draw_text(f"{name} {math.ceil(duration)}", x + 41, 45, 12, color, "center", 900)
            x += 89

        self.draw_icon_button(858, 22, 38, "Ⅱ", self.state == "paused")
        self.draw_icon_button(906, 22, 38, "×" if self.muted else "♪", self.muted)

    def draw_controls(self):
        ctx = self.ctx
        self.draw_control_button(18, 444, 66, 66, "▲", "UP", self.touch_up)
        self.draw_control_button(91, 444, 66, 66, "▼", "DOWN", self.touch_down)

        repair = REPAIRS[self.selected_repair]
        ctx.fillStyle = "rgba(247,243,232,0.96)"
        rounded_rect(ctx, 330, 447, 300, 60, 18, True, False)
        self.draw_icon_button(338, 458, 42, "‹", False)
        ctx.fillStyle = repair["color"]
        rounded_rect(ctx, 390, 456, 180, 42, 13, True, False)
        self.draw_text(repair["symbol"], 411, 484, 22, COLORS["white"], "center", 900)
        self.draw_text(repair["name"].upper(), 482, 483, 15, COLORS["white"], "center", 900)
        self.draw_icon_button(580, 458, 42, "›", False)

        pressed = self.repair_cooldown > 0.22
        ctx.fillStyle = "#0d7f49" if pressed else COLORS["leaf"]
        rounded_rect(ctx, 733, 438, 207, 72, 22, True, False)
        self.draw_text("REPAIR", 836, 474, 25, COLORS["white"], "center", 900)
        self.draw_text("SPACE", 836, 496, 11, "rgba(255,255,255,0.82)", "center", 900)

    def draw_control_button(self, x, y, w, h, symbol, label, active):
        self.ctx.fillStyle = "#d9ead8" if active else "rgba(247,243,232,0.96)"
        rounded_rect(self.ctx, x, y, w, h, 18, True, False)
        self.draw_text(symbol, x + w / 2, y + 29, 21, COLORS["green"], "center", 900)
        self.draw_text(label, x + w / 2, y + 51, 10, COLORS["muted"], "center", 900)

    def draw_icon_button(self, x, y, size, label, active):
        self.ctx.fillStyle = "#d9ead8" if active else "rgba(247,243,232,0.96)"
        rounded_rect(self.ctx, x, y, size, size, 12, True, False)
        self.draw_text(label, x + size / 2, y + size * 0.68, int(size * 0.48), COLORS["green"], "center", 900)

    def draw_intro(self):
        ctx = self.ctx
        ctx.fillStyle = "rgba(8,51,31,0.76)"
        ctx.fillRect(0, 0, WIDTH, HEIGHT)
        ctx.fillStyle = COLORS["cream"]
        rounded_rect(ctx, 118, 48, 724, 438, 28, True, False)

        self.draw_text("FRESHEST.COM PRESENTS", WIDTH / 2, 82, 13, COLORS["leaf"], "center", 900)
        self.draw_text("RENOVATION RUSH", WIDTH / 2, 122, 43, COLORS["green"], "center", 900)
        self.draw_text("Fix the block then plan your real project with Freshest.com.", WIDTH / 2, 154, 18, COLORS["muted"], "center", 600)

        repair_y = 205
        icon_spacing = 112
        icon_start = WIDTH / 2 - ((len(REPAIRS) - 1) * icon_spacing) / 2
        for index, repair in enumerate(REPAIRS):
            x = icon_start + index * icon_spacing
            ctx.fillStyle = repair["color"]
            ctx.beginPath()
            ctx.arc(x, repair_y, 27, 0, math.pi * 2)
            ctx.fill()
            self.draw_text(repair["symbol"], x, repair_y + 8, 22, COLORS["white"], "center", 900)
            self.draw_text(str(index + 1), x, repair_y + 49, 11, COLORS["muted"], "center", 900)

        self.draw_text("STEER", 245, 285, 13, COLORS["muted"], "center", 900)
        self.draw_text("↑  ↓  or  W  S", 245, 316, 21, COLORS["green"], "center", 800)
        self.draw_text("CHOOSE SERVICE", 480, 285, 13, COLORS["muted"], "center", 900)
        self.draw_text("1–4  or  Q  E", 480, 316, 21, COLORS["green"], "center", 800)
        self.draw_text("THROW REPAIR", 715, 285, 13, COLORS["muted"], "center", 900)
        self.draw_text("SPACE", 715, 316, 21, COLORS["green"], "center", 800)

        ctx.fillStyle = COLORS["leaf"]
        rounded_rect(ctx, 309, 350, 342, 72, 22, True, False)
        self.draw_text("START THE FRESHEST RUSH", WIDTH / 2, 394, 21, COLORS["white"], "center", 900)
        self.draw_text("Music, keyboard, and touch controls included", WIDTH / 2, 456, 14, COLORS["muted"], "center", 600)

    def draw_overlay(self, title, subtitle, button):
        ctx = self.ctx
        ctx.fillStyle = "rgba(8,51,31,0.72)"
        ctx.fillRect(0, 0, WIDTH, HEIGHT)
        ctx.fillStyle = COLORS["cream"]
        rounded_rect(ctx, 272, 151, 416, 238, 26, True, False)
        self.draw_text(title, WIDTH / 2, 225, 42, COLORS["green"], "center", 900)
        self.draw_text(subtitle, WIDTH / 2, 264, 17, COLORS["muted"], "center", 600)
        ctx.fillStyle = COLORS["leaf"]
        rounded_rect(ctx, 347, 300, 266, 62, 18, True, False)
        self.draw_text(button, WIDTH / 2, 339, 21, COLORS["white"], "center", 900)

    def draw_game_over(self):
        ctx = self.ctx
        ctx.fillStyle = "rgba(8,51,31,0.78)"
        ctx.fillRect(0, 0, WIDTH, HEIGHT)
        ctx.fillStyle = COLORS["cream"]
        rounded_rect(ctx, 185, 38, 590, 464, 28, True, False)
        title = "SHIFT COMPLETE" if self.time_left <= 0 else "VAN DOWN"
        self.draw_text(title, WIDTH / 2, 94, 35, COLORS["green"], "center", 900)
        self.draw_text(self.rank(), WIDTH / 2, 130, 20, COLORS["leaf"], "center", 800)

        self.draw_text("FINAL SCORE", 350, 181, 13, COLORS["muted"], "center", 900)
        self.draw_text(f"{self.score:,}", 350, 226, 38, COLORS["green"], "center", 900)
        self.draw_text("BEST COMBO", 610, 181, 13, COLORS["muted"], "center", 900)
        self.draw_text(f"{self.best_combo}×", 610, 226, 38, COLORS["green"], "center", 900)

        ctx.fillStyle = "#e2ecd9"
        rounded_rect(ctx, 263, 258, 434, 58, 16, True, False)
        self.draw_text("Your real home deserves a Freshest.com plan.", WIDTH / 2, 293, 16, COLORS["green"], "center", 700)

        ctx.fillStyle = COLORS["leaf"]
        rounded_rect(ctx, 270, 343, 420, 64, 19, True, False)
        self.draw_text("PLAY AGAIN", WIDTH / 2, 383, 22, COLORS["white"], "center", 900)

        ctx.fillStyle = COLORS["green"]
        rounded_rect(ctx, 270, 418, 420, 52, 17, True, False)
        self.draw_text("PLAN A REAL PROJECT", WIDTH / 2, 451, 17, COLORS["white"], "center", 900)

    def draw_heart(self, x, y, size, color):
        ctx = self.ctx
        ctx.fillStyle = color
        ctx.beginPath()
        ctx.moveTo(x, y + size * 0.35)
        ctx.bezierCurveTo(x - size * 1.1, y - size * 0.45, x - size * 1.4, y + size * 0.85, x, y + size * 1.45)
        ctx.bezierCurveTo(x + size * 1.4, y + size * 0.85, x + size * 1.1, y - size * 0.45, x, y + size * 0.35)
        ctx.fill()

    def draw_text(self, text, x, y, size, color, align="left", weight=700):
        self.ctx.font = f"{weight} {size}px Inter, Arial, sans-serif"
        self.ctx.textAlign = align
        self.ctx.fillStyle = color
        self.ctx.fillText(text, x, y)

    def pointer_position(self, event):
        rect = self.canvas.getBoundingClientRect()
        x = (event.clientX - rect.left) * WIDTH / rect.width
        y = (event.clientY - rect.top) * HEIGHT / rect.height
        return x, y

    def handle_pointer_down(self, event):
        event.preventDefault()
        self.canvas.focus()
        self.pointer_id = event.pointerId
        try:
            self.canvas.setPointerCapture(event.pointerId)
        except Exception:
            pass
        x, y = self.pointer_position(event)

        if self.state == "intro":
            if point_in_rect(x, y, 309, 348, 342, 72):
                self.reset()
            return
        if self.state == "paused":
            if point_in_rect(x, y, 347, 300, 266, 62):
                self.resume_game()
            return
        if self.state == "game_over":
            if point_in_rect(x, y, 270, 343, 420, 64):
                self.reset()
            elif point_in_rect(x, y, 270, 418, 420, 52):
                window.parent.location.href = "/projects/new/"
            return
        if self.state != "playing":
            return

        if point_in_rect(x, y, 858, 22, 38, 38):
            self.pause_game()
        elif point_in_rect(x, y, 906, 22, 38, 38):
            self.toggle_mute()
        elif point_in_rect(x, y, 18, 444, 66, 66):
            self.touch_up = True
        elif point_in_rect(x, y, 91, 444, 66, 66):
            self.touch_down = True
        elif point_in_rect(x, y, 338, 458, 42, 42):
            self.cycle_repair(-1)
        elif point_in_rect(x, y, 580, 458, 42, 42):
            self.cycle_repair(1)
        elif point_in_rect(x, y, 733, 438, 207, 72):
            self.throw_repair()

    def handle_pointer_up(self, event):
        event.preventDefault()
        self.touch_up = False
        self.touch_down = False
        self.pointer_id = None

    def handle_key_down(self, event):
        key = event.key
        if key in ["ArrowUp", "ArrowDown", " ", "Spacebar"]:
            event.preventDefault()
        self.keys.add(key)

        if self.state == "intro" and key in ["Enter", " ", "Spacebar"]:
            self.reset()
            return
        if self.state == "game_over" and key in ["Enter", " ", "Spacebar"]:
            self.reset()
            return
        if key in ["p", "P", "Escape"]:
            if self.state == "playing":
                self.pause_game()
            elif self.state == "paused":
                self.resume_game()
            return
        if key in ["m", "M"]:
            self.toggle_mute()
            return
        if self.state != "playing":
            return
        if key in [" ", "Spacebar"]:
            self.throw_repair()
        elif key in ["q", "Q", "ArrowLeft"]:
            self.cycle_repair(-1)
        elif key in ["e", "E", "ArrowRight"]:
            self.cycle_repair(1)
        elif key.isdigit() and 1 <= int(key) <= len(REPAIRS):
            self.selected_repair = int(key) - 1

    def handle_key_up(self, event):
        self.keys.discard(event.key)

    def handle_blur(self, event):
        self.keys.clear()
        self.touch_up = False
        self.touch_down = False
        if self.state == "playing":
            self.pause_game()


def clamp(value, minimum, maximum):
    return max(minimum, min(maximum, value))


def rects_overlap(ax, ay, aw, ah, bx, by, bw, bh):
    return ax < bx + bw and ax + aw > bx and ay < by + bh and ay + ah > by


def point_in_rect(x, y, rx, ry, rw, rh):
    return rx <= x <= rx + rw and ry <= y <= ry + rh


def rounded_rect(ctx, x, y, width, height, radius, fill=True, stroke=False):
    radius = min(radius, width / 2, height / 2)
    ctx.beginPath()
    ctx.moveTo(x + radius, y)
    ctx.arcTo(x + width, y, x + width, y + height, radius)
    ctx.arcTo(x + width, y + height, x, y + height, radius)
    ctx.arcTo(x, y + height, x, y, radius)
    ctx.arcTo(x, y, x + width, y, radius)
    ctx.closePath()
    if fill:
        ctx.fill()
    if stroke:
        ctx.stroke()


canvas = document.getElementById("game-canvas")
ctx = canvas.getContext("2d")
game = Game(canvas, ctx)
loading = document.getElementById("game-loading")
loading.classList.add("is-hidden")


def on_pointer_down(event):
    game.handle_pointer_down(event)


def on_pointer_up(event):
    game.handle_pointer_up(event)


def on_key_down(event):
    game.handle_key_down(event)


def on_key_up(event):
    game.handle_key_up(event)


def on_blur(event):
    game.handle_blur(event)


def frame(timestamp):
    if game.last_time == 0:
        game.last_time = timestamp
    dt = min(0.034, max(0.0, (timestamp - game.last_time) / 1000.0))
    game.last_time = timestamp
    game.update(dt)
    game.draw()
    window.requestAnimationFrame(frame_proxy)


pointer_down_proxy = create_proxy(on_pointer_down)
pointer_up_proxy = create_proxy(on_pointer_up)
key_down_proxy = create_proxy(on_key_down)
key_up_proxy = create_proxy(on_key_up)
blur_proxy = create_proxy(on_blur)
context_menu_proxy = create_proxy(lambda event: event.preventDefault())
frame_proxy = create_proxy(frame)

canvas.addEventListener("pointerdown", pointer_down_proxy)
canvas.addEventListener("pointerup", pointer_up_proxy)
canvas.addEventListener("pointercancel", pointer_up_proxy)
canvas.addEventListener("contextmenu", context_menu_proxy)
document.addEventListener("keydown", key_down_proxy)
document.addEventListener("keyup", key_up_proxy)
window.addEventListener("blur", blur_proxy)
window.requestAnimationFrame(frame_proxy)