#include "Render_Plan.hpp"
#include <algorithm>
#include <limits>
#include <stdexcept>
#include <unordered_map>
#include <unordered_set>

namespace {

bool edge_less(const Render_Edge& left, const Render_Edge& right) {
    return left.from < right.from || (left.from == right.from && left.to < right.to);
}

void normalize(Render_Graph& graph) {
    std::sort(graph.nodes.begin(), graph.nodes.end(), [](const Render_Node& left,
                                                        const Render_Node& right) {
        return left.node_id < right.node_id;
    });
    for (std::size_t index = 0; index < graph.nodes.size(); ++index)
        graph.nodes[index].execution_index = index;
    std::sort(graph.edges.begin(), graph.edges.end(), edge_less);
    graph.edges.erase(std::unique(graph.edges.begin(), graph.edges.end()), graph.edges.end());
}

bool same_topology(const Render_Graph& left, const Render_Graph& right) {
    if (left.nodes.size() != right.nodes.size() || left.edges != right.edges)
        return false;
    for (std::size_t index = 0; index < left.nodes.size(); ++index) {
        const auto& a = left.nodes[index];
        const auto& b = right.nodes[index];
        if (a.node_id != b.node_id || a.owner_id != b.owner_id || a.name != b.name ||
            a.kind != b.kind)
            return false;
    }
    return true;
}

}

Render_Graph_Builder::Task Render_Graph_Builder::emplace(
    Render_Node_Id node_id, std::uint64_t owner_id, std::string name,
    Render_Node_Kind kind) {
    if (node_id == 0)
        throw std::invalid_argument("render node id must not be zero");
    if (std::any_of(graph_.nodes.begin(), graph_.nodes.end(), [node_id](const Render_Node& node) {
            return node.node_id == node_id;
        }))
        throw std::invalid_argument("duplicate render node id");
    graph_.nodes.push_back({node_id, owner_id, std::move(name), kind,
                            graph_.nodes.size()});
    return {this, generation_, graph_.nodes.size() - 1};
}

void Render_Graph_Builder::precede(Task from, Task to) {
    validate(from);
    validate(to);
    if (from.index == to.index || reaches(to.index, from.index))
        throw std::invalid_argument("render graph cycle");
    const Render_Edge edge{graph_.nodes[from.index].node_id, graph_.nodes[to.index].node_id};
    if (std::find(graph_.edges.begin(), graph_.edges.end(), edge) == graph_.edges.end())
        graph_.edges.push_back(edge);
}

const Render_Graph& Render_Graph_Builder::graph() const noexcept {
    return graph_;
}

Render_Graph Render_Graph_Builder::finish() && {
    ++generation_;
    normalize(graph_);
    return std::move(graph_);
}

void Render_Graph_Builder::validate(Task task) const {
    if (task.builder != this || task.generation != generation_ ||
        task.index >= graph_.nodes.size())
        throw std::invalid_argument("invalid render graph task");
}

bool Render_Graph_Builder::reaches(std::size_t from, std::size_t target) const {
    std::unordered_map<Render_Node_Id, std::size_t> indices;
    indices.reserve(graph_.nodes.size());
    for (std::size_t index = 0; index < graph_.nodes.size(); ++index)
        indices.emplace(graph_.nodes[index].node_id, index);
    std::vector<std::size_t> stack{from};
    std::vector<bool> visited(graph_.nodes.size());
    while (!stack.empty()) {
        const std::size_t index = stack.back();
        stack.pop_back();
        if (index == target)
            return true;
        if (visited[index])
            continue;
        visited[index] = true;
        const Render_Node_Id id = graph_.nodes[index].node_id;
        for (const Render_Edge& edge : graph_.edges) {
            if (edge.from == id)
                stack.push_back(indices.at(edge.to));
        }
    }
    return false;
}

std::shared_ptr<const Render_Plan> Render_Plan_History::publish(Render_Graph graph) {
    normalize(graph);
    std::lock_guard lock(mutex_);
    if (!plans_.empty() && same_topology(plans_.back()->graph, graph))
        return plans_.back();
    if (next_version_ == 0 ||
        next_version_ == std::numeric_limits<Render_Plan_Version>::max())
        throw std::overflow_error("render plan version exhausted");
    auto plan = std::make_shared<Render_Plan>();
    plan->version = next_version_++;
    plan->graph = std::move(graph);
    plans_.push_back(plan);
    return plan;
}

std::shared_ptr<const Render_Plan> Render_Plan_History::current() const {
    std::lock_guard lock(mutex_);
    return plans_.empty() ? nullptr : plans_.back();
}

std::shared_ptr<const Render_Plan> Render_Plan_History::find(Render_Plan_Version version) const {
    std::lock_guard lock(mutex_);
    const auto iterator = std::lower_bound(
        plans_.begin(), plans_.end(), version,
        [](const std::shared_ptr<const Render_Plan>& plan, Render_Plan_Version value) {
            return plan->version < value;
        });
    return iterator != plans_.end() && (*iterator)->version == version ? *iterator : nullptr;
}

Render_Plan_Difference compare_render_plans(const Render_Plan& before,
                                            const Render_Plan& after) {
    Render_Plan_Difference result;
    std::unordered_set<Render_Node_Id> before_nodes;
    std::unordered_set<Render_Node_Id> after_nodes;
    before_nodes.reserve(before.graph.nodes.size());
    after_nodes.reserve(after.graph.nodes.size());
    for (const auto& node : before.graph.nodes)
        before_nodes.insert(node.node_id);
    for (const auto& node : after.graph.nodes)
        after_nodes.insert(node.node_id);
    for (Render_Node_Id id : after_nodes) {
        if (!before_nodes.contains(id))
            result.added_nodes.push_back(id);
    }
    for (Render_Node_Id id : before_nodes) {
        if (!after_nodes.contains(id))
            result.removed_nodes.push_back(id);
    }
    for (const auto& edge : after.graph.edges) {
        if (std::find(before.graph.edges.begin(), before.graph.edges.end(), edge) ==
            before.graph.edges.end())
            result.added_edges.push_back(edge);
    }
    for (const auto& edge : before.graph.edges) {
        if (std::find(after.graph.edges.begin(), after.graph.edges.end(), edge) ==
            after.graph.edges.end())
            result.removed_edges.push_back(edge);
    }
    std::sort(result.added_nodes.begin(), result.added_nodes.end());
    std::sort(result.removed_nodes.begin(), result.removed_nodes.end());
    std::sort(result.added_edges.begin(), result.added_edges.end(), edge_less);
    std::sort(result.removed_edges.begin(), result.removed_edges.end(), edge_less);
    return result;
}
