#include <gtest/gtest.h>

#include "render_3D/Render_3D.h"
#include "render_3D/detail/Datoviz_Backend.h"
#include "render_3D/detail/Ordered_Backend_Shutdown.h"

#include <atomic>
#include <cstdint>
#include <exception>
#include <memory>
#include <thread>

namespace renderive {
namespace {

std::unique_ptr<Render_3D> build_or_skip(Frame_3D_Control_Mode mode) {
    try {
        Render_3D::Builder builder;
        return builder
            .frame_control(mode)
            .viewport({96, 64})
            .frame_slots(2)
            .gpu(0, false)
            .clear_color({0.02F, 0.03F, 0.04F, 1.0F})
            .build();
    } catch (const std::exception&) {
        return {};
    }
    return {};
}

bool publish_and_render(Render_3D& renderer, Frame_3D_Control_Mode mode) {
    if (!renderer.prepare_frame())
        return false;
    if (mode == Frame_3D_Control_Mode::Manual && !renderer.refresh_manual_frame())
        return false;
    return renderer.render_prepared_frame();
}

struct Backend_Harness {
    explicit Backend_Harness(const detail::Render_3D_State& state) {
        backend = std::make_unique<detail::Datoviz_Backend>(0, false, state);
        targets = backend->create_target_pool(2);
    }

    ~Backend_Harness() {
        if (backend == nullptr)
            return;
        if (targets != nullptr)
            targets->wait_all();
        detail::ordered_backend_shutdown(
            [&] { backend->destroy_runtime(); },
            [&] { targets.reset(); },
            [&] { backend->destroy_scene(); },
            [&] { backend->destroy_gpu_context(); });
    }

