#include "Datoviz_Backend.h"

#include "Datoviz_Frame_Executor.h"

#include <datoviz/drp2/stream.h>

#include <cstdio>
#include <stdexcept>

namespace renderive::detail {

Datoviz_Backend::Datoviz_Backend(
    std::uint32_t gpu_index, bool validation_enabled, const Render_3D_State& initial_state) {
    try {
        DvzGpuCtxConfig configuration = dvz_gpu_ctx_config();
        dvz_gpu_ctx_config_validation(&configuration, validation_enabled);
        dvz_gpu_ctx_config_gpu(&configuration, gpu_index);
        dvz_gpu_ctx_config_enable_canvas_extensions(&configuration, false);
        gpu_context_ = dvz_gpu_ctx(&configuration);
        if (gpu_context_ == nullptr)
            throw std::runtime_error("failed to create Datoviz GPU context");
        if (dvz_gpu_ctx_device(gpu_context_) == nullptr ||
            dvz_gpu_ctx_alloc(gpu_context_) == nullptr ||
            dvz_gpu_ctx_queue(gpu_context_, DVZ_QUEUE_MAIN) == nullptr)
            throw std::runtime_error("Datoviz GPU context does not expose required resources");

        create_runtime();
        create_scene(initial_state.viewport);
    } catch (...) {
        destroy_runtime();
        destroy_scene();
        destroy_gpu_context();
        throw;
    }
}

Datoviz_Backend::~Datoviz_Backend() {
    destroy_runtime();
    destroy_scene();
    destroy_gpu_context();
}

std::unique_ptr<Frame_Target_Pool> Datoviz_Backend::create_target_pool(std::size_t slot_count) {
    return std::make_unique<Frame_Target_Pool>(gpu_context_, slot_count);
}

bool Datoviz_Backend::render(
    const Render_3D_Frame_Data& frame, Frame_Target_Pool& targets) {
    if (frame.state.viewport.empty())
        return false;
    if (runtime_ == nullptr || scene_binding_ == nullptr) {
        if (!rebuild(frame, targets))
            return false;
    }
    if (!scene_binding_->apply(frame.state, frame.revision)) {
        std::fputs("Render_3D: failed to apply logical state to the Datoviz scene\n", stderr);
        (void)rebuild(frame, targets);
        return false;
    }

    try {
        Frame_Target& target = targets.acquire(frame.frame_sequence, frame.state.viewport);
        if (Datoviz_Frame_Executor::execute(*this, targets, target, frame))
            return true;
        target.abort();
    } catch (const std::exception& error) {
        std::fprintf(stderr, "Render_3D: backend transaction threw: %s\n", error.what());
    } catch (...) {
        std::fputs("Render_3D: backend transaction failed with an unknown error\n", stderr);
    }
    (void)rebuild(frame, targets);
    return false;
}

void Datoviz_Backend::destroy_runtime() noexcept {
    if (runtime_ != nullptr) {
        dvz_drp2_runtime_destroy(runtime_);
        runtime_ = nullptr;
    }
}

void Datoviz_Backend::destroy_scene() noexcept {
    scene_binding_.reset();
    if (scene_ != nullptr) {
        dvz_scene_destroy(scene_);
        scene_ = nullptr;
    }
}

void Datoviz_Backend::destroy_gpu_context() noexcept {
    if (gpu_context_ != nullptr) {
        dvz_gpu_ctx_destroy(gpu_context_);
        gpu_context_ = nullptr;
    }
}

void Datoviz_Backend::create_runtime() {
    DvzDrp2RuntimeConfig configuration = dvz_drp2_runtime_vklite_config(
        dvz_gpu_ctx_device(gpu_context_), dvz_gpu_ctx_alloc(gpu_context_));
    runtime_ = dvz_drp2_runtime_vklite(&configuration);
    if (runtime_ == nullptr)
        throw std::runtime_error("failed to create Datoviz DRP2 runtime");
}

void Datoviz_Backend::create_scene(Extent_2D extent) {
    scene_ = dvz_scene();
    if (scene_ == nullptr)
        throw std::runtime_error("failed to create Datoviz scene");
    const DvzCapabilitySnapshot capabilities = dvz_capability_snapshot();
    if (dvz_scene_set_capabilities(scene_, &capabilities) != DVZ_OK)
        throw std::runtime_error("failed to configure Datoviz scene capabilities");
    scene_binding_ = std::make_unique<Datoviz_Scene_Binding>(scene_, extent);
}

bool Datoviz_Backend::rebuild(
    const Render_3D_Frame_Data& frame, Frame_Target_Pool& targets) noexcept {
    targets.wait_all();
    destroy_runtime();
    targets.runtime_released_targets();
    destroy_scene();
    try {
        create_runtime();
        create_scene(frame.state.viewport);
        return scene_binding_->apply(frame.state, frame.revision);
    } catch (...) {
        destroy_runtime();
        destroy_scene();
        return false;
    }
}

DvzSceneFrameArtifact* Datoviz_Backend::emit(
    const DvzFramePlanEmitConfig& configuration) {
    const DvzCapabilitySnapshot capabilities = dvz_capability_snapshot();
    return scene_binding_->emit(capabilities, configuration);
}

bool Datoviz_Backend::attach(const DvzStreamFrame& frame) {
    return dvz_drp2_runtime_attach_frame_target(runtime_, Datoviz_Color_Target_Id, &frame);
}

bool Datoviz_Backend::execute(const DvzDrp2CommandStream& stream) {
    const DvzDrp2ValidationResult result = dvz_drp2_runtime_execute(runtime_, &stream);
    if (!result.ok) {
        const DvzDrp2Command* command = dvz_drp2_stream_get(&stream, result.command_index);
        std::fprintf(
            stderr, "Render_3D: DRP2 execution failed (code=%u command=%u type=%u count=%u)\n",
            static_cast<unsigned>(result.code), result.command_index,
            command == nullptr ? 0U : static_cast<unsigned>(dvz_drp2_command_type(command)),
            dvz_drp2_stream_count(&stream));
    }
    return result.ok;
}

} // namespace renderive::detail
