#include "../include/asset_mgr.hpp"
#include "include/font_writer.hpp"
#include "include/menu.hpp"
#include "include/paco.hpp"
#include "Overlay/Overlays.hpp"
#include <FontWriter/fonts.hpp>
#include <FontWriter/font_writer.hpp>
#include <PSX/Audio/CDDA.hpp>
#include <PSX/Audio/CDXA.hpp>
#include <PSX/Periphery/periphery.hpp>
#include <stdio.hpp>

using namespace JabyEngine;
using DigitalButton = Periphery::GenericController::Button;

struct CDPlayer {
    static constexpr auto MaxChannels = 2;

    uint8_t channel;
    bool    is_xa;

    static constexpr CDPlayer create() {
        return CDPlayer{.channel = 0, .is_xa = true};
    }

    void play() {
        if(this->is_xa) {
            Assets::XAAudio::play_mix();
        }

        else {
            const auto [first_track, last_track] = CDDA::get_tracks();
            CDDA::play(first_track);
        }
    }

    void stop() {
        if(this->is_xa) {
            CDXA::stop();
        }
        
        else {
            CDDA::stop();
        }
    }

    void change_channel(int8_t step) {
        if(this->is_xa) {
            this->channel = static_cast<uint8_t>((this->channel + step))%MaxChannels;
            CDXA::set_channel(this->channel);
        }
    }

    void change_audio() {
        CDPlayer::stop();
        this->is_xa = !this->is_xa;
        CDPlayer::play();
    }

    void push() {
        if(this->is_xa) {
            CDXA::push_play();
        }

        else {
            CDDA::push_play();
        }
    }

    void pop() {
        if(this->is_xa) {
            CDXA::pop_play();
        }

        else {
            CDDA::pop_play();
        }
    }
};

struct StateChange {
    void (*asset_load)();
    void (*main)();

    static constexpr StateChange empty() {
        return StateChange{.asset_load = nullptr, .main = nullptr};
    }

    void clear() {
        this->asset_load = nullptr;
        this->main       = nullptr;
    }

    bool contains_state() const {
        return this->main;
    }

    auto operator<=>(const StateChange&) const = default;
};

static const Menu::SimpleMenu::Entry MenuEntries[] = {
    {"Controller Test"},
    {"GPU Test"},
    {"GTE Test"},
    {"Font Cycler"},
    {"Screen Center"},
    {"BIOS Information"}
};

static const auto doener_fish = Make::SPRT(
    Make::AreaI16(Make::PositionI16(8, GPU::Display::Height - Assets::Main::DoenerFishInfo.size.height), Assets::Main::DoenerFishInfo.size), //         v this needs to be nicer! Has to be
    Make::OffsetPageWithClut(Assets::Main::DoenerFishInfo.tim.get_page_offset_clut4(), Make::PageClut(Assets::Main::DoenerFishInfo.tim.get_clut_position())),
    GPU::Color24::Grey()
);

static CDPlayer         cd_player = CDPlayer::create();
static object::Paco     paco;
static Menu::SimpleMenu menu;
static StateChange      state_changer;
static StateChange      old_state_changer;

namespace Shared {
    Menu::BackMenu           back_menu;
    JabyEngine::GPU::POLY_G4 background = Make::POLY_G4(
        Make::AreaI16(0, 0, GPU::Display::Width, GPU::Display::Height),
        {GPU::Color24::Red(0xA0), GPU::Color24::Green(0xA0), GPU::Color24::Blue(0xA0), GPU::Color24::Black()}
    );
    bool                     load_test = false;
}

static void setup() {
    Assets::Main::load();
    FontWriter::setup();
    paco.setup();
    Shared::back_menu.setup(&FontWriter::bios_font_writer);
    
    menu.setup([](uint32_t selection) {
        switch(selection) {
            case 0:
                state_changer.asset_load = Assets::Overlay::load_controller_test;
                state_changer.main       = ControllerTest::main;
                break;

            case 1:
                state_changer.asset_load = Assets::Overlay::load_gpu_test;
                state_changer.main       = GPUTest::main;
                break;

            case 2:
                state_changer.asset_load = Assets::Overlay::load_gte_test;
                state_changer.main       = GTETest::main;
                break;

            case 3:
                state_changer.asset_load = Assets::Overlay::load_font_cycler;
                state_changer.main       = FontCycler::main;
                break;

            case 4:
                state_changer.asset_load = Assets::Overlay::load_screen_center;
                state_changer.main       = ScreenCenter::main;
                break;

            case 5:
                state_changer.asset_load = Assets::Overlay::load_bios_info;
                state_changer.main       = BIOSInfo::main;
                break;
        }
    },MenuEntries);
    cd_player.play();
}

