#pragma once

#include "engine/prefabs/includes.h"
#include "entities/song.h"
#include "rapidjson/filereadstream.h"
#include "background.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <string>
#include <unordered_set>
#include <vector>
#include <deque>

namespace
{
constexpr int LANE_COUNT = 12;
constexpr int OCTAVE_COUNT = 2;
constexpr int MAX_GAMEPADS = 4;
constexpr int MAX_INSTRUMENT_TYPES = MAX_GAMEPADS;
constexpr float RECEPTOR_HEIGHT = 150.0f;
constexpr float HIT_ZONE_MARGIN = 20.0f;
constexpr float SIMULTANEOUS_NOTE_Y_TOLERANCE = 2.0f;
constexpr float SCROLL_PX_PER_SEC = 350.0f;
constexpr float LEAD_OFFSET_SECONDS = 3.0f;
constexpr float GLYPH_HEIGHT_FRACTION_OF_LANE = 0.5f;
constexpr float MIN_SUSTAIN_FALLBACK_SEC = 0.05f;
constexpr float MIN_GLYPH_DURATION_SEC = 0.05f;

const int GAMEPAD_BUTTONS[LANE_COUNT] = {
    GAMEPAD_BUTTON_LEFT_FACE_LEFT, // Left
    GAMEPAD_BUTTON_LEFT_FACE_UP, // Up
    GAMEPAD_BUTTON_LEFT_FACE_RIGHT, // Right
    GAMEPAD_BUTTON_LEFT_FACE_DOWN, // Down
    GAMEPAD_BUTTON_RIGHT_FACE_LEFT, // X
    GAMEPAD_BUTTON_RIGHT_FACE_UP, // Y
    GAMEPAD_BUTTON_RIGHT_FACE_RIGHT, // B
    GAMEPAD_BUTTON_RIGHT_FACE_DOWN, // A
    GAMEPAD_BUTTON_LEFT_TRIGGER_2, // LT
    GAMEPAD_BUTTON_LEFT_TRIGGER_1, // LB
    GAMEPAD_BUTTON_RIGHT_TRIGGER_1, // RB
    GAMEPAD_BUTTON_RIGHT_TRIGGER_2, // RT
};

const char* const GAMEPAD_BUTTON_LABELS[LANE_COUNT] = {
    "<", "^", ">", "v", "X", "Y", "B", "A", "LT", "LB", "RB", "RT",
};

constexpr int COMBO_TIER_COUNT = 20;
constexpr int COMBO_DISPLAY_THRESHOLD = 5;
constexpr int COMBO_NOTES_PER_TIER = 10;
const char* const COMBO_TIER_LABELS[COMBO_TIER_COUNT] = {
    "Cool!", "Awesome!", "Radical!", "Amazing!", "Incredible!",
    "Fantastic!", "Excellent!", "Outstanding!", "Perfect!", "Brilliant!",
    "Superb!", "Magnificent!", "Phenomenal!", "Splendid!", "Terrific!",
    "Stellar!", "Legendary!", "Epic!", "Divine!", "Godlike!",
};

const int KEY_KEYS[LANE_COUNT] = {
    KEY_Q,
    KEY_W,
    KEY_E,
    KEY_R,
    KEY_A,
    KEY_S,
    KEY_D,
    KEY_F,
    KEY_Z,
    KEY_X,
    KEY_C,
    KEY_V,
};

/* One color per track (track index % MAX_INSTRUMENT_TYPES). */
const Color INSTRUMENT_COLORS[MAX_INSTRUMENT_TYPES] = {
    {220, 100, 100, 255}, // red
    {100, 160, 255, 255}, // blue
    {100, 220, 120, 255}, // green
    {255, 200, 80, 255}, // yellow
};

const char* const INSTRUMENT_LANE_WAV[MAX_INSTRUMENT_TYPES][LANE_COUNT * OCTAVE_COUNT] {
    { // Instrument 0 - Bass
        "assets/sounds/genesis_bass/genesis_bass_024.wav", "assets/sounds/genesis_bass/genesis_bass_025.wav",
        "assets/sounds/genesis_bass/genesis_bass_026.wav", "assets/sounds/genesis_bass/genesis_bass_027.wav",
        "assets/sounds/genesis_bass/genesis_bass_028.wav", "assets/sounds/genesis_bass/genesis_bass_029.wav",
        "assets/sounds/genesis_bass/genesis_bass_030.wav", "assets/sounds/genesis_bass/genesis_bass_031.wav",
        "assets/sounds/genesis_bass/genesis_bass_032.wav", "assets/sounds/genesis_bass/genesis_bass_033.wav",
        "assets/sounds/genesis_bass/genesis_bass_034.wav", "assets/sounds/genesis_bass/genesis_bass_035.wav",
        "assets/sounds/genesis_bass/genesis_bass_036.wav", "assets/sounds/genesis_bass/genesis_bass_037.wav",
        "assets/sounds/genesis_bass/genesis_bass_038.wav", "assets/sounds/genesis_bass/genesis_bass_039.wav",
        "assets/sounds/genesis_bass/genesis_bass_040.wav", "assets/sounds/genesis_bass/genesis_bass_041.wav",
        "assets/sounds/genesis_bass/genesis_bass_042.wav", "assets/sounds/genesis_bass/genesis_bass_043.wav",
        "assets/sounds/genesis_bass/genesis_bass_044.wav", "assets/sounds/genesis_bass/genesis_bass_045.wav",
        "assets/sounds/genesis_bass/genesis_bass_046.wav", "assets/sounds/genesis_bass/genesis_bass_047.wav"
    },
    { // Instrument 1 - Synth
        "assets/sounds/snes_synth/snes_synth_048.wav", "assets/sounds/snes_synth/snes_synth_049.wav",
        "assets/sounds/snes_synth/snes_synth_050.wav", "assets/sounds/snes_synth/snes_synth_051.wav",
        "assets/sounds/snes_synth/snes_synth_052.wav", "assets/sounds/snes_synth/snes_synth_053.wav",
        "assets/sounds/snes_synth/snes_synth_054.wav", "assets/sounds/snes_synth/snes_synth_055.wav",
        "assets/sounds/snes_synth/snes_synth_056.wav", "assets/sounds/snes_synth/snes_synth_057.wav",
        "assets/sounds/snes_synth/snes_synth_058.wav", "assets/sounds/snes_synth/snes_synth_059.wav",
        "assets/sounds/snes_synth/snes_synth_060.wav", "assets/sounds/snes_synth/snes_synth_061.wav",
        "assets/sounds/snes_synth/snes_synth_062.wav", "assets/sounds/snes_synth/snes_synth_063.wav",
        "assets/sounds/snes_synth/snes_synth_064.wav", "assets/sounds/snes_synth/snes_synth_065.wav",
        "assets/sounds/snes_synth/snes_synth_066.wav", "assets/sounds/snes_synth/snes_synth_067.wav",
        "assets/sounds/snes_synth/snes_synth_068.wav", "assets/sounds/snes_synth/snes_synth_069.wav",
        "assets/sounds/snes_synth/snes_synth_070.wav", "assets/sounds/snes_synth/snes_synth_071.wav"
    },
    { // Instrument 2
        "assets/sounds/genesis_bass/genesis_bass_024.wav", "assets/sounds/genesis_bass/genesis_bass_025.wav",
        "assets/sounds/genesis_bass/genesis_bass_026.wav", "assets/sounds/genesis_bass/genesis_bass_027.wav",
        "assets/sounds/genesis_bass/genesis_bass_028.wav", "assets/sounds/genesis_bass/genesis_bass_029.wav",
        "assets/sounds/genesis_bass/genesis_bass_030.wav", "assets/sounds/genesis_bass/genesis_bass_031.wav",
        "assets/sounds/genesis_bass/genesis_bass_032.wav", "assets/sounds/genesis_bass/genesis_bass_033.wav",
        "assets/sounds/genesis_bass/genesis_bass_034.wav", "assets/sounds/genesis_bass/genesis_bass_035.wav",
        "assets/sounds/genesis_bass/genesis_bass_036.wav", "assets/sounds/genesis_bass/genesis_bass_037.wav",
        "assets/sounds/genesis_bass/genesis_bass_038.wav", "assets/sounds/genesis_bass/genesis_bass_039.wav",
        "assets/sounds/genesis_bass/genesis_bass_040.wav", "assets/sounds/genesis_bass/genesis_bass_041.wav",
        "assets/sounds/genesis_bass/genesis_bass_042.wav", "assets/sounds/genesis_bass/genesis_bass_043.wav",
        "assets/sounds/genesis_bass/genesis_bass_044.wav", "assets/sounds/genesis_bass/genesis_bass_045.wav",
        "assets/sounds/genesis_bass/genesis_bass_046.wav", "assets/sounds/genesis_bass/genesis_bass_047.wav"
    },
    { // Instrument 3
        "assets/sounds/snes_synth/snes_synth_048.wav", "assets/sounds/snes_synth/snes_synth_049.wav",
        "assets/sounds/snes_synth/snes_synth_050.wav", "assets/sounds/snes_synth/snes_synth_051.wav",
        "assets/sounds/snes_synth/snes_synth_052.wav", "assets/sounds/snes_synth/snes_synth_053.wav",
        "assets/sounds/snes_synth/snes_synth_054.wav", "assets/sounds/snes_synth/snes_synth_055.wav",
        "assets/sounds/snes_synth/snes_synth_056.wav", "assets/sounds/snes_synth/snes_synth_057.wav",
        "assets/sounds/snes_synth/snes_synth_058.wav", "assets/sounds/snes_synth/snes_synth_059.wav",
        "assets/sounds/snes_synth/snes_synth_060.wav", "assets/sounds/snes_synth/snes_synth_061.wav",
        "assets/sounds/snes_synth/snes_synth_062.wav", "assets/sounds/snes_synth/snes_synth_063.wav",
        "assets/sounds/snes_synth/snes_synth_064.wav", "assets/sounds/snes_synth/snes_synth_065.wav",
        "assets/sounds/snes_synth/snes_synth_066.wav", "assets/sounds/snes_synth/snes_synth_067.wav",
        "assets/sounds/snes_synth/snes_synth_068.wav", "assets/sounds/snes_synth/snes_synth_069.wav",
        "assets/sounds/snes_synth/snes_synth_070.wav", "assets/sounds/snes_synth/snes_synth_071.wav"
    }
};

struct Glyph
{
    float time = 0.0f;           // Time when the BOTTOM of the glyph hits the line (when note should be played)
    float duration_sec = 0.0f;   // Duration extends upward from bottom (top = time + duration)
    int lane = 0;
    int instrument_slot = 0;
    int octave = 0;

