#include <cstdio>
#include <catch2/catch_template_test_macros.hpp>
#include "audio_core/hle/shared_memory.h"
#include "common/settings.h"
#include "tests/audio_core/merryhime_3ds_audio/merry_audio/merry_audio.h"

TEST_CASE_METHOD(MerryAudio::MerryAudioFixture, "Verify SourceStatus::Status::last_buffer_id 1",
                 "[audio_core][hle]") {
    //  World's worst triangle wave generator.
    //  Generates PCM16.
    auto fillBuffer = [this](u32* audio_buffer, size_t size, unsigned freq) {
        for (size_t i = 0; i < size; i++) {
            u32 data = (i % freq) * 256;
            audio_buffer[i] = (data << 16) | (data & 0xFFFF);
        }

        DSP_FlushDataCache(audio_buffer, size);
    };

    constexpr size_t NUM_SAMPLES = 160 * 1;
    u32* audio_buffer = (u32*)linearAlloc(NUM_SAMPLES * sizeof(u32));
    fillBuffer(audio_buffer, NUM_SAMPLES, 160);
    u32* audio_buffer2 = (u32*)linearAlloc(NUM_SAMPLES * sizeof(u32));
    fillBuffer(audio_buffer2, NUM_SAMPLES, 80);
    u32* audio_buffer3 = (u32*)linearAlloc(NUM_SAMPLES * sizeof(u32));
    fillBuffer(audio_buffer3, NUM_SAMPLES, 40);

    MerryAudio::AudioState state;
    {
        std::vector<u8> dspfirm;
        SECTION("HLE") {
            // The test case assumes HLE AudioCore doesn't require a valid firmware
            InitDspCore(Settings::AudioEmulation::HLE);
            dspfirm = {0};
        }
        SECTION("LLE Sanity") {
            InitDspCore(Settings::AudioEmulation::LLE);
            dspfirm = loadDspFirmFromFile();
        }
        if (!dspfirm.size()) {
            SKIP("Couldn't load firmware\n");
            return;
        }
        auto ret = audioInit(dspfirm);
        if (!ret) {
            INFO("Couldn't init audio\n");
            goto end;
        }
        state = *ret;
    }

    state.waitForSync();
    initSharedMem(state);
    state.notifyDsp();

    state.waitForSync();
    state.notifyDsp();
    state.waitForSync();
    state.notifyDsp();
    state.waitForSync();
    state.notifyDsp();
    state.waitForSync();
    state.notifyDsp();

    {
        u16 buffer_id = 0;
        size_t next_queue_position = 0;

        state.write().source_configurations->config[0].play_position = 0;
        state.write().source_configurations->config[0].physical_address =
            osConvertVirtToPhys(audio_buffer3);
        state.write().source_configurations->config[0].length = NUM_SAMPLES;
        state.write().source_configurations->config[0].mono_or_stereo.Assign(
            AudioCore::HLE::SourceConfiguration::Configuration::MonoOrStereo::Stereo);
        state.write().source_configurations->config[0].format.Assign(
            AudioCore::HLE::SourceConfiguration::Configuration::Format::PCM16);
        state.write().source_configurations->config[0].fade_in.Assign(false);
        state.write().source_configurations->config[0].adpcm_dirty.Assign(false);
        state.write().source_configurations->config[0].is_looping.Assign(false);
        state.write().source_configurations->config[0].buffer_id = ++buffer_id;
        state.write().source_configurations->config[0].partial_reset_flag.Assign(true);
        state.write().source_configurations->config[0].play_position_dirty.Assign(true);
        state.write().source_configurations->config[0].embedded_buffer_dirty.Assign(true);

        state.write()
            .source_configurations->config[0]
            .buffers[next_queue_position]
            .physical_address = osConvertVirtToPhys(buffer_id % 2 ? audio_buffer2 : audio_buffer);
        state.write().source_configurations->config[0].buffers[next_queue_position].length =
            NUM_SAMPLES;
        state.write().source_configurations->config[0].buffers[next_queue_position].adpcm_dirty =
            false;
        state.write().source_configurations->config[0].buffers[next_queue_position].is_looping =
            false;
        state.write().source_configurations->config[0].buffers[next_queue_position].buffer_id =
            ++buffer_id;
        state.write().source_configurations->config[0].buffers_dirty |= 1 << next_queue_position;
        next_queue_position = (next_queue_position + 1) % 4;
        state.write().source_configurations->config[0].buffer_queue_dirty.Assign(true);
        state.write().source_configurations->config[0].enable = true;
        state.write().source_configurations->config[0].enable_dirty.Assign(true);

        state.notifyDsp();

        for (size_t frame_count = 0; frame_count < 10; frame_count++) {
            state.waitForSync();
            if (!state.read().source_statuses->status[0].is_enabled) {
                state.write().source_configurations->config[0].enable = true;
                state.write().source_configurations->config[0].enable_dirty.Assign(true);
            }

            if (state.read().source_statuses->status[0].current_buffer_id_dirty) {
                if (state.read().source_statuses->status[0].current_buffer_id == buffer_id ||
                    state.read().source_statuses->status[0].current_buffer_id == 0) {
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .physical_address =
                        osConvertVirtToPhys(buffer_id % 2 ? audio_buffer2 : audio_buffer);
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .length = NUM_SAMPLES;
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .adpcm_dirty = false;
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .is_looping = false;
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .buffer_id = ++buffer_id;
                    state.write().source_configurations->config[0].buffers_dirty |=
                        1 << next_queue_position;
                    next_queue_position = (next_queue_position + 1) % 4;
                    state.write().source_configurations->config[0].buffer_queue_dirty.Assign(true);
                }
            }

            state.notifyDsp();
        }

        // current_buffer_id should be 0 if the queue is not empty
        REQUIRE(state.read().source_statuses->status[0].last_buffer_id == 0);

        // Let the queue finish playing
        for (size_t frame_count = 0; frame_count < 10; frame_count++) {
            state.waitForSync();
            state.notifyDsp();
        }

        // TODO: There seems to be some nuances with how the LLE firmware runs the buffer queue,
        // that differs from the HLE implementation
        // REQUIRE(state.read().source_statuses->status[0].last_buffer_id == 5);

        // current_buffer_id should be equal to buffer_id once the queue is empty
        REQUIRE(state.read().source_statuses->status[0].last_buffer_id == buffer_id);
    }

end:
    audioExit(state);
}