namespace NormalScene {
    static void update() {
        static const char     Title[]       = ">> Pool Box <<";
        static const char     Version[]     = "Ver. 0.9.0";
        static constexpr auto TitleLength   = DefaultFont::Info.estimate_str_render_length(Title);
        static constexpr auto VersionLength = DefaultFont::Info.estimate_str_render_length(Version);

        Periphery::query_controller();
        const auto& controller = Periphery::get_primary_controller_as<JabyEngine::Periphery::GenericController>();

        if(controller.is_connected()) {
            if(controller.button.went_down(DigitalButton::SEL)) {
                cd_player.change_audio();
            }

            if(controller.button.went_down(DigitalButton::R1)) {
                cd_player.change_channel(1);
            }

            if(controller.button.went_down(DigitalButton::L1)) {
                cd_player.change_channel(-1);
            }

            // Trigger load test
            if(controller.button.is_down(DigitalButton::R2) && controller.button.is_down(DigitalButton::L2) && controller.button.is_down(DigitalButton::ST)) {
                Shared::load_test = true;
            }
        }

        auto cursor = FontWriter::update(Make::PositionI16((GPU::Display::Width-TitleLength)/2, 16));
        paco.update();

        FontWriter::new_font_writer.write(cursor, Title, GPU::Color24::Yellow(0xD0), &FontWriter::wiggle);
        FontWriter::new_font_writer.write(cursor.change_position(Make::PositionI16((GPU::Display::Width-VersionLength)/2, 16 + DefaultFont::Info.get_kern_size().height)), Version, GPU::Color24::Green(0xD0), &FontWriter::wiggle);
        menu.update(FontWriter::bios_font_writer, cursor, Make::PositionI16(8, 64));

        cursor.change_position(Make::PositionI16(doener_fish.position.x + doener_fish.size.width, GPU::Display::Height - 48));
        FontWriter::bios_font_writer.write(cursor, "Audio:\n%s\n(SEL/R1/R2)", cd_player.is_xa ? "CD-XA" : "CD-DA");

        if(Shared::load_test) {
            // Force state change if we are in the load_test state
            state_changer.asset_load = Assets::Overlay::load_large_gpu_test;
            state_changer.main       = GPUTest::main;
        }
    }

    static void render() {
        GPU::render(Shared::background);
        FontWriter::new_font_writer.render();
        FontWriter::bios_font_writer.render();
        paco.render();
        GPU::render(doener_fish);
    }

    static void run() {
        update();
        GPU::swap_buffers_vsync(1);
        render();  
    }
}

namespace LoadingScene {
    static SimpleTimer<uint8_t> jaby_timer;
    static uint8_t              jaby_frame_offset;

    static void update() {
        jaby_timer.reset();
        jaby_frame_offset = 0;
    }

    static void vsync_render() {
        static constexpr auto StartPosition = Make::PositionI16(24, 64);
        const auto load_font = Make::SPRT(
            Make::AreaI16(StartPosition.move(Assets::Main::JabyLoader::JabyFrame.size.width + 8, 0), Assets::Main::JabyLoader::FontFrame.size),
            Make::OffsetPageWithClut(Assets::Main::JabyLoader::TIMLoaction.get_page_offset_clut4().move(Assets::Main::JabyLoader::FontFrame.position.x, Assets::Main::JabyLoader::FontFrame.position.y), Make::PageClut(Assets::Main::JabyLoader::TIMLoaction.get_clut_position())),
            GPU::Color24::Grey()
        );
        auto jaby_sprt = Make::SPRT(
            Make::AreaI16(StartPosition, Assets::Main::JabyLoader::JabyFrame.size),
            Make::OffsetPageWithClut(Assets::Main::JabyLoader::TIMLoaction.get_page_offset_clut4(), Make::PageClut(Assets::Main::JabyLoader::TIMLoaction.get_clut_position())),
            GPU::Color24::Grey()
        );

        if(jaby_timer.is_expired_for(500_ms)) {
            jaby_frame_offset = jaby_frame_offset ? 0 : 32;
            jaby_timer.reset();
        }
        jaby_sprt.tex_offset.add(jaby_frame_offset, 0);

        GPU::swap_buffers(!Shared::load_test);
        GPU::render(jaby_sprt);
        GPU::render(load_font);
        jaby_sprt.position.move(Assets::Main::JabyLoader::FontFrame.size.width + Assets::Main::JabyLoader::JabyFrame.size.width + 8, 0); 
        GPU::render(jaby_sprt);
    }

    static void run() {
        if(Shared::load_test || old_state_changer != state_changer) {
            GPU::set_vsync_callback(vsync_render);

            cd_player.push();
            state_changer.asset_load();
            old_state_changer = state_changer;
            cd_player.pop();

            GPU::set_vsync_callback(nullptr);
        }

        state_changer.main();
        state_changer.clear();
    }
}

void main() {
    const auto& controller = Periphery::get_primary_controller_as<JabyEngine::Periphery::GenericController>();
    
    setup();
    while(true) {
        if(state_changer.contains_state()) {
            LoadingScene::run();
        }
        
        else {
            NormalScene::run();
        }
    }
}