#include "Render_3D.h"

#include "detail/Datoviz_Backend.h"
#include "detail/Ordered_Backend_Shutdown.h"
#include "detail/Render_3D_Core.h"
#include "detail/Render_Domain.h"

#include <algorithm>
#include <atomic>
#include <cmath>
#include <limits>
#include <stdexcept>

namespace renderive {
namespace {

bool finite(Vec_3 value) {
    return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z);
}

void validate_camera(const Camera_3D& camera) {
    if (!finite(camera.eye) || !finite(camera.target) || !finite(camera.up) ||
        !std::isfinite(camera.vertical_field_of_view_radians) ||
        !std::isfinite(camera.near_clip) || !std::isfinite(camera.far_clip) ||
        !std::isfinite(camera.orthographic_height) || camera.near_clip <= 0.0F ||
        camera.far_clip <= camera.near_clip || camera.vertical_field_of_view_radians <= 0.0F ||
        camera.vertical_field_of_view_radians >= 3.141592654F ||
        camera.orthographic_height <= 0.0F)
        throw std::invalid_argument("invalid Render_3D camera");
}

void validate_clear_color(const Clear_Color& color) {
    const auto valid = [](float value) { return std::isfinite(value) && value >= 0.0F && value <= 1.0F; };
    if (!valid(color.red) || !valid(color.green) || !valid(color.blue) || !valid(color.alpha))
        throw std::invalid_argument("Render_3D clear color components must be in [0, 1]");
}

void validate_scale(float device_x, float device_y, float render, float user) {
    const auto valid = [](float value) { return std::isfinite(value) && value > 0.0F; };
    if (!valid(device_x) || !valid(device_y) || !valid(render) || !valid(user))
        throw std::invalid_argument("Render_3D scale values must be finite and positive");
}

void validate_sphere(const Sphere_3D& sphere) {
    if (!finite(sphere.position) || !std::isfinite(sphere.radius) || sphere.radius <= 0.0F)
        throw std::invalid_argument("invalid Render_3D sphere");
    for (const float value : sphere.transform.values) {
        if (!std::isfinite(value))
            throw std::invalid_argument("Render_3D transform contains a non-finite value");
    }
}

} // namespace

struct Render_3D::Configuration {
    Frame_3D_Control_Mode frame_control_mode{Frame_3D_Control_Mode::Low_Latency};
    double maximum_frames_per_second{60.0};
    std::size_t frame_slot_count{2};
    std::uint32_t gpu_index{};
    bool validation_enabled{};
    detail::Render_3D_State initial_state;
};

struct Render_3D::Impl {
    explicit Impl(const Configuration& configuration)
        : states(configuration.initial_state),
          scheduler(configuration.frame_control_mode, configuration.maximum_frames_per_second) {
        try {
            render_domain.invoke([&] {
                backend = std::make_unique<detail::Datoviz_Backend>(
                    configuration.gpu_index, configuration.validation_enabled,
                    configuration.initial_state);
                targets = backend->create_target_pool(configuration.frame_slot_count);
            });
        } catch (...) {
            render_domain.invoke([&] {
                targets.reset();
                backend.reset();
            });
            throw;
        }
    }

    ~Impl() {
        accepting_frames.store(false, std::memory_order_release);
        render_domain.invoke([&] {
            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(); });
            backend.reset();
        });
    }

    [[nodiscard]] bool prepare_frame() {
        return accepting_frames.load(std::memory_order_acquire) && scheduler.prepare(states);
    }

    [[nodiscard]] bool render_prepared_frame() {
        if (!accepting_frames.load(std::memory_order_acquire))
            return false;
        return render_domain.invoke([&] {
            return scheduler.render([&](const detail::Render_3D_Frame_Data& frame) {
                return backend->render(frame, *targets);
            });
        });
    }

    void wait_for_render() {
        if (!accepting_frames.load(std::memory_order_acquire))
            return;
        render_domain.invoke([&] { targets->wait_all(); });
    }

    [[nodiscard]] Object_3D_Id allocate_object_id() {
        auto value = next_object_id.load(std::memory_order_relaxed);
        for (;;) {
            if (value == 0 || value == std::numeric_limits<std::uint64_t>::max())
                throw std::overflow_error("Render_3D object identifiers are exhausted");
            if (next_object_id.compare_exchange_weak(
                    value, value + 1, std::memory_order_relaxed))
                return {value};
        }
    }

    detail::Render_Domain render_domain;
    std::unique_ptr<detail::Datoviz_Backend> backend;
    std::unique_ptr<detail::Frame_Target_Pool> targets;
    detail::Logical_State_Buffer states;
    detail::Frame_Scheduler scheduler;
    std::atomic<std::uint64_t> next_object_id{1};
    std::atomic<bool> accepting_frames{true};
};

Render_3D::Builder& Render_3D::Builder::frame_control(
    Frame_3D_Control_Mode mode, double maximum_frames_per_second) {
    frame_control_mode_ = mode;
    maximum_frames_per_second_ = maximum_frames_per_second;
    return *this;
}

Render_3D::Builder& Render_3D::Builder::viewport(Extent_2D extent) {
    viewport_ = extent;
    return *this;
}