TEST_CASE_METHOD(MerryAudio::MerryAudioFixture, "Verify SourceStatus::Status::last_buffer_id 2",
                 "[audio_core][hle]") {
    //  World's worst triangle wave generator.
    //  Generates PCM16.
    auto fillBuffer = [this](u32* audio_buffer, size_t size, unsigned freq) {
        for (size_t i = 0; i < size; i++) {
            u32 data = (i % freq) * 256;
            audio_buffer[i] = (data << 16) | (data & 0xFFFF);
        }

        DSP_FlushDataCache(audio_buffer, size);
    };

    constexpr size_t NUM_SAMPLES = 160 * 1;
    u32* audio_buffer = (u32*)linearAlloc(NUM_SAMPLES * sizeof(u32));
    fillBuffer(audio_buffer, NUM_SAMPLES, 160);
    u32* audio_buffer2 = (u32*)linearAlloc(NUM_SAMPLES * sizeof(u32));
    fillBuffer(audio_buffer2, NUM_SAMPLES, 80);
    u32* audio_buffer3 = (u32*)linearAlloc(NUM_SAMPLES * sizeof(u32));
    fillBuffer(audio_buffer3, NUM_SAMPLES, 40);

    MerryAudio::AudioState state;
    {
        std::vector<u8> dspfirm;
        SECTION("HLE") {
            // The test case assumes HLE AudioCore doesn't require a valid firmware
            InitDspCore(Settings::AudioEmulation::HLE);
            dspfirm = {0};
        }
        SECTION("LLE Sanity") {
            InitDspCore(Settings::AudioEmulation::LLE);
            dspfirm = loadDspFirmFromFile();
        }
        if (!dspfirm.size()) {
            SKIP("Couldn't load firmware\n");
            return;
        }
        auto ret = audioInit(dspfirm);
        if (!ret) {
            INFO("Couldn't init audio\n");
            goto end;
        }
        state = *ret;
    }

    state.waitForSync();
    initSharedMem(state);
    state.notifyDsp();

    state.waitForSync();
    state.notifyDsp();
    state.waitForSync();
    state.notifyDsp();
    state.waitForSync();
    state.notifyDsp();
    state.waitForSync();
    state.notifyDsp();

    {
        u16 buffer_id = 0;
        size_t next_queue_position = 0;

        state.write().source_configurations->config[0].play_position = 0;
        state.write().source_configurations->config[0].physical_address =
            osConvertVirtToPhys(audio_buffer3);
        state.write().source_configurations->config[0].length = NUM_SAMPLES;
        state.write().source_configurations->config[0].mono_or_stereo.Assign(
            AudioCore::HLE::SourceConfiguration::Configuration::MonoOrStereo::Stereo);
        state.write().source_configurations->config[0].format.Assign(
            AudioCore::HLE::SourceConfiguration::Configuration::Format::PCM16);
        state.write().source_configurations->config[0].fade_in.Assign(false);
        state.write().source_configurations->config[0].adpcm_dirty.Assign(false);
        state.write().source_configurations->config[0].is_looping.Assign(false);
        state.write().source_configurations->config[0].buffer_id = ++buffer_id;
        state.write().source_configurations->config[0].partial_reset_flag.Assign(true);
        state.write().source_configurations->config[0].play_position_dirty.Assign(true);
        state.write().source_configurations->config[0].embedded_buffer_dirty.Assign(true);

        state.write()
            .source_configurations->config[0]
            .buffers[next_queue_position]
            .physical_address = osConvertVirtToPhys(buffer_id % 2 ? audio_buffer2 : audio_buffer);
        state.write().source_configurations->config[0].buffers[next_queue_position].length =
            NUM_SAMPLES;
        state.write().source_configurations->config[0].buffers[next_queue_position].adpcm_dirty =
            false;
        state.write().source_configurations->config[0].buffers[next_queue_position].is_looping =
            false;
        state.write().source_configurations->config[0].buffers[next_queue_position].buffer_id =
            ++buffer_id;
        state.write().source_configurations->config[0].buffers_dirty |= 1 << next_queue_position;
        next_queue_position = (next_queue_position + 1) % 4;
        state.write().source_configurations->config[0].buffer_queue_dirty.Assign(true);
        state.write().source_configurations->config[0].enable = true;
        state.write().source_configurations->config[0].enable_dirty.Assign(true);

        state.notifyDsp();

        for (size_t frame_count = 0; frame_count < 10; frame_count++) {
            state.waitForSync();
            if (!state.read().source_statuses->status[0].is_enabled) {
                state.write().source_configurations->config[0].enable = true;
                state.write().source_configurations->config[0].enable_dirty.Assign(true);
            }

            if (state.read().source_statuses->status[0].current_buffer_id_dirty) {
                if (state.read().source_statuses->status[0].current_buffer_id == buffer_id ||
                    state.read().source_statuses->status[0].current_buffer_id == 0) {
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .physical_address =
                        osConvertVirtToPhys(buffer_id % 2 ? audio_buffer2 : audio_buffer);
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .length = NUM_SAMPLES;
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .adpcm_dirty = false;
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .is_looping = false;
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .buffer_id = ++buffer_id;
                    state.write().source_configurations->config[0].buffers_dirty |=
                        1 << next_queue_position;
                    next_queue_position = (next_queue_position + 1) % 4;
                    state.write().source_configurations->config[0].buffer_queue_dirty.Assign(true);
                }
            }

            state.notifyDsp();
        }

        // current_buffer_id should be 0 if the queue is not empty
        REQUIRE(state.read().source_statuses->status[0].last_buffer_id == 0);

        // Let the queue finish playing
        for (size_t frame_count = 0; frame_count < 10; frame_count++) {
            state.waitForSync();
            state.notifyDsp();
        }

        // TODO: There seems to be some nuances with how the LLE firmware runs the buffer queue,
        // that differs from the HLE implementation
        // REQUIRE(state.read().source_statuses->status[0].last_buffer_id == 5);

        // current_buffer_id should be equal to buffer_id once the queue is empty
        REQUIRE(state.read().source_statuses->status[0].last_buffer_id == buffer_id);

        // Restart Playing
        for (size_t frame_count = 0; frame_count < 10; frame_count++) {
            state.waitForSync();
            if (!state.read().source_statuses->status[0].is_enabled) {
                state.write().source_configurations->config[0].enable = true;
                state.write().source_configurations->config[0].enable_dirty.Assign(true);
            }

            if (state.read().source_statuses->status[0].current_buffer_id_dirty) {
                if (state.read().source_statuses->status[0].current_buffer_id == buffer_id ||
                    state.read().source_statuses->status[0].current_buffer_id == 0) {
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .physical_address =
                        osConvertVirtToPhys(buffer_id % 2 ? audio_buffer2 : audio_buffer);
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .length = NUM_SAMPLES;
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .adpcm_dirty = false;
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .is_looping = false;
                    state.write()
                        .source_configurations->config[0]
                        .buffers[next_queue_position]
                        .buffer_id = ++buffer_id;
                    state.write().source_configurations->config[0].buffers_dirty |=
                        1 << next_queue_position;
                    next_queue_position = (next_queue_position + 1) % 4;
                    state.write().source_configurations->config[0].buffer_queue_dirty.Assign(true);
                }
            }

            state.notifyDsp();
        }

        // current_buffer_id should be 0 if the queue is not empty
        REQUIRE(state.read().source_statuses->status[0].last_buffer_id == 0);

        // Let the queue finish playing
        for (size_t frame_count = 0; frame_count < 10; frame_count++) {
            state.waitForSync();
            state.notifyDsp();
        }

        // current_buffer_id should be equal to buffer_id once the queue is empty
        REQUIRE(state.read().source_statuses->status[0].last_buffer_id == buffer_id);
    }

end:
    audioExit(state);
}