    // Returns Y position of the bottom of the glyph
    float y_position(float song_time, float hit_line_y) const
    {
        return hit_line_y - (time - song_time) * SCROLL_PX_PER_SEC;
    }
};

struct PendingSound
{
    float play_time = 0.0f;
    float duration_sec = 0.0f;
    int lane = 0;
    int instrument_slot = 0;
    int octave = 0;
};

struct ActiveSustainedSound
{
    float start_time = 0.0f;
    float end_time = 0.0f;
    int lane = 0;
    int instrument_slot = 0;
};

static size_t pick_track_by_note_count(const Song& song)
{
    size_t best = 0;
    size_t best_count = 0;
    for (size_t i = 0; i < song.tracks.size(); i++)
    {
        size_t n = song.tracks[i].notes.size();
        if (n > best_count)
        {
            best_count = n;
            best = i;
        }
    }
    return best;
}

std::vector<Glyph> chart_from_song(const Song& song, int track_override)
{
    std::vector<Glyph> glyphs;
    if (song.tracks.empty())
        return glyphs;

    size_t track_idx;
    if (track_override >= 0 &&
        static_cast<size_t>(track_override) < song.tracks.size())
    {
        track_idx = static_cast<size_t>(track_override);
    }
    else
    {
        track_idx = pick_track_by_note_count(song);
    }
    const Track& track = song.tracks[track_idx];
    int ppq = song.header.ppq > 0 ? song.header.ppq : 480;
    float bpm = song.header.bpm > 0.0f ? song.header.bpm : 120.0f;
    float ticks_per_sec = ppq * (bpm / 60.0f);

    std::vector<std::pair<int, const Note*>> timed_notes;
    for (const Note& note : track.notes)
    {
        if (note.midi >= 0 && note.midi <= 127)
            timed_notes.push_back({note.ticks, &note});
    }
    std::sort(timed_notes.begin(), timed_notes.end(),
              [](const auto& a, const auto& b) {
                  if (a.first != b.first)
                      return a.first < b.first;
                  return a.second->midi < b.second->midi;
              });

    int note_index = 0;
    for (const auto& pair : timed_notes)
    {
        const Note& note = *pair.second;
        float time_sec = note.ticks / ticks_per_sec;  // This is now the BOTTOM time
        float duration_sec = note.duration_ticks / ticks_per_sec;
        int lane = note.midi % LANE_COUNT;
        int octave = note.midi % (LANE_COUNT * OCTAVE_COUNT);
        int instrument_slot = note_index % MAX_INSTRUMENT_TYPES;

        // Log original glyph timing (time is bottom, time + duration is top)
        float new_bottom_time = time_sec;
        float new_top_time = time_sec + duration_sec;
        std::printf("[NEW GLYPH] lane=%d bottom=%.3f top=%.3f dur=%.3f\n",
                    lane, new_bottom_time, new_top_time, duration_sec);

        Glyph* last_in_lane = nullptr;
        for (size_t i = glyphs.size(); i-- > 0;)
        {
            if (glyphs[i].lane == lane)
            {
                last_in_lane = &glyphs[i];
                break;
            }
        }
        if (last_in_lane != nullptr)
        {
            // Check if existing glyph's top overlaps with new glyph's bottom
            float existing_bottom = last_in_lane->time;
            float existing_top = last_in_lane->time + last_in_lane->duration_sec;

            std::printf("  [CHECK] existing lane=%d bottom=%.3f top=%.3f vs new_bottom=%.3f\n",
                        last_in_lane->lane, existing_bottom, existing_top, new_bottom_time);

            if (existing_top > new_bottom_time)
            {
                // Truncate existing glyph so its top doesn't go past new glyph's bottom
                float shortened_duration = new_bottom_time - existing_bottom;

                std::printf("  [OVERLAP DETECTED] shortened_dur=%.3f (was %.3f)\n",
                            shortened_duration, last_in_lane->duration_sec);

                if (shortened_duration < MIN_GLYPH_DURATION_SEC)
                {
                    std::printf("  [SKIP] shortened duration %.3f < MIN %.3f, skipping new glyph\n",
                                shortened_duration, MIN_GLYPH_DURATION_SEC);
                    note_index++;
                    continue;
                }

                float old_duration = last_in_lane->duration_sec;
                last_in_lane->duration_sec = shortened_duration;

                std::printf("  [TRUNCATED] lane=%d: duration %.3f -> %.3f (new_top=%.3f)\n",
                            last_in_lane->lane, old_duration, shortened_duration,
                            last_in_lane->time + shortened_duration);
            }
        }

        glyphs.push_back(Glyph{time_sec, duration_sec, lane, instrument_slot, octave});
        std::printf("  [ADDED] glyph count now: %zu\n\n", glyphs.size());
        note_index++;
    }

    return glyphs;
}

std::vector<Glyph> load_chart(const char* path, int track_override)
{
    std::vector<Glyph> empty;
    std::FILE* fp = std::fopen(path, "rb");
    if (!fp)
        return empty;
    char read_buf[65536];
    rapidjson::FileReadStream is(fp, read_buf, sizeof(read_buf));
    rapidjson::Document doc;
    if (!doc.ParseStream(is).HasParseError())
    {
        Song song = parseSong(doc);
        std::fclose(fp);
        return chart_from_song(song, track_override);
    }
    std::fclose(fp);
    return empty;
}

} // namespace