Render_3D::Builder& Render_3D::Builder::frame_slots(std::size_t count) {
    frame_slot_count_ = count;
    return *this;
}

Render_3D::Builder& Render_3D::Builder::gpu(
    std::uint32_t index, bool validation_enabled) {
    gpu_index_ = index;
    validation_enabled_ = validation_enabled;
    return *this;
}

Render_3D::Builder& Render_3D::Builder::camera(Camera_3D value) {
    camera_ = value;
    return *this;
}

Render_3D::Builder& Render_3D::Builder::clear_color(Clear_Color value) {
    clear_color_ = value;
    return *this;
}

Render_3D::Builder& Render_3D::Builder::scale(
    float device_x, float device_y, float render, float user) {
    device_scale_x_ = device_x;
    device_scale_y_ = device_y;
    render_scale_ = render;
    user_scale_ = user;
    return *this;
}

std::unique_ptr<Render_3D> Render_3D::Builder::build() const {
    if (!std::isfinite(maximum_frames_per_second_) || maximum_frames_per_second_ <= 0.0)
        throw std::invalid_argument("Render_3D frame frequency must be finite and positive");
    if (frame_slot_count_ < 2)
        throw std::invalid_argument("Render_3D requires at least two frame slots");
    validate_camera(camera_);
    validate_clear_color(clear_color_);
    validate_scale(device_scale_x_, device_scale_y_, render_scale_, user_scale_);

    Configuration configuration;
    configuration.frame_control_mode = frame_control_mode_;
    configuration.maximum_frames_per_second = maximum_frames_per_second_;
    configuration.frame_slot_count = frame_slot_count_;
    configuration.gpu_index = gpu_index_;
    configuration.validation_enabled = validation_enabled_;
    configuration.initial_state.viewport = viewport_;
    configuration.initial_state.camera = camera_;
    configuration.initial_state.clear_color = clear_color_;
    configuration.initial_state.device_scale_x = device_scale_x_;
    configuration.initial_state.device_scale_y = device_scale_y_;
    configuration.initial_state.render_scale = render_scale_;
    configuration.initial_state.user_scale = user_scale_;
    return std::unique_ptr<Render_3D>(new Render_3D(configuration));
}

Render_3D::Render_3D(const Configuration& configuration)
    : impl_(std::make_unique<Impl>(configuration)) {}

Render_3D::~Render_3D() = default;

void Render_3D::resize(Extent_2D extent) {
    impl_->states.update([extent](detail::Render_3D_State& state) { state.viewport = extent; });
}

void Render_3D::set_camera(Camera_3D camera) {
    validate_camera(camera);
    impl_->states.update([camera](detail::Render_3D_State& state) { state.camera = camera; });
}

void Render_3D::set_clear_color(Clear_Color color) {
    validate_clear_color(color);
    impl_->states.update([color](detail::Render_3D_State& state) { state.clear_color = color; });
}

void Render_3D::set_scale(float device_x, float device_y, float render, float user) {
    validate_scale(device_x, device_y, render, user);
    impl_->states.update([=](detail::Render_3D_State& state) {
        state.device_scale_x = device_x;
        state.device_scale_y = device_y;
        state.render_scale = render;
        state.user_scale = user;
    });
}

Object_3D_Id Render_3D::add_sphere(Sphere_3D sphere) {
    validate_sphere(sphere);
    const Object_3D_Id id = impl_->allocate_object_id();
    impl_->states.update([id, sphere](detail::Render_3D_State& state) {
        state.objects.push_back({id, sphere, 1});
    });
    return id;
}

bool Render_3D::replace_sphere(Object_3D_Id id, Sphere_3D sphere) {
    if (!id)
        return false;
    validate_sphere(sphere);
    bool replaced{};
    impl_->states.update([&](detail::Render_3D_State& state) {
        const auto iterator = std::find_if(
            state.objects.begin(), state.objects.end(),
            [id](const detail::Object_3D_State& object) { return object.id == id; });
        if (iterator == state.objects.end())
            return;
        iterator->sphere = sphere;
        ++iterator->revision;
        replaced = true;
    });
    return replaced;
}

bool Render_3D::erase_object(Object_3D_Id id) {
    if (!id)
        return false;
    bool erased{};
    impl_->states.update([&](detail::Render_3D_State& state) {
        const auto iterator = std::find_if(
            state.objects.begin(), state.objects.end(),
            [id](const detail::Object_3D_State& object) { return object.id == id; });
        if (iterator == state.objects.end())
            return;
        state.objects.erase(iterator);
        erased = true;
    });
    return erased;
}

bool Render_3D::prepare_frame() {
    return impl_->prepare_frame();
}

bool Render_3D::refresh_manual_frame() {
    return impl_->accepting_frames.load(std::memory_order_acquire) && impl_->scheduler.refresh();
}

bool Render_3D::discard_pending_frame() {
    return impl_->accepting_frames.load(std::memory_order_acquire) && impl_->scheduler.discard_pending();
}

bool Render_3D::render_prepared_frame() {
    return impl_->render_prepared_frame();
}

void Render_3D::wait_for_render() {
    impl_->wait_for_render();
}

} // namespace renderive