    std::unique_ptr<detail::Datoviz_Backend> backend;
    std::unique_ptr<detail::Frame_Target_Pool> targets;
};

class Render3DStrategyIntegration
    : public testing::TestWithParam<Frame_3D_Control_Mode> {};

TEST_P(Render3DStrategyIntegration, UsesExternalTargetsAcrossResizeGenerations) {
    const auto mode = GetParam();
    auto renderer = build_or_skip(mode);
    if (renderer == nullptr)
        GTEST_SKIP() << "Vulkan/Datoviz backend unavailable";
    ASSERT_NE(renderer, nullptr);

    Sphere_3D sphere;
    sphere.radius = 0.35F;
    sphere.color = {220, 80, 40, 255};
    const Object_3D_Id id = renderer->add_sphere(sphere);

    ASSERT_TRUE(publish_and_render(*renderer, mode));
    for (int frame = 0; frame < 2; ++frame) {
        sphere.position.x += 0.02F;
        ASSERT_TRUE(renderer->replace_sphere(id, sphere));
        ASSERT_TRUE(publish_and_render(*renderer, mode));
    }
    for (const Extent_2D extent : {Extent_2D{128, 72}, Extent_2D{80, 80}, Extent_2D{160, 90}}) {
        renderer->resize(extent);
        sphere.position.x += 0.05F;
        ASSERT_TRUE(renderer->replace_sphere(id, sphere));
        ASSERT_TRUE(publish_and_render(*renderer, mode));
    }
    renderer->wait_for_render();
}

INSTANTIATE_TEST_SUITE_P(
    AllKernelStrategies, Render3DStrategyIntegration,
    testing::Values(
        Frame_3D_Control_Mode::Low_Latency,
        Frame_3D_Control_Mode::Manual,
        Frame_3D_Control_Mode::Playback));

TEST(Render3DIntegration, BuiltObjectOutlivesBuilderConfiguration) {
    std::unique_ptr<Render_3D> renderer;
    try {
        {
            Render_3D::Builder builder;
            renderer = builder.viewport({64, 64}).frame_slots(2).build();
        }
    } catch (const std::exception& error) {
        GTEST_SKIP() << "Vulkan/Datoviz backend unavailable: " << error.what();
    }

    ASSERT_NE(renderer, nullptr);
    (void)renderer->add_sphere({});
    ASSERT_TRUE(renderer->prepare_frame());
    EXPECT_TRUE(renderer->render_prepared_frame());
}

TEST(Render3DIntegration, RepeatedCreateRenderResizeDestroyIsStable) {
    for (int iteration = 0; iteration < 3; ++iteration) {
        auto renderer = build_or_skip(Frame_3D_Control_Mode::Low_Latency);
        if (renderer == nullptr)
            GTEST_SKIP() << "Vulkan/Datoviz backend unavailable";
        ASSERT_NE(renderer, nullptr);
        (void)renderer->add_sphere({});
        ASSERT_TRUE(publish_and_render(*renderer, Frame_3D_Control_Mode::Low_Latency));
        renderer->resize({static_cast<std::uint32_t>(100 + iteration * 8), 72});
        ASSERT_TRUE(publish_and_render(*renderer, Frame_3D_Control_Mode::Low_Latency));
    }
}

TEST(Render3DIntegration, SlotTargetsRotateAndBackendRebuildsFromLogicalState) {
    detail::Render_3D_State state;
    state.viewport = {64, 64};
    std::unique_ptr<Backend_Harness> harness;
    try {
        harness = std::make_unique<Backend_Harness>(state);
    } catch (const std::exception& error) {
        GTEST_SKIP() << "Vulkan/Datoviz backend unavailable: " << error.what();
    }

    detail::Frame_Target& slot_0 = harness->targets->acquire(1, state.viewport);
    ASSERT_TRUE(slot_0.begin());
    const DvzStreamFrame slot_0_frame = slot_0.stream_frame();
    EXPECT_TRUE(slot_0_frame.command_buffer_recording);
    EXPECT_TRUE(slot_0_frame.image_borrowed);
    EXPECT_TRUE(slot_0_frame.image_view_borrowed);
    EXPECT_TRUE(slot_0_frame.command_buffer_borrowed);
    slot_0.abort();

    detail::Frame_Target& slot_1 = harness->targets->acquire(2, state.viewport);
    const DvzStreamFrame slot_1_frame = slot_1.stream_frame();
    EXPECT_NE(slot_0_frame.image, slot_1_frame.image);
    EXPECT_EQ(slot_0_frame.resource_generation, slot_1_frame.resource_generation);

    detail::Frame_Target& reused_slot_0 = harness->targets->acquire(3, state.viewport);
    const DvzStreamFrame reused_slot_0_frame = reused_slot_0.stream_frame();
    EXPECT_EQ(slot_0_frame.image, reused_slot_0_frame.image);
    EXPECT_EQ(slot_0_frame.command_buffer, reused_slot_0_frame.command_buffer);

    detail::Render_3D_Frame_Data frame{state, 1, 4};
    ASSERT_TRUE(harness->backend->render(frame, *harness->targets));

    harness->backend->destroy_runtime();
    frame.state.clear_color.red = 0.25F;
    ++frame.revision;
    ++frame.frame_sequence;
    EXPECT_TRUE(harness->backend->render(frame, *harness->targets));
}

TEST(Render3DIntegration, ConcurrentStateUpdatesAndRenderingStopBeforeDestroy) {
    auto renderer = build_or_skip(Frame_3D_Control_Mode::Low_Latency);
    if (renderer == nullptr)
        GTEST_SKIP() << "Vulkan/Datoviz backend unavailable";

    Sphere_3D sphere;
    const Object_3D_Id id = renderer->add_sphere(sphere);
    std::atomic<bool> first_frame_prepared{};
    std::atomic<bool> failed{};

    std::thread producer([&] {
        try {
            for (int update = 0; update < 6; ++update) {
                sphere.position.x = static_cast<float>(update) * 0.03F;
                if (!renderer->replace_sphere(id, sphere) || !renderer->prepare_frame()) {
                    failed.store(true, std::memory_order_release);
                    return;
                }
                if (update == 0)
                    first_frame_prepared.store(true, std::memory_order_release);
                std::this_thread::yield();
            }
        } catch (...) {
            failed.store(true, std::memory_order_release);
        }
    });

    std::thread consumer([&] {
        while (!first_frame_prepared.load(std::memory_order_acquire) &&
               !failed.load(std::memory_order_acquire))
            std::this_thread::yield();
        try {
            for (int frame = 0; frame < 6; ++frame)
                (void)renderer->render_prepared_frame();
        } catch (...) {
            failed.store(true, std::memory_order_release);
        }
    });

    producer.join();
    consumer.join();
    ASSERT_FALSE(failed.load(std::memory_order_acquire));
    ASSERT_TRUE(renderer->prepare_frame());
    ASSERT_TRUE(renderer->render_prepared_frame());
    renderer->wait_for_render();
}

} // namespace
} // namespace renderive