extern std::string SELECTED_SONG_PATH;
extern int SELECTED_TRACK_OVERRIDE;
extern int INSTRUMENT_GAMEPAD_INDEX[MAX_INSTRUMENT_TYPES];
extern int INSTRUMENT_PHYSICAL_GAMEPAD[MAX_INSTRUMENT_TYPES];

class GHHBScene : public Scene
{
public:
    Font font = {0};
    Music music = {0};
    bool music_loaded = false;
    std::vector<Glyph> chart;
    std::vector<Glyph*> spawned;
    std::unordered_set<Glyph*> completed_notes;
    std::unordered_set<Glyph*> missed_notes;
    std::vector<PendingSound> pending_sounds;
    std::vector<ActiveSustainedSound> active_sustained;
    float song_time = 0.0f;
    float chart_time_offset = 0.0f;
    int score = 0;
    int combo = 0;
    float upper_bar_y = 0.0f;
    float hit_line_y = 0.0f;
    float lane_width = 0.0f;
    float screen_width = 0.0f;
    float screen_height = 0.0f;
    Sound note_sounds[MAX_INSTRUMENT_TYPES][LANE_COUNT * OCTAVE_COUNT] = {{0}};
    bool note_sounds_loaded[MAX_INSTRUMENT_TYPES][LANE_COUNT * OCTAVE_COUNT] = {{false}};
    std::deque<Sound> note_sounds_playing[LANE_COUNT][MAX_INSTRUMENT_TYPES];
    static constexpr float PRESS_FLASH_DURATION = 0.12f;
    float press_flash_timer[LANE_COUNT] = {0};
    float hit_flash_timer[LANE_COUNT] = {0};
    bool game_ended = false;
    bool dev_auto_hit_mode = false;
    static constexpr float RESULTS_DELAY_AFTER_LAST_NOTE = 1.0f;
    std::shared_ptr<Background> background;

