#pragma once

#include <algorithm>
#include <chrono>
#include <cstddef>
#include <limits>
#include <cstdint>

namespace renderive {

enum class Render_Partition_Mode : std::uint8_t {
    Automatic,
    Fixed
};

namespace detail {

struct Render_Partition_Range {
    std::size_t first{};
    std::size_t last{};
};

inline Render_Partition_Range render_partition_range(std::size_t size,
                                                     int index,
                                                     int count) noexcept {
    if (size == 0 || count <= 0 || index < 0 || index >= count)
        return {};
    return {size * static_cast<std::size_t>(index) / static_cast<std::size_t>(count),
            size * static_cast<std::size_t>(index + 1) / static_cast<std::size_t>(count)};
}

class Adaptive_Render_Partitioner {
public:
    [[nodiscard]] int graph_partition_count(Render_Partition_Mode mode,
                                            int configured_count,
                                             int worker_count,
                                             std::size_t work_size,
                                             std::size_t minimum_partition_size) noexcept {
        if (work_size == 0)
            return 0;
        const int useful = static_cast<int>(std::min<std::size_t>(
            static_cast<std::size_t>(std::numeric_limits<int>::max()), work_size));
        if (mode == Render_Partition_Mode::Fixed)
            return std::clamp(configured_count, 1, useful);

        const std::size_t granularity = std::max<std::size_t>(1, minimum_partition_size);
        const int work_limit = static_cast<int>(std::min<std::size_t>(
            static_cast<std::size_t>(std::numeric_limits<int>::max()),
            std::max<std::size_t>(1, work_size / granularity)));
        const int automatic_limit = std::clamp(work_limit, 1, std::max(1, worker_count));
        automatic_count_ = std::clamp(automatic_count_, 1, automatic_limit);
        return automatic_count_;
    }

    int begin(int graph_partition_count, std::size_t work_size) noexcept {
        started_at_ = Clock::now();
        if (work_size == 0 || graph_partition_count <= 0)
            return 0;
        const int useful = static_cast<int>(std::min<std::size_t>(
            static_cast<std::size_t>(std::numeric_limits<int>::max()),
            work_size));
        return std::clamp(graph_partition_count, 1, useful);
    }

    [[nodiscard]] bool finish(Render_Partition_Mode mode, int active_count,
                              std::uint64_t frame_interval_ns, int worker_count,
                              std::size_t work_size,
                              std::size_t minimum_partition_size) noexcept {
        if (mode != Render_Partition_Mode::Automatic || work_size == 0 ||
            active_count <= 0)
            return false;
        const std::size_t granularity = std::max<std::size_t>(1, minimum_partition_size);
        const int work_limit = static_cast<int>(std::min<std::size_t>(
            static_cast<std::size_t>(std::numeric_limits<int>::max()),
            std::max<std::size_t>(1, work_size / granularity)));
        const int automatic_limit = std::clamp(work_limit, 1, std::max(1, worker_count));
        const auto elapsed = Clock::now() - started_at_;
        const auto elapsed_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed);
        const auto bottleneck_threshold = frame_interval_ns == 0
            ? std::chrono::duration_cast<std::chrono::nanoseconds>(
                  std::chrono::microseconds(1500))
            : std::max(std::chrono::duration_cast<std::chrono::nanoseconds>(
                           std::chrono::microseconds(750)),
                       std::chrono::nanoseconds(frame_interval_ns / 4));
        const int previous = automatic_count_;
        const auto estimated_serial_cost = elapsed_ns * std::max(1, active_count);
        if (elapsed_ns > bottleneck_threshold && active_count < automatic_limit) {
            automatic_count_ = std::min(automatic_limit, active_count * 2);
        } else if (active_count > 1 &&
                   estimated_serial_cost < bottleneck_threshold * 3 / 5) {
            automatic_count_ = 1;
        } else {
            automatic_count_ = std::clamp(active_count, 1, automatic_limit);
        }
        return automatic_count_ != previous;
    }

private:
    using Clock = std::chrono::steady_clock;
    Clock::time_point started_at_{};
    int automatic_count_{1};
};

} // namespace detail
} // namespace renderive
