#include "Datoviz_Scene_Binding.h"

#include <algorithm>
#include <array>
#include <stdexcept>
#include <unordered_set>

namespace renderive::detail {

Datoviz_Scene_Binding::Datoviz_Scene_Binding(DvzScene* scene, Extent_2D initial_extent)
    : scene_(scene) {
    if (scene_ == nullptr)
        throw std::invalid_argument("Datoviz scene binding requires a scene");
    const auto width = std::max(1U, initial_extent.width);
    const auto height = std::max(1U, initial_extent.height);
    figure_ = dvz_figure(scene_, width, height, 0);
    if (figure_ == nullptr)
        throw std::runtime_error("failed to create Datoviz figure");
    panel_ = dvz_panel_full(figure_);
    if (panel_ == nullptr)
        throw std::runtime_error("failed to create Datoviz panel");
}

bool Datoviz_Scene_Binding::apply(const Render_3D_State& state, std::uint64_t revision) {
    if (revision == applied_revision_)
        return true;
    if (!state.viewport.empty() &&
        dvz_figure_resize(figure_, state.viewport.width, state.viewport.height) != DVZ_OK)
        return false;
    if (!apply_camera(state.camera))
        return false;

    std::unordered_set<std::uint64_t> live_ids;
    live_ids.reserve(state.objects.size());
    for (const auto& object : state.objects) {
        live_ids.insert(object.id.value);
        auto [iterator, inserted] = visuals_.try_emplace(object.id.value);
        auto& binding = iterator->second;
        if (inserted) {
            binding.visual = dvz_sphere(scene_, 0);
            if (binding.visual == nullptr ||
                dvz_panel_add_visual(panel_, binding.visual, nullptr) != DVZ_OK ||
                dvz_visual_set_depth_test(binding.visual, true) != DVZ_OK)
                return false;
        }
        if (binding.applied_revision != object.revision && !apply_object(object, binding))
            return false;
    }

    for (auto iterator = visuals_.begin(); iterator != visuals_.end();) {
        if (!live_ids.contains(iterator->first)) {
            if (dvz_visual_set_visible(iterator->second.visual, false) != DVZ_OK)
                return false;
            iterator = visuals_.erase(iterator);
        } else
            ++iterator;
    }
    applied_revision_ = revision;
    return true;
}

DvzSceneFrameArtifact* Datoviz_Scene_Binding::emit(
    const DvzCapabilitySnapshot& capabilities, const DvzFramePlanEmitConfig& configuration) {
    DvzDiagnosticReport report{};
    dvz_diagnostic_report_init(&report);
    return dvz_figure_emit_frame(figure_, &capabilities, &report, &configuration);
}

bool Datoviz_Scene_Binding::apply_camera(const Camera_3D& camera) {
    DvzPanelView3DDesc description = dvz_panel_view3d_desc();
    description.view.eye[0] = camera.eye.x;
    description.view.eye[1] = camera.eye.y;
    description.view.eye[2] = camera.eye.z;
    description.view.target[0] = camera.target.x;
    description.view.target[1] = camera.target.y;
    description.view.target[2] = camera.target.z;
    description.view.up[0] = camera.up.x;
    description.view.up[1] = camera.up.y;
    description.view.up[2] = camera.up.z;
    description.projection.type = camera.projection == Camera_Projection::Perspective
                                      ? DVZ_CAMERA_PERSPECTIVE
                                      : DVZ_CAMERA_ORTHOGRAPHIC;
    description.projection.fov_y = camera.vertical_field_of_view_radians;
    description.projection.near_clip = camera.near_clip;
    description.projection.far_clip = camera.far_clip;
    description.projection.ortho_height = camera.orthographic_height;
    return dvz_panel_set_view3d_desc(panel_, &description) == DVZ_OK;
}

bool Datoviz_Scene_Binding::apply_object(
    const Object_3D_State& object, Visual_Binding& binding) {
    const std::array<float, 3> position{object.sphere.position.x, object.sphere.position.y, object.sphere.position.z};
    const std::array<std::uint8_t, 4> color{
        object.sphere.color.red, object.sphere.color.green,
        object.sphere.color.blue, object.sphere.color.alpha};
    const float radius = object.sphere.radius;
    mat4 transform{};
    for (std::size_t row = 0; row < 4; ++row) {
        for (std::size_t column = 0; column < 4; ++column)
            transform[row][column] = object.sphere.transform.values[row * 4 + column];
    }

    if (dvz_visual_set_data(binding.visual, "position", position.data(), 1) != DVZ_OK ||
        dvz_visual_set_data(binding.visual, "color", color.data(), 1) != DVZ_OK ||
        dvz_visual_set_data(binding.visual, "radius", &radius, 1) != DVZ_OK ||
        dvz_visual_set_transform(binding.visual, transform) != DVZ_OK ||
        dvz_visual_set_visible(binding.visual, object.sphere.visible) != DVZ_OK)
        return false;

    binding.applied_revision = object.revision;
    return true;
}

} // namespace renderive::detail