    void on_enter() override
    {
        chart = load_chart(SELECTED_SONG_PATH.c_str(), SELECTED_TRACK_OVERRIDE);
        float first_note_time = 0.0f;
        if (!chart.empty())
        {
            first_note_time = chart.front().time;
            float lead_seconds = (hit_line_y - 60.0f) / SCROLL_PX_PER_SEC;
            chart_time_offset = lead_seconds + LEAD_OFFSET_SECONDS - first_note_time;
        }
        song_time = 0.0f;
        score = 0;
        combo = 0;
        game_ended = false;
        dev_auto_hit_mode = false;
        spawned.clear();
        completed_notes.clear();
        missed_notes.clear();
        pending_sounds.clear();
        active_sustained.clear();
        for (int i = 0; i < LANE_COUNT; i++)
        {
            press_flash_timer[i] = 0.0f;
            hit_flash_timer[i] = 0.0f;
        }
        if (music_loaded)
        {
            StopMusicStream(music);
            PlayMusicStream(music);
            if (chart_time_offset < 0.0f)
            {
                SeekMusicStream(music, -chart_time_offset);
            }
        }
    }

    void on_exit() override
    {
        if (music_loaded)
        {
            StopMusicStream(music);
        }
    }

    ~GHHBScene() override
    {
        if (music_loaded)
        {
            StopMusicStream(music);
            UnloadMusicStream(music);
        }
        for (int slot = 0; slot < MAX_INSTRUMENT_TYPES; slot++)
        {
            for (int lane = 0; lane < LANE_COUNT * OCTAVE_COUNT; lane++)
            {
                if (note_sounds_loaded[slot][lane])
                {
                    UnloadSound(note_sounds[slot][lane]);
                }
            }
        }
    }

    void init_services() override {
        add_service<TextureService>();
    }

    void init() override
    {
        auto font_manager = game->get_manager<FontManager>();
        font = font_manager->get_font("Roboto");
        update_layout();
        for (int slot = 0; slot < MAX_INSTRUMENT_TYPES; slot++)
        {
            for (int lane = 0; lane < LANE_COUNT * OCTAVE_COUNT; lane++)
            {
                const char* path = INSTRUMENT_LANE_WAV[slot][lane];
                if (FileExists(path))
                {
                    note_sounds[slot][lane] = LoadSound(path);
                    note_sounds_loaded[slot][lane] = true;
                }
            }
        }
        background = add_game_object<Background>();
        background->add_tag("background");
    }

    float lane_center_x(int lane) const
    {
        return (lane + 0.5f) * lane_width;
    }

