#pragma once

#include "render_2D/renderable/Renderable.h"
#include <renderive/scene/base/Scene_Base.hpp>

#include <nlohmann/json.hpp>

#include <algorithm>
#include <array>
#include <cmath>
#include <map>
#include <numeric>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>

namespace renderive::web {

namespace gallery_capture_detail {

using Json = nlohmann::json;

inline const char* node_kind(Render_Node_Kind kind) noexcept {
    switch (kind) {
    case Render_Node_Kind::prepare:
        return "prepare";
    case Render_Node_Kind::paint:
        return "paint";
    case Render_Node_Kind::composite:
        return "composite";
    }
    return "prepare";
}

inline const char* execution_status(Node_Execution_Status status) noexcept {
    switch (status) {
    case Node_Execution_Status::pending:
        return "pending";
    case Node_Execution_Status::running:
        return "running";
    case Node_Execution_Status::complete:
        return "complete";
    case Node_Execution_Status::failed:
        return "failed";
    }
    return "pending";
}

inline Json metrics_json(const Node_Execution_Metrics& metrics) {
    Json result = Json::object();
    constexpr std::array names{"input_count", "chunk_size", "prepared_cells",
                               "pixel_count", "primitive_count"};
    for (std::size_t index = 0; index < names.size(); ++index) {
        const auto kind = static_cast<Node_Metric_Kind>(index);
        if (metrics.contains(kind))
            result[names[index]] = metrics.get(kind);
    }
    return result;
}

inline std::unordered_map<std::uint64_t, std::string> owner_names(
    const Scene_Base& scene) {
    std::unordered_map<std::uint64_t, std::string> result;
    const auto topology = scene.topology_snapshot();
    result.reserve(topology.renderables.size());
    for (const auto& base : topology.renderables) {
        std::string name;
        if (const auto* renderable = dynamic_cast<const renderive::Renderable*>(base.get()))
            name = renderable->object_name();
        if (name.empty())
            name = "Renderable " + std::to_string(base->renderable_id());
        result.emplace(base->renderable_id(), std::move(name));
    }
    return result;
}

inline Json plan_json(const Scene_Base& scene, const Render_Plan& plan) {
    const auto names = owner_names(scene);
    Json nodes = Json::array();
    Json edges = Json::array();
    std::map<std::uint64_t, std::pair<bool, bool>> active_stages;
    std::set<std::uint64_t> owners;
    for (const auto& node : plan.graph.nodes) {
        const std::string owner = node.owner_id == 0
            ? "Scene"
            : names.contains(node.owner_id)
                ? names.at(node.owner_id)
                : "Renderable " + std::to_string(node.owner_id);
        nodes.push_back({
            {"id", std::to_string(node.node_id)},
            {"node_id", node.node_id},
            {"owner_id", node.owner_id},
            {"owner", owner},
            {"label", owner + " / " + node.name},
            {"name", node.name},
            {"kind", node_kind(node.kind)},
            {"execution_index", node.execution_index}
        });
        if (node.owner_id != 0) {
            owners.insert(node.owner_id);
            auto& stages = active_stages[node.owner_id];
            stages.first = stages.first || node.kind == Render_Node_Kind::prepare;
            stages.second = stages.second || node.kind == Render_Node_Kind::paint;
        }
    }
    for (const auto& edge : plan.graph.edges) {
        edges.push_back({{"from", std::to_string(edge.from)},
                         {"to", std::to_string(edge.to)},
                         {"kind", "dependency"}});
    }
    Json renderables = Json::array();
    for (const std::uint64_t owner_id : owners) {
        const auto stages = active_stages.find(owner_id);
        const bool has_prepare = stages != active_stages.end() && stages->second.first;
        const bool has_paint = stages != active_stages.end() && stages->second.second;
        renderables.push_back({
            {"owner_id", owner_id},
            {"name", names.contains(owner_id)
                ? names.at(owner_id)
                : "Renderable " + std::to_string(owner_id)},
            {"prepare_cache", has_prepare ? "MISS" : "HIT"},
            {"paint_cache", has_paint ? "MISS" : "HIT"},
            {"prepare_in_plan", has_prepare},
            {"paint_in_plan", has_paint}
        });
    }
    return {{"version", plan.version},
            {"topology_id", "plan-" + std::to_string(plan.version)},
            {"nodes", std::move(nodes)},
            {"edges", std::move(edges)},
            {"renderables", std::move(renderables)}};
}

inline Json execution_json(const Node_Execution& execution,
                           const Frame_Snapshot& frame) {
    return {
        {"node_id", execution.node_id},
        {"start_time_ns", execution.start_time_ns},
        {"end_time_ns", execution.end_time_ns},
        {"start_offset_ns", execution.start_time_ns >= frame.render_start_ns
            ? execution.start_time_ns - frame.render_start_ns : 0},
        {"end_offset_ns", execution.end_time_ns >= frame.render_start_ns
            ? execution.end_time_ns - frame.render_start_ns : 0},
        {"duration_ns", execution.duration_ns()},
        {"worker_id", execution.worker_id},
        {"status", execution_status(execution.status)},
        {"metrics", metrics_json(execution.metrics)}
    };
}

inline Json node_analysis_json(const Node_Frame_Analysis& node) {
    return {
        {"node_id", node.node_id},
        {"duration_ns", node.duration_ns},
        {"start_offset_ns", node.start_offset_ns},
        {"end_offset_ns", node.end_offset_ns},
        {"dependency_ready_time_ns", node.dependency_ready_time_ns},
        {"scheduler_wait_ns", node.scheduler_wait_ns},
        {"work_contribution", node.work_contribution},
        {"critical_path_contribution", node.critical_path_contribution},
        {"on_critical_path", node.on_critical_path}
    };
}

inline Json frame_json(const Captured_Frame& frame) {
    Json executions = Json::array();
    for (const auto& execution : frame.snapshot->node_executions)
        executions.push_back(execution_json(execution, *frame.snapshot));
    Json nodes = Json::array();
    for (const auto& node : frame.analysis.nodes)
        nodes.push_back(node_analysis_json(node));
    Json workers = Json::array();
    for (const auto& worker : frame.analysis.workers) {
        workers.push_back({{"worker_id", worker.worker_id},
                           {"work_duration_ns", worker.work_duration_ns},
                           {"utilization", worker.utilization}});
    }
    return {
        {"frame_id", frame.snapshot->frame_id},
        {"render_plan_version", frame.snapshot->render_plan_version},
        {"render_start_ns", frame.snapshot->render_start_ns},
        {"render_end_ns", frame.snapshot->render_end_ns},
        {"render_duration_ns", frame.snapshot->render_duration_ns()},
        {"node_executions", std::move(executions)},
        {"analysis", {
            {"total_render_duration_ns", frame.analysis.total_render_duration_ns},
            {"critical_path_duration_ns", frame.analysis.critical_path_duration_ns},
            {"total_work_duration_ns", frame.analysis.total_work_duration_ns},
            {"parallel_overlap_ns", frame.analysis.parallel_overlap_ns},
            {"peak_parallelism", frame.analysis.peak_parallelism},
            {"average_parallelism", frame.analysis.average_parallelism},
            {"critical_path", frame.analysis.critical_path},
            {"bottleneck_nodes", frame.analysis.bottleneck_nodes},
            {"nodes", std::move(nodes)},
            {"workers", std::move(workers)}
        }}
    };
}

inline Json node_statistics_json(const Node_Statistics& statistics) {
    return {
        {"node_id", statistics.node_id},
        {"execution_count", statistics.execution_count},
        {"average_ns", statistics.average_ns},
        {"moving_average_ns", statistics.moving_average_ns},
        {"p50_ns", statistics.p50_ns},
        {"p95_ns", statistics.p95_ns},
        {"p99_ns", statistics.p99_ns},
        {"minimum_ns", statistics.minimum_ns},
        {"maximum_ns", statistics.maximum_ns},
        {"critical_path_frequency", statistics.critical_path_frequency},
        {"average_scheduler_wait_ns", statistics.average_scheduler_wait_ns}
    };
}

inline Json plan_statistics_json(const Plan_Version_Statistics& statistics) {
    Json nodes = Json::array();
    for (const auto& node : statistics.nodes)
        nodes.push_back(node_statistics_json(node));
    return {
        {"render_plan_version", statistics.render_plan_version},
        {"frame_count", statistics.frame_count},
        {"render_average_ns", statistics.render_average_ns},
        {"render_p50_ns", statistics.render_p50_ns},
        {"render_p95_ns", statistics.render_p95_ns},
        {"render_maximum_ns", statistics.render_maximum_ns},
        {"average_parallelism", statistics.average_parallelism},
        {"peak_parallelism", statistics.peak_parallelism},
        {"scheduler_wait_average_ns", statistics.scheduler_wait_average_ns},
        {"scheduler_wait_p95_ns", statistics.scheduler_wait_p95_ns},
        {"nodes", std::move(nodes)}
    };
}

inline double percentile(std::vector<std::uint64_t> values, double quantile) {
    if (values.empty())
        return 0.0;
    std::sort(values.begin(), values.end());
    const double position = quantile * static_cast<double>(values.size() - 1);
    const auto lower = static_cast<std::size_t>(std::floor(position));
    const auto upper = static_cast<std::size_t>(std::ceil(position));
    const double fraction = position - static_cast<double>(lower);
    return static_cast<double>(values[lower]) * (1.0 - fraction) +
           static_cast<double>(values[upper]) * fraction;
}

inline Json session_summary_json(const Capture_Session& session) {
    std::vector<std::uint64_t> durations;
    std::vector<std::uint64_t> waits;
    std::map<Render_Node_Id, std::size_t> critical_frequency;
    double parallelism{};
    std::size_t peak{};
    for (const auto& frame : session.frames) {
        durations.push_back(frame.analysis.total_render_duration_ns);
        parallelism += frame.analysis.average_parallelism;
        peak = std::max(peak, frame.analysis.peak_parallelism);
        for (const auto& node : frame.analysis.nodes)
            waits.push_back(node.scheduler_wait_ns);
        for (Render_Node_Id node_id : frame.analysis.critical_path)
            ++critical_frequency[node_id];
    }
    const double duration_sum = std::accumulate(
        durations.begin(), durations.end(), 0.0);
    const double wait_sum = std::accumulate(waits.begin(), waits.end(), 0.0);
    Json critical = Json::array();
    for (const auto& [node_id, frequency] : critical_frequency)
        critical.push_back({{"node_id", node_id}, {"frequency", frequency}});
    return {
        {"render_average_ns", durations.empty() ? 0.0 : duration_sum / durations.size()},
        {"render_p50_ns", percentile(durations, 0.50)},
        {"render_p95_ns", percentile(durations, 0.95)},
        {"render_maximum_ns", durations.empty() ? 0 : *std::max_element(durations.begin(), durations.end())},
        {"average_parallelism", session.frames.empty() ? 0.0 : parallelism / session.frames.size()},
        {"peak_parallelism", peak},
        {"scheduler_wait_average_ns", waits.empty() ? 0.0 : wait_sum / waits.size()},
        {"scheduler_wait_p95_ns", percentile(waits, 0.95)},
        {"critical_path_frequency", std::move(critical)}
    };
}

inline Json plan_difference_json(const Render_Plan_Difference& difference) {
    Json added_edges = Json::array();
    Json removed_edges = Json::array();
    for (const auto& edge : difference.added_edges)
        added_edges.push_back({{"from", edge.from}, {"to", edge.to}});
    for (const auto& edge : difference.removed_edges)
        removed_edges.push_back({{"from", edge.from}, {"to", edge.to}});
    return {{"added_nodes", difference.added_nodes},
            {"removed_nodes", difference.removed_nodes},
            {"added_edges", std::move(added_edges)},
            {"removed_edges", std::move(removed_edges)}};
}

} // namespace gallery_capture_detail

inline nlohmann::json gallery_render_plan_json(const Scene_Base& scene) {
    const auto plan = scene.render_plan_snapshot();
    return plan ? gallery_capture_detail::plan_json(scene, *plan)
                : nlohmann::json{{"version", 0}, {"nodes", nlohmann::json::array()},
                                 {"edges", nlohmann::json::array()},
                                 {"renderables", nlohmann::json::array()}};
}

inline nlohmann::json gallery_performance_capture_json(const Scene_Base& scene) {
    using namespace gallery_capture_detail;
    const auto controller = scene.capture_state();
    Json sessions = Json::array();
    std::set<Render_Plan_Version> referenced_plans;
    for (Capture_Session_Id id : scene.capture_sessions()) {
        const auto session = scene.capture_session(id);
        if (!session)
            continue;
        Json frames = Json::array();
        for (const auto& frame : session->frames) {
            frames.push_back(frame_json(frame));
            referenced_plans.insert(frame.snapshot->render_plan_version);
        }
        Json node_statistics = Json::array();
        for (const auto& node : scene.node_statistics(id))
            node_statistics.push_back(node_statistics_json(node));
        Json plan_statistics = Json::array();
        for (const auto& plan : scene.plan_version_statistics(id))
            plan_statistics.push_back(plan_statistics_json(plan));
        sessions.push_back({
            {"session_id", session->session_id},
            {"requested_count", session->requested_count},
            {"captured_count", session->captured_count()},
            {"active", session->active()},
            {"frames", std::move(frames)},
            {"summary", session_summary_json(*session)},
            {"node_statistics", std::move(node_statistics)},
            {"plan_statistics", std::move(plan_statistics)}
        });
    }
    if (const auto current = scene.render_plan_snapshot())
        referenced_plans.insert(current->version);
    Json plans = Json::array();
    std::shared_ptr<const Render_Plan> previous;
    for (Render_Plan_Version version : referenced_plans) {
        const auto plan = scene.find_render_plan(version);
        if (!plan)
            continue;
        Json value = plan_json(scene, *plan);
        value["difference"] = previous
            ? plan_difference_json(compare_render_plans(*previous, *plan))
            : Json{{"added_nodes", Json::array()}, {"removed_nodes", Json::array()},
                   {"added_edges", Json::array()}, {"removed_edges", Json::array()}};
        plans.push_back(std::move(value));
        previous = plan;
    }
    return {
        {"controller", {
            {"enabled", controller.enabled()},
            {"session_id", controller.session_id},
            {"remaining_frame_count", controller.remaining_frame_count}
        }},
        {"sessions", std::move(sessions)},
        {"plans", std::move(plans)}
    };
}

} // namespace renderive::web
