import pyxel

# Screen
SCR_W, SCR_H = 160, 120

# Physics
GRAVITY = 0.35
JUMP_VEL = -4.5
MAX_FALL = 3.5
WALK_SPEED = 1.8
COYOTE_FRAMES = 3
JUMP_BUFFER_FRAMES = 4

# Tiles
TILE = 8
MAP_W = 80  # tiles
MAP_H = 15

# Scene
SCENE_TITLE, SCENE_GAME, SCENE_GAMEOVER = 0, 1, 2


class App:
    def __init__(self):
        pyxel.init(SCR_W, SCR_H, title="Pyxel Mario", fps=30)
        self._init_sprites()
        self._init_map()
        self._init_sounds()
        self._init_bgm()
        self.scene = SCENE_TITLE
        self.high_score = 0
        self._reset()
        pyxel.run(self.update, self.draw)

    def _reset(self):
        self.px = 16.0
        self.py = 80.0
        self.vx = 0.0
        self.vy = 0.0
        self.direction = 1
        self.on_ground = False
        self.coyote = 0
        self.jump_buffer = 0
        self.score = 0
        self.lives = 3
        self.camera_x = 0
        self.coins = []
        self.enemies = []
        self.particles = []
        self.hit_timer = 0
        self.shake_dur = 0
        self.shake_mag = 0
        self._spawn_coins()
        self._spawn_enemies()

    # --- Sprites ---

    def _init_sprites(self):
        # Player: idle at (0,0), walk at (8,0), jump at (16,0)
        pyxel.images[0].set(0, 0, [
            "00088000",
            "00888800",
            "004f4f00",
            "00ffff00",
            "08899880",
            "00899800",
            "00044000",
            "00400400",
        ])
        pyxel.images[0].set(8, 0, [
            "00088000",
            "00888800",
            "004f4f00",
            "00ffff00",
            "08899880",
            "00899800",
            "00040400",
            "00040040",
        ])
        pyxel.images[0].set(16, 0, [
            "00088000",
            "00888800",
            "004f4f00",
            "00ffff00",
            "08899880",
            "00899800",
            "00440000",
            "00004400",
        ])
        # Coin at (0,8) - 2 frames
        pyxel.images[0].set(0, 8, [
            "000aa000",
            "00a00a00",
            "0a0aa0a0",
            "0a0aa0a0",
            "0a0aa0a0",
            "0a0aa0a0",
            "00a00a00",
            "000aa000",
        ])
        pyxel.images[0].set(8, 8, [
            "000aa000",
            "00aaaa00",
            "0aa00aa0",
            "0aa00aa0",
            "0aa00aa0",
            "0aa00aa0",
            "00aaaa00",
            "000aa000",
        ])
        # Enemy (goomba) at (0,16) - 2 frames
        pyxel.images[0].set(0, 16, [
            "00044000",
            "00444400",
            "04f44f40",
            "04444440",
            "44988944",
            "04499440",
            "00400400",
            "04000040",
        ])
        pyxel.images[0].set(8, 16, [
            "00044000",
            "00444400",
            "04f44f40",
            "04444440",
            "44988944",
            "04499440",
            "00040400",
            "00400040",
        ])
        # Cloud at (0,24) - 16x8
        pyxel.images[0].set(0, 24, [
            "0000066000006600",
            "0006666600666660",
            "0666666666666660",
            "6666666666666666",
            "6666666666666666",
            "0666666666666660",
            "0066666666666600",
            "0000666666660000",
        ])
        # Bush at (16, 24) - 16x8
        pyxel.images[0].set(16, 24, [
            "0000033000003300",
            "0003b33300b3b330",
            "03b3b3b3b3b3b330",
            "3b3b3b3b3b3b3b33",
            "3b3b3b3b3b3b3b33",
            "03b3b3b3b3b3b330",
            "003b3b3b3b3b3300",
            "0000333333330000",
        ])

    # --- Map ---

    def _init_map(self):
        self.tilemap = []
        for y in range(MAP_H):
            row = []
            for x in range(MAP_W):
                if y >= 13:
                    row.append(1)  # ground
                elif y == 12 and x >= 2:
                    row.append(1)  # ground surface
                else:
                    row.append(0)  # air
            self.tilemap.append(row)

        # Platforms
        platforms = [
            (10, 8, 4), (18, 6, 3), (25, 9, 5), (35, 7, 3),
            (42, 5, 4), (50, 8, 3), (58, 6, 4), (66, 9, 3),
        ]
        for px, py, pw in platforms:
            for dx in range(pw):
                if 0 <= px + dx < MAP_W and 0 <= py < MAP_H:
                    self.tilemap[py][px + dx] = 2  # platform

        # Gaps in ground
        for gx in [20, 21, 45, 46, 47, 65, 66]:
            if gx < MAP_W:
                self.tilemap[12][gx] = 0
                self.tilemap[13][gx] = 0
                self.tilemap[14][gx] = 0

        # Pipes
        pipes = [(15, 10), (38, 9), (55, 10), (72, 9)]
        for px_pos, py_pos in pipes:
            for dy in range(py_pos, 12):
                if 0 <= px_pos < MAP_W and 0 <= dy < MAP_H:
                    self.tilemap[dy][px_pos] = 3
                    if px_pos + 1 < MAP_W:
                        self.tilemap[dy][px_pos + 1] = 3

    def _tile_at(self, x, y):
        tx = int(x) // TILE
        ty = int(y) // TILE
        if tx < 0 or tx >= MAP_W or ty < 0 or ty >= MAP_H:
            return 0
        return self.tilemap[ty][tx]

    def _is_solid(self, x, y):
        return self._tile_at(x, y) != 0

    # --- Entities ---

    def _spawn_coins(self):
        coin_positions = [
            (12, 7), (13, 7), (14, 7),
            (19, 5), (20, 5),
            (27, 8), (28, 8),
            (36, 6), (37, 6),
            (43, 4), (44, 4), (45, 4),
            (51, 7), (52, 7),
            (59, 5), (60, 5), (61, 5),
            (67, 8), (68, 8),
        ]
        self.coins = [{"x": cx * TILE + 1, "y": cy * TILE, "alive": True}
                      for cx, cy in coin_positions]

    def _spawn_enemies(self):
        enemy_positions = [
            (22, 11), (30, 11), (40, 11), (48, 11),
            (53, 11), (60, 11), (70, 11),
        ]
        self.enemies = [{"x": float(ex * TILE), "y": float(ey * TILE),
                         "vx": -0.5, "alive": True}
                        for ex, ey in enemy_positions]

    # --- Sounds ---

    def _init_sounds(self):
        # Jump
        pyxel.sounds[0].set(
            notes="c2e2g2c3", tones="s", volumes="7776", effects="nnnn", speed=8)
        # Coin
        pyxel.sounds[1].set(
            notes="c3e3g3c4c4", tones="s", volumes="55555", effects="nnnnf", speed=7)
        # Stomp
        pyxel.sounds[2].set(
            notes="g3c3", tones="s", volumes="74", effects="nn", speed=5)
        # Death
        pyxel.sounds[3].set(
            notes="f3b2f2b1f1f1f1f1", tones="p",
            volumes="44444321", effects="nnnnnnnf", speed=9)
        # Game over
        pyxel.sounds[4].set(
            notes="c3g2e2c2c2c2", tones="p",
            volumes="543210", effects="nnnnnn", speed=12)

    def _init_bgm(self):
        mml = pyxel.gen_bgm(0, 1, seed=42)
        for i in range(3):
            pyxel.sounds[10 + i].mml(mml[i])
        pyxel.musics[0].set([10], [11], [12])

    # --- Update ---

    def update(self):
        if self.scene == SCENE_TITLE:
            self._update_title()
        elif self.scene == SCENE_GAME:
            self._update_game()
        elif self.scene == SCENE_GAMEOVER:
            self._update_gameover()

    def _update_title(self):
        if pyxel.btnp(pyxel.KEY_RETURN) or pyxel.btnp(pyxel.KEY_SPACE):
            self.scene = SCENE_GAME
            self._reset()
            pyxel.playm(0, loop=True)

    def _update_gameover(self):
        if pyxel.btnp(pyxel.KEY_RETURN) or pyxel.btnp(pyxel.KEY_SPACE):
            self.scene = SCENE_TITLE

    def _update_game(self):
        self._update_player()
        self._update_enemies()
        self._update_coins()
        self._update_particles()
        self._update_camera()

        if self.hit_timer > 0:
            self.hit_timer -= 1
        if self.shake_dur > 0:
            self.shake_dur -= 1
            self.shake_mag *= 0.7

    def _update_player(self):
        # Horizontal
        if pyxel.btn(pyxel.KEY_LEFT):
            self.vx = -WALK_SPEED
            self.direction = -1
        elif pyxel.btn(pyxel.KEY_RIGHT):
            self.vx = WALK_SPEED
            self.direction = 1
        else:
            self.vx *= 0.7
            if abs(self.vx) < 0.1:
                self.vx = 0

        # Coyote time
        if self.on_ground:
            self.coyote = COYOTE_FRAMES
        else:
            self.coyote = max(0, self.coyote - 1)

        # Jump buffer
        if pyxel.btnp(pyxel.KEY_SPACE) or pyxel.btnp(pyxel.KEY_UP):
            self.jump_buffer = JUMP_BUFFER_FRAMES

        if self.jump_buffer > 0:
            self.jump_buffer -= 1
            if self.coyote > 0:
                self.vy = JUMP_VEL
                self.coyote = 0
                self.jump_buffer = 0
                pyxel.play(3, 0)

        # Gravity
        self.vy = min(self.vy + GRAVITY, MAX_FALL)

        # Move X
        new_x = self.px + self.vx
        if self.vx > 0:
            if self._is_solid(new_x + 6, self.py + 2) or self._is_solid(new_x + 6, self.py + 6):
                new_x = (int(new_x + 6) // TILE) * TILE - 7
                self.vx = 0
        elif self.vx < 0:
            if self._is_solid(new_x + 1, self.py + 2) or self._is_solid(new_x + 1, self.py + 6):
                new_x = (int(new_x + 1) // TILE + 1) * TILE - 1
                self.vx = 0
        self.px = new_x

        # Move Y
        new_y = self.py + self.vy
        self.on_ground = False
        if self.vy > 0:
            if self._is_solid(self.px + 2, new_y + 7) or self._is_solid(self.px + 5, new_y + 7):
                new_y = (int(new_y + 7) // TILE) * TILE - 8
                self.vy = 0
                self.on_ground = True
        elif self.vy < 0:
            if self._is_solid(self.px + 2, new_y) or self._is_solid(self.px + 5, new_y):
                new_y = (int(new_y) // TILE + 1) * TILE
                self.vy = 0
        self.py = new_y

        # Clamp
        self.px = max(self.camera_x, self.px)

        # Fall death
        if self.py > SCR_H + 16:
            self._die()

    def _update_enemies(self):
        for e in list(self.enemies):
            if not e["alive"]:
                continue
            # Skip far offscreen
            if abs(e["x"] - self.camera_x) > SCR_W + 32:
                continue
            e["x"] += e["vx"]
            # Turn at walls
            if e["vx"] < 0 and self._is_solid(e["x"], e["y"] + 4):
                e["vx"] = 0.5
            elif e["vx"] > 0 and self._is_solid(e["x"] + 7, e["y"] + 4):
                e["vx"] = -0.5
            # Turn at edges
            if e["vx"] < 0 and not self._is_solid(e["x"], e["y"] + 8):
                e["vx"] = 0.5
            elif e["vx"] > 0 and not self._is_solid(e["x"] + 7, e["y"] + 8):
                e["vx"] = -0.5

            # Collision with player
            if not e["alive"]:
                continue
            dx = abs(self.px + 3 - (e["x"] + 4))
            dy = self.py + 4 - (e["y"] + 4)
            if dx < 6 and abs(dy) < 6:
                if self.vy > 0 and dy < -2:
                    # Stomp
                    e["alive"] = False
                    self.vy = -3
                    self.score += 100
                    pyxel.play(3, 2)
                    self.shake_dur = 3
                    self.shake_mag = 2
                    for _ in range(6):
                        self.particles.append({
                            "x": e["x"] + 4, "y": e["y"] + 4,
                            "dx": pyxel.rndf(-1.5, 1.5),
                            "dy": pyxel.rndf(-2, 0),
                            "life": 10, "col": 9,
                        })
                else:
                    self._die()

        self.enemies = [e for e in self.enemies if e["alive"]]

    def _update_coins(self):
        for c in self.coins:
            if not c["alive"]:
                continue
            dx = abs(self.px + 3 - (c["x"] + 3))
            dy = abs(self.py + 3 - (c["y"] + 3))
            if dx < 6 and dy < 6:
                c["alive"] = False
                self.score += 50
                pyxel.play(3, 1)
                for _ in range(4):
                    self.particles.append({
                        "x": c["x"] + 4, "y": c["y"] + 4,
                        "dx": pyxel.rndf(-1, 1),
                        "dy": pyxel.rndf(-2, -0.5),
                        "life": 8, "col": 10,
                    })

    def _update_particles(self):
        for p in self.particles:
            p["x"] += p["dx"]
            p["y"] += p["dy"]
            p["dy"] += 0.1
            p["life"] -= 1
        self.particles = [p for p in self.particles if p["life"] > 0]

    def _update_camera(self):
        target = self.px - SCR_W // 3
        self.camera_x += (target - self.camera_x) * 0.1
        self.camera_x = max(0, self.camera_x)

    def _die(self):
        self.lives -= 1
        pyxel.play(3, 3)
        if self.lives <= 0:
            self.high_score = max(self.high_score, self.score)
            pyxel.play(3, 4)
            self.scene = SCENE_GAMEOVER
        else:
            self.px = max(self.camera_x + 16, 16)
            self.py = 40.0
            self.vx = 0
            self.vy = 0
            self.hit_timer = 30

    # --- Draw ---

    def draw(self):
        pyxel.cls(12)
        if self.scene == SCENE_TITLE:
            self._draw_title()
        elif self.scene == SCENE_GAME:
            self._draw_game()
        elif self.scene == SCENE_GAMEOVER:
            self._draw_gameover()

    def _draw_title(self):
        # Clouds
        for i in range(5):
            x = (i * 40 + pyxel.frame_count // 3) % (SCR_W + 32) - 16
            pyxel.blt(x, 15 + i * 8, 0, 0, 24, 16, 8, colkey=0)

        # Title
        t = "PYXEL MARIO"
        tx = (SCR_W - len(t) * pyxel.FONT_WIDTH) // 2
        pyxel.text(tx + 1, 36, t, 1)
        pyxel.text(tx, 35, t, 8)

        # Subtitle
        t2 = "A Platformer Adventure"
        pyxel.text((SCR_W - len(t2) * pyxel.FONT_WIDTH) // 2, 50, t2, 7)

        # Controls
        pyxel.text(30, 70, "ARROWS: MOVE", 13)
        pyxel.text(30, 80, "SPACE:  JUMP", 13)

        # Blinking prompt
        if pyxel.frame_count % 40 < 28:
            t3 = "PRESS ENTER"
            pyxel.text((SCR_W - len(t3) * pyxel.FONT_WIDTH) // 2, 100, t3, 10)

        # Animated player
        u = pyxel.frame_count // 6 % 2 * 8
        pyxel.blt(20, 95, 0, u, 0, 8, 8, colkey=0)
        pyxel.blt(SCR_W - 28, 95, 0, u, 0, -8, 8, colkey=0)

    def _draw_game(self):
        cx = int(self.camera_x)

        # Screen shake
        ox, oy = 0, 0
        if self.shake_dur > 0:
            ox = pyxel.rndi(-int(self.shake_mag), int(self.shake_mag))
            oy = pyxel.rndi(-int(self.shake_mag), int(self.shake_mag))
        pyxel.camera(cx + ox, oy)

        # Background clouds (parallax)
        bg_offset = cx // 3
        for i in range(8):
            bx = i * 30 + bg_offset % 30 - 30 + cx
            pyxel.blt(bx, 10 + (i % 3) * 12, 0, 0, 24, 16, 8, colkey=0)

        # Background bushes (parallax)
        bush_offset = cx // 2
        for i in range(6):
            bx = i * 40 + bush_offset % 40 - 40 + cx
            pyxel.blt(bx, 88, 0, 16, 24, 16, 8, colkey=0)

        # Draw tilemap
        start_tx = max(0, cx // TILE - 1)
        end_tx = min(MAP_W, (cx + SCR_W) // TILE + 2)
        for ty in range(MAP_H):
            for tx in range(start_tx, end_tx):
                t = self.tilemap[ty][tx]
                if t == 1:  # ground
                    pyxel.rect(tx * TILE, ty * TILE, TILE, TILE, 4)
                    if ty > 0 and self.tilemap[ty - 1][tx] == 0:
                        pyxel.rect(tx * TILE, ty * TILE, TILE, 2, 3)
                elif t == 2:  # platform
                    pyxel.rect(tx * TILE, ty * TILE, TILE, TILE, 4)
                    pyxel.rect(tx * TILE, ty * TILE, TILE, 2, 9)
                elif t == 3:  # pipe
                    pyxel.rect(tx * TILE, ty * TILE, TILE, TILE, 3)
                    pyxel.rect(tx * TILE, ty * TILE, 2, TILE, 11)

        # Coins
        for c in self.coins:
            if c["alive"]:
                u = pyxel.frame_count // 8 % 2 * 8
                pyxel.blt(c["x"], c["y"], 0, u, 8, 8, 8, colkey=0)

        # Enemies
        for e in self.enemies:
            if abs(e["x"] - cx) > SCR_W + 16:
                continue
            u = pyxel.frame_count // 6 % 2 * 8
            w = 8 if e["vx"] < 0 else -8
            pyxel.blt(e["x"], e["y"], 0, u, 16, w, 8, colkey=0)

        # Player
        if self.hit_timer == 0 or pyxel.frame_count % 4 < 2:
            if not self.on_ground:
                u = 16
            elif abs(self.vx) > 0.3:
                u = pyxel.frame_count // 4 % 2 * 8
            else:
                u = 0
            w = 8 if self.direction > 0 else -8
            pyxel.blt(self.px, self.py, 0, u, 0, w, 8, colkey=0)

        # Particles
        for p in self.particles:
            col = p["col"] if p["life"] > 4 else 9
            pyxel.pset(int(p["x"]), int(p["y"]), col)

        # HUD
        pyxel.camera()
        pyxel.text(4, 4, f"SCORE:{self.score:05d}", 7)
        for i in range(self.lives):
            pyxel.blt(SCR_W - 12 - i * 10, 2, 0, 0, 0, 8, 8, colkey=0)

    def _draw_gameover(self):
        pyxel.cls(0)
        t = "GAME OVER"
        pyxel.text((SCR_W - len(t) * pyxel.FONT_WIDTH) // 2, 40, t, 8)
        t2 = f"SCORE: {self.score:05d}"
        pyxel.text((SCR_W - len(t2) * pyxel.FONT_WIDTH) // 2, 55, t2, 7)
        t3 = f"HIGH:  {self.high_score:05d}"
        pyxel.text((SCR_W - len(t3) * pyxel.FONT_WIDTH) // 2, 65, t3, 10)
        if pyxel.frame_count % 40 < 28:
            t4 = "PRESS ENTER"
            pyxel.text((SCR_W - len(t4) * pyxel.FONT_WIDTH) // 2, 90, t4, 13)


App()