    bool is_lane_pressed(int lane) const
    {
        if (IsKeyPressed(KEY_KEYS[lane]))
        {
            return true;
        }
        for (int i = 0; i < MAX_GAMEPADS; i++)
        {
            if (IsGamepadAvailable(i) && IsGamepadButtonPressed(i, GAMEPAD_BUTTONS[lane]))
            {
                return true;
            }
        }
        return false;
    }

    bool is_lane_held_by_instrument(int lane, int instrument_slot) const
    {
        int physical_id = INSTRUMENT_PHYSICAL_GAMEPAD[instrument_slot];
        if (physical_id < 0)
        {
            if (IsKeyDown(KEY_KEYS[lane]))
                return true;
            for (int i = 0; i < MAX_GAMEPADS; i++)
            {
                if (IsGamepadAvailable(i) && IsGamepadButtonDown(i, GAMEPAD_BUTTONS[lane]))
                    return true;
            }
            return false;
        }
        return IsGamepadAvailable(physical_id) &&
               IsGamepadButtonDown(physical_id, GAMEPAD_BUTTONS[lane]);
    }

    void stop_playing_released_notes(int lane, float min_sustain_sec)
    {
        for (int slot = 0; slot < MAX_INSTRUMENT_TYPES; slot++)
        {
            if (is_lane_held_by_instrument(lane, slot))
                continue;
            auto it = std::find_if(active_sustained.begin(), active_sustained.end(),
                [lane, slot](const ActiveSustainedSound& a) {
                    return a.lane == lane && a.instrument_slot == slot;
                });
            if (it == active_sustained.end())
                continue;
            if (song_time < it->start_time + min_sustain_sec)
                continue;
            if (!note_sounds_playing[lane][slot].empty())
            {
                StopSound(note_sounds_playing[lane][slot].front());
                note_sounds_playing[lane][slot].pop_front();
            }
            active_sustained.erase(it);
        }
    }

    bool lane_pressed_by_instrument_owner(int lane, int instrument_slot) const
    {
        int physical_id = INSTRUMENT_PHYSICAL_GAMEPAD[instrument_slot];
        if (physical_id < 0)
        {
            for (int i = 0; i < MAX_GAMEPADS; i++)
            {
                if (IsGamepadAvailable(i) && IsGamepadButtonPressed(i, GAMEPAD_BUTTONS[lane]))
                    return true;
            }
            return false;
        }
        return IsGamepadAvailable(physical_id) &&
               IsGamepadButtonPressed(physical_id, GAMEPAD_BUTTONS[lane]);
    }

    bool is_menu_pressed() const
    {
        if (IsKeyPressed(KEY_ENTER))
        {
            return true;
        }
        for (int i = 0; i < MAX_GAMEPADS; i++)
        {
            if (IsGamepadAvailable(i) && IsGamepadButtonPressed(i, GAMEPAD_BUTTON_MIDDLE_RIGHT))
            {
                return true;
            }
        }
        return false;
    }

    bool is_select_pressed() const
    {
        for (int i = 0; i < MAX_GAMEPADS; i++)
        {
            if (IsGamepadAvailable(i) && IsGamepadButtonPressed(i, GAMEPAD_BUTTON_MIDDLE_LEFT))
            {
                return true;
            }
        }
        return false;
    }

    // Returns the Y position of the BOTTOM of the glyph
    // n.time represents when the bottom should reach the hit line
    float glyph_bottom_y(const Glyph& n) const
    {
        return hit_line_y - (n.time + chart_time_offset - song_time) * SCROLL_PX_PER_SEC;
    }

    static float glyph_height_px(const Glyph& n, float lane_width_val)
    {
        float min_height = lane_width_val * GLYPH_HEIGHT_FRACTION_OF_LANE;
        float duration_height = n.duration_sec * SCROLL_PX_PER_SEC;
        return std::max(min_height, duration_height);
    }

    bool is_note_hittable(const Glyph& n) const
    {
        float bottom_y = glyph_bottom_y(n);
        return bottom_y >= upper_bar_y - HIT_ZONE_MARGIN && bottom_y <= hit_line_y + HIT_ZONE_MARGIN;
    }

    void consume_note(Glyph* n)
    {
        auto it = std::find_if(spawned.begin(), spawned.end(), [n](Glyph* p) { return p == n; });
        if (it != spawned.end())
        {
            hit_flash_timer[n->lane] = PRESS_FLASH_DURATION;
            completed_notes.insert(n);
            // n.time is when the bottom should hit the line
            float bottom_hits_line_time = n->time + chart_time_offset;
            if (note_sounds_loaded[n->instrument_slot][n->octave])
                pending_sounds.push_back(
                    {bottom_hits_line_time, n->duration_sec, n->lane, n->instrument_slot, n->octave});
            float bottom_y_n = glyph_bottom_y(*n);
            for (Glyph* other : spawned)
            {
                if (other == n)
                    continue;
                if (other->lane != n->lane || other->instrument_slot != n->instrument_slot)
                    continue;
                float bottom_y_other = glyph_bottom_y(*other);
                if (fabsf(bottom_y_other - bottom_y_n) <= SIMULTANEOUS_NOTE_Y_TOLERANCE)
                {
                    completed_notes.insert(other);
                    float other_bottom_hits_line_time = other->time + chart_time_offset;
                    if (note_sounds_loaded[other->instrument_slot][other->octave])
                        pending_sounds.push_back({other_bottom_hits_line_time, other->duration_sec,
                                                  other->lane, other->instrument_slot, other->octave});
                }
            }
        }
        combo++;
        score += 100 + std::min(combo * 10, 50);
    }

