#include "Frame_Strategy_Observer.hpp"

#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstddef>
#include <mutex>
#include <utility>
#include <vector>

#include "renderive/renderable/base/Renderable_Base_p.hpp"
#include "renderive/scene/base/Scene_Base.hpp"

struct Frame_Strategy_Real_Time_Data_Observer::State {
    using Target = Renderable_Base::Impl::Real_Time_Data_State;

    struct Entry {
        explicit Entry(std::shared_ptr<Target> target)
            : target(std::move(target)) {}

        std::shared_ptr<Target> target;
        std::size_t binding_count{1};
        std::atomic<bool> active{true};
    };

    using Entry_List = std::vector<std::shared_ptr<Entry>>;

    class Binding;

    std::mutex mutex;
    std::shared_ptr<const Entry_List> entries{
        std::make_shared<Entry_List>()};
};

class Frame_Strategy_Real_Time_Data_Observer::State::Binding {
public:
    Binding(std::shared_ptr<State> state, std::shared_ptr<Entry> entry)
        : state_(std::move(state)), entry_(std::move(entry)) {}

    ~Binding() {
        std::lock_guard lock(state_->mutex);
        if (entry_->binding_count > 1) {
            --entry_->binding_count;
            return;
        }
        entry_->binding_count = 0;
        entry_->active.store(false, std::memory_order_release);
        try {
            auto entries = std::make_shared<Entry_List>();
            entries->reserve(state_->entries->size());
            for (const auto& value : *state_->entries) {
                if (value->active.load(std::memory_order_acquire))
                    entries->push_back(value);
            }
            state_->entries = std::move(entries);
        } catch (...) {}
    }

private:
    std::shared_ptr<State> state_;
    std::shared_ptr<Entry> entry_;
};

Frame_Strategy_Real_Time_Data_Observer::
    Frame_Strategy_Real_Time_Data_Observer()
    : state_(std::make_shared<State>()) {}

Frame_Strategy_Real_Time_Data_Observer::
    Frame_Strategy_Real_Time_Data_Observer(Renderable_Base& renderable)
    : Frame_Strategy_Real_Time_Data_Observer() {
    direct_binding_ = bind(renderable);
}

std::shared_ptr<void> Frame_Strategy_Real_Time_Data_Observer::bind(
    Renderable_Base& renderable) {
    const auto target = renderable.d_func().real_time_data_state;
    std::shared_ptr<State::Entry> entry;
    {
        std::lock_guard lock(state_->mutex);
        for (const auto& value : *state_->entries) {
            if (value->active.load(std::memory_order_acquire) &&
                value->target == target) {
                ++value->binding_count;
                entry = value;
                break;
            }
        }
        if (!entry) {
            entry = std::make_shared<State::Entry>(target);
            auto entries = std::make_shared<State::Entry_List>();
            entries->reserve(state_->entries->size() + 1);
            for (const auto& value : *state_->entries) {
                if (value->active.load(std::memory_order_acquire))
                    entries->push_back(value);
            }
            entries->push_back(entry);
            state_->entries = std::move(entries);
        }
    }
    return std::make_shared<State::Binding>(state_, std::move(entry));
}

void Frame_Strategy_Real_Time_Data_Observer::observe(
    const Real_Time_Data_Observation& observation) noexcept {
    std::shared_ptr<const State::Entry_List> entries;
    {
        std::lock_guard lock(state_->mutex);
        entries = state_->entries;
    }
    const auto is_attached = [](const auto& entry) {
        return entry->active.load(std::memory_order_acquire) &&
               entry->target->renderable_alive.load(
                   std::memory_order_acquire) &&
               entry->target->attached.load(std::memory_order_acquire);
    };
    for (std::size_t index = 0; index < entries->size(); ++index) {
        const auto& entry = (*entries)[index];
        if (!is_attached(entry))
            continue;
        const auto& lifetime = entry->target->scene_lifetime;
        bool first_for_scene = true;
        for (std::size_t previous = 0; previous < index; ++previous) {
            const auto& previous_entry = (*entries)[previous];
            if (is_attached(previous_entry) &&
                previous_entry->target->scene_lifetime.get() ==
                    lifetime.get()) {
                first_for_scene = false;
                break;
            }
        }
        if (!first_for_scene)
            continue;
        bool discard_stale_frame{};
        if (observation.state.retention ==
            Real_Time_Data_Retention::latest) {
            for (const auto& value : *entries) {
                if (is_attached(value) &&
                    value->target->scene_lifetime.get() == lifetime.get() &&
                    value->target
                        ->discard_stale_frame_on_latest_data_update.load(
                            std::memory_order_acquire)) {
                    discard_stale_frame = true;
                    break;
                }
            }
        }
        auto lease = lifetime->acquire();
        if (!lease)
            continue;
        auto& strategy = lease.scene().frame_control_strategy();
        strategy.on_real_time_data_update(observation);
        const double frequency_hz = strategy.frequency_hz();
        if (discard_stale_frame && std::isfinite(frequency_hz) &&
            frequency_hz > 0.0)
            strategy.discard_stale_latest_data_frame();
    }
}