    void update_layout()
    {
        screen_width = static_cast<float>(GetScreenWidth());
        screen_height = static_cast<float>(GetScreenHeight());
        upper_bar_y = screen_height - RECEPTOR_HEIGHT;
        hit_line_y = screen_height - RECEPTOR_HEIGHT / 2.0f;
        lane_width = screen_width / LANE_COUNT;
    }

    void update(float delta_time) override
    {
        update_layout();
        {
            static bool debug_bottom_printed = false;
            if (!debug_bottom_printed && !chart.empty() && lane_width > 0.0f)
            {
                for (const Glyph& g : chart)
                {
                    float bottom_hits_line_time = g.time + chart_time_offset;
                    float top_hits_line_time = bottom_hits_line_time + g.duration_sec;
                    std::printf("[chart] glyph lane=%d bottom_time=%.3f top_time=%.3f dur=%.3f\n",
                                g.lane, bottom_hits_line_time, top_hits_line_time, g.duration_sec);
                }
                debug_bottom_printed = true;
            }
        }
        if (is_select_pressed())
        {
            dev_auto_hit_mode = !dev_auto_hit_mode;
        }
        if (game_ended)
        {
            if (is_menu_pressed())
            {
                game->go_to_scene("title");
            }
            return;
        }

        if (music_loaded)
        {
            UpdateMusicStream(music);
            song_time = GetMusicTimePlayed(music);
        }
        else
        {
            song_time += delta_time;
        }

        float min_glyph_height_px = lane_width * GLYPH_HEIGHT_FRACTION_OF_LANE;
        float time_per_glyph_height =
            min_glyph_height_px > 0.f ? min_glyph_height_px / SCROLL_PX_PER_SEC : MIN_SUSTAIN_FALLBACK_SEC;
        for (auto it = pending_sounds.begin(); it != pending_sounds.end();)
        {
            if (song_time >= it->play_time)
            {
                Sound s = note_sounds[it->instrument_slot][it->octave];
                PlaySound(s);
                note_sounds_playing[it->lane][it->instrument_slot].push_back(s);
                float sustain_sec = std::max(it->duration_sec, time_per_glyph_height);
                active_sustained.push_back(
                    {it->play_time, it->play_time + sustain_sec, it->lane, it->instrument_slot});
                it = pending_sounds.erase(it);
            }
            else
            {
                ++it;
            }
        }

        for (auto it = active_sustained.begin(); it != active_sustained.end();)
        {
            if (song_time >= it->end_time)
            {
                if (!note_sounds_playing[it->lane][it->instrument_slot].empty())
                {
                    StopSound(note_sounds_playing[it->lane][it->instrument_slot].front());
                    note_sounds_playing[it->lane][it->instrument_slot].pop_front();
                }
                it = active_sustained.erase(it);
            }
            else
            {
                ++it;
            }
        }

        float last_note_time = 0.0f;
        for (const auto& n : chart)
        {
            float t = n.time + chart_time_offset;
            if (t > last_note_time)
            {
                last_note_time = t;
            }
        }
        float note_exit_seconds = (hit_line_y - upper_bar_y) / SCROLL_PX_PER_SEC;
        if (song_time >= last_note_time + note_exit_seconds + RESULTS_DELAY_AFTER_LAST_NOTE)
        {
            game_ended = true;
            return;
        }

        float lead_seconds = (hit_line_y - 60.0f) / SCROLL_PX_PER_SEC;
        for (auto& n : chart)
        {
            if (completed_notes.count(&n) != 0)
            {
                continue;
            }
            bool already = std::find_if(spawned.begin(), spawned.end(),
                                       [&n](Glyph* p) { return p == &n; }) != spawned.end();
            if (!already && song_time >= n.time + chart_time_offset - lead_seconds)
            {
                spawned.push_back(&n);
            }
        }

        for (Glyph* n : spawned)
        {
            if (completed_notes.count(n) != 0)
                continue;
            if (missed_notes.count(n) != 0)
                continue;
            float bottom_y = glyph_bottom_y(*n);
            if (bottom_y > hit_line_y + HIT_ZONE_MARGIN)
            {
                missed_notes.insert(n);
                combo = 0;
            }
        }

        for (auto it = spawned.begin(); it != spawned.end();)
        {
            Glyph* n = *it;
            float bottom_y = glyph_bottom_y(*n);
            if (bottom_y > screen_height + 40.0f)
            {
                completed_notes.erase(n);
                missed_notes.erase(n);
                it = spawned.erase(it);
            }
            else
            {
                ++it;
            }
        }

        for (int lane = 0; lane < LANE_COUNT; lane++)
        {
            press_flash_timer[lane] -= delta_time;
            if (press_flash_timer[lane] < 0.0f)
            {
                press_flash_timer[lane] = 0.0f;
            }
            hit_flash_timer[lane] -= delta_time;
            if (hit_flash_timer[lane] < 0.0f)
            {
                hit_flash_timer[lane] = 0.0f;
            }
        }

        if (dev_auto_hit_mode)
        {
            std::vector<Glyph*> to_consume;
            for (Glyph* n : spawned)
            {
                if (completed_notes.count(n) != 0)
                    continue;
                if (is_note_hittable(*n))
                {
                    to_consume.push_back(n);
                }
            }
            for (Glyph* n : to_consume)
            {
                consume_note(n);
            }
        }
        else
        {
        for (int lane = 0; lane < LANE_COUNT; lane++)
        {
            stop_playing_released_notes(lane, time_per_glyph_height);
            bool pressed = is_lane_pressed(lane);
            if (pressed)
                press_flash_timer[lane] = PRESS_FLASH_DURATION;
            if (!pressed)
            {
                continue;
            }
            Glyph* best = nullptr;
            float best_dist = 1e9f;
            for (Glyph* n : spawned)
            {
                if (n->lane != lane)
                {
                    continue;
                }
                if (!is_note_hittable(*n))
                {
                    continue;
                }
                if (!lane_pressed_by_instrument_owner(lane, n->instrument_slot))
                {
                    continue;
                }
                float bottom_y = glyph_bottom_y(*n);
                float d = fabsf(bottom_y - hit_line_y);
                if (d < best_dist)
                {
                    best_dist = d;
                    best = n;
                }
            }
            if (best != nullptr && pressed)
            {
                consume_note(best);
            }
            else
            {
                combo = 0;
                score = std::max(0, score - 25);
            }
        }
        }

        if (is_menu_pressed())
        {
            game->go_to_scene("title");
        }
    }

    void draw_scene() override
    {
        if (background)
        {
            background->draw_object();
        }
        draw();
    }

    void draw() override
    {
        for (int lane = 0; lane < LANE_COUNT; lane++)
        {
            float cx = lane_center_x(lane);
            DrawLineEx(Vector2{cx, 0}, Vector2{cx, screen_height}, 2.0f, Color{70, 70, 90, 255});
        }

        DrawRectangle(0,
                      static_cast<int>(upper_bar_y),
                      static_cast<int>(screen_width),
                      static_cast<int>(RECEPTOR_HEIGHT),
                      Color{60, 60, 100, 255});
        for (int lane = 0; lane < LANE_COUNT; lane++)
        {
            if (hit_flash_timer[lane] > 0.0f)
            {
                float alpha = 180.0f * (hit_flash_timer[lane] / PRESS_FLASH_DURATION);
                DrawRectangle(static_cast<int>(lane * lane_width),
                              static_cast<int>(upper_bar_y),
                              static_cast<int>(lane_width),
                              static_cast<int>(RECEPTOR_HEIGHT),
                              Color{80, 255, 120, static_cast<unsigned char>(alpha)});
            }
            else if (press_flash_timer[lane] > 0.0f)
            {
                float alpha = 180.0f * (press_flash_timer[lane] / PRESS_FLASH_DURATION);
                DrawRectangle(static_cast<int>(lane * lane_width),
                              static_cast<int>(upper_bar_y),
                              static_cast<int>(lane_width),
                              static_cast<int>(RECEPTOR_HEIGHT),
                              Color{255, 255, 255, static_cast<unsigned char>(alpha)});
            }
        }
        DrawLineEx(Vector2{0, upper_bar_y}, Vector2{screen_width, upper_bar_y}, 3.0f, WHITE);
        DrawLineEx(Vector2{0, hit_line_y}, Vector2{screen_width, hit_line_y}, 3.0f, WHITE);

        const float button_label_font_size = 28.0f;
        const float button_label_y = hit_line_y + 18.0f;
        for (int lane = 0; lane < LANE_COUNT; lane++)
        {
            const char* label = GAMEPAD_BUTTON_LABELS[lane];
            float label_w = MeasureTextEx(font, label, button_label_font_size, 1).x;
            float cx = lane_center_x(lane);
            DrawTextEx(font, label,
                       {cx - label_w / 2.0f, button_label_y},
                       button_label_font_size, 1, Color{220, 220, 240, 255});
        }

        const Color MISSED_GLYPH_COLOR = {140, 140, 140, 255};
        std::vector<std::vector<Glyph*>> by_lane(static_cast<size_t>(LANE_COUNT));
        for (Glyph* n : spawned)
        {
            float height = glyph_height_px(*n, lane_width);
            float bottom = glyph_bottom_y(*n);
            float top = bottom - height;
            if (bottom < -40.0f || top > screen_height + 40.0f)
            {
                continue;
            }
            by_lane[static_cast<size_t>(n->lane)].push_back(n);
        }
        for (int lane = 0; lane < LANE_COUNT; lane++)
        {
            std::vector<Glyph*>& list = by_lane[static_cast<size_t>(lane)];
            // Sort by top Y (which is bottom - height, i.e., earlier in time)
            std::sort(list.begin(), list.end(),
                     [this](Glyph* a, Glyph* b) {
                         float top_a = glyph_bottom_y(*a) - glyph_height_px(*a, lane_width);
                         float top_b = glyph_bottom_y(*b) - glyph_height_px(*b, lane_width);
                         return top_a < top_b;
                     });
            float left_base = lane * lane_width;
            for (size_t i = 0; i < list.size();)
            {
                size_t j = i;
                float top_y0 = glyph_bottom_y(*list[i]) - glyph_height_px(*list[i], lane_width);
                while (j < list.size())
                {
                    float top_y_j = glyph_bottom_y(*list[j]) - glyph_height_px(*list[j], lane_width);
                    if (top_y_j - top_y0 > SIMULTANEOUS_NOTE_Y_TOLERANCE)
                        break;
                    j++;
                }
                std::vector<Glyph*> group(list.begin() + static_cast<std::ptrdiff_t>(i),
                                         list.begin() + static_cast<std::ptrdiff_t>(j));
                std::sort(group.begin(), group.end(),
                          [](Glyph* a, Glyph* b) { return a->instrument_slot < b->instrument_slot; });
                int instrument_count = 0;
                for (size_t g = 0; g < group.size(); g++)
                {
                    if (g == 0 || group[g]->instrument_slot != group[g - 1]->instrument_slot)
                    {
                        instrument_count++;
                    }
                }
                float slice_width = lane_width / static_cast<float>(instrument_count);
                int column = 0;
                for (size_t g = 0; g < group.size();)
                {
                    int slot = group[g]->instrument_slot;
                    float left = left_base + column * slice_width;
                    Glyph* n = group[g];
                    float glyph_height = glyph_height_px(*n, lane_width);
                    float bottom = glyph_bottom_y(*n);
                    float top = bottom - glyph_height;
                    bool missed = missed_notes.count(n) != 0;
                    Color fill = missed ? MISSED_GLYPH_COLOR : INSTRUMENT_COLORS[n->instrument_slot];
                    Color edge = Color{
                        static_cast<unsigned char>(std::min(255, fill.r + 35)),
                        static_cast<unsigned char>(std::min(255, fill.g + 50)),
                        static_cast<unsigned char>(std::min(255, fill.b + 50)), 255};
                    DrawRectangle(static_cast<int>(left), static_cast<int>(top),
                                  static_cast<int>(slice_width),
                                  static_cast<int>(glyph_height), fill);
                    DrawRectangleLinesEx(
                        Rectangle{left, top, slice_width, glyph_height}, 2.0f, edge);
                    while (g < group.size() && group[g]->instrument_slot == slot)
                    {
                        g++;
                    }
                    column++;
                }
                i = j;
            }
        }

        std::string score_text = "Score: " + std::to_string(score);
        DrawTextEx(font, score_text.c_str(), {20, 16}, 28, 1, WHITE);

        if (combo >= COMBO_DISPLAY_THRESHOLD)
        {
            int tier_index = std::min(COMBO_TIER_COUNT - 1,
                                     (combo - COMBO_DISPLAY_THRESHOLD) / COMBO_NOTES_PER_TIER);
            int size_tier = (combo - COMBO_DISPLAY_THRESHOLD) / COMBO_NOTES_PER_TIER;
            std::string combo_popup = std::string(COMBO_TIER_LABELS[tier_index]) + " " + std::to_string(combo);
            const float combo_font_size = 72.0f + size_tier * 6.0f;
            float combo_w = MeasureTextEx(font, combo_popup.c_str(), combo_font_size, 1).x;
            const float combo_margin = 24.0f;
            DrawTextEx(font, combo_popup.c_str(),
                       {screen_width - combo_w - combo_margin, combo_margin},
                       combo_font_size, 1, Color{255, 220, 100, 255});
        }

        DrawTextEx(font, "Arrows / D-pad / X Y A B / LB LT RB RT: hit when note is between the two white lines",
                  {20, upper_bar_y - 28}, 18, 1, Color{200, 200, 200, 255});

        if (game_ended)
        {
            DrawRectangle(0, 0, static_cast<int>(screen_width), static_cast<int>(screen_height),
                          Color{0, 0, 0, 200});
            const char* title_text = "Song complete!";
            const float title_font_size = 42.0f;
            float title_w = MeasureTextEx(font, title_text, title_font_size, 1).x;
            DrawTextEx(font, title_text,
                       {screen_width / 2.0f - title_w / 2.0f, screen_height / 2.0f - 80},
                       title_font_size, 1, WHITE);
            std::string final_score_text = "Final score: " + std::to_string(score);
            const float score_font_size = 32.0f;
            float score_w = MeasureTextEx(font, final_score_text.c_str(), score_font_size, 1).x;
            DrawTextEx(font, final_score_text.c_str(),
                       {screen_width / 2.0f - score_w / 2.0f, screen_height / 2.0f - 20},
                       score_font_size, 1, Color{220, 220, 255, 255});
            const char* prompt = "Press Enter to return to menu";
            const float prompt_font_size = 20.0f;
            float prompt_w = MeasureTextEx(font, prompt, prompt_font_size, 1).x;
            DrawTextEx(font, prompt,
                       {screen_width / 2.0f - prompt_w / 2.0f, screen_height / 2.0f + 40},
                       prompt_font_size, 1, Color{180, 180, 200, 255});
        }
    }
};
