#include "GraphvizLayout.h"

#include <cassert>
#include <iomanip>
#include <set>
#include <sstream>
#include <stack>
#include <unordered_map>
#include <unordered_set>
#include <queue>

#include <gvc.h>

GraphvizLayout::GraphvizLayout(LayoutType lineType, Direction direction)
    : GraphLayout({})
    , direction(direction)
    , layoutType(lineType)
{}

static GraphLayout::GraphEdge::ArrowDirection getArrowDirection(
    QPointF direction, bool preferVertical)
{
    if (abs(direction.x()) > abs(direction.y()) * (preferVertical ? 3.0 : 1.0)) {
        if (direction.x() > 0) {
            return GraphLayout::GraphEdge::Right;
        } else {
            return GraphLayout::GraphEdge::Left;
        }
    } else {
        if (direction.y() > 0) {
            return GraphLayout::GraphEdge::Down;
        } else {
            return GraphLayout::GraphEdge::Up;
        }
    }
}

static std::set<std::pair<ut64, ut64>> SelectLoopEdges(const GraphLayout::Graph &graph, ut64 entry)
{
    std::set<std::pair<ut64, ut64>> result;
    // Run DFS to select backwards/loop edges
    // 0 - not visited
    // 1 - in stack
    // 2 - visited
    std::unordered_map<ut64, uint8_t> visited;
    visited.reserve(graph.size());
    std::stack<std::pair<ut64, size_t>> stack;
    auto dfsFragment = [&visited, &graph, &stack, &result](ut64 first) {
        visited[first] = 1;
        stack.push({first, 0});
        while (!stack.empty()) {
            auto v = stack.top().first;
            auto edge_index = stack.top().second;
            auto blockIt = graph.find(v);
            if (blockIt == graph.end()) {
                continue;
            }
            const auto &block = blockIt->second;
            if (edge_index < block.edges.size()) {
                ++stack.top().second;
                auto target = block.edges[edge_index].target;
                auto &targetState = visited[target];
                if (targetState == 0) {
                    targetState = 1;
                    stack.push({target, 0});
                } else if (targetState == 1) {
                    result.insert({v, target});
                }
            } else {
                stack.pop();
                visited[v] = 2;
            }
        }
    };

    dfsFragment(entry);
    for (auto &blockIt : graph) {
        if (!visited[blockIt.first]) {
            dfsFragment(blockIt.first);
        }
    }

    return result;
}

void GraphvizLayout::CalculateLayout(
    std::unordered_map<ut64, GraphBlock> &blocks, ut64 entry, int &width, int &height) const
{
    // https://gitlab.com/graphviz/graphviz/issues/1441
#define STR(v) const_cast<char *>(v)

    width = height = 10;
    GVC_t *gvc = gvContext();
    Agraph_t *g = agopen(STR("G"), Agdirected, nullptr);

    std::unordered_map<ut64, Agnode_t *> nodes;
    for (const auto &block : blocks) {
        nodes[block.first] = agnode(g, nullptr, TRUE);
    }

    std::vector<std::string> strc;
    strc.reserve(2 * blocks.size());
    std::map<std::pair<ut64, ut64>, Agedge_t *> edges;

    agsafeset(
        g,
        STR("splines"),
        layoutType == LayoutType::DotOrtho ? STR("ortho") : STR("polyline"),
        STR(""));
    switch (direction) {
    case Direction::LR:
        agsafeset(g, STR("rankdir"), STR("LR"), STR(""));
        break;
    case Direction::TB:
        agsafeset(g, STR("rankdir"), STR("BT"), STR(""));
        break;
    }
    agsafeset(g, STR("newrank"), STR("true"), STR(""));
    // graphviz has builtin 72 dpi setting for input that differs from output
    // it's easier to use 72 everywhere
    const double dpi = 72.0;
    agsafeset(g, STR("dpi"), STR("72"), STR(""));

    auto widhAttr = agattr(g, AGNODE, STR("width"), STR("1"));
    auto heightAatr = agattr(g, AGNODE, STR("height"), STR("1"));
    agattr(g, AGNODE, STR("shape"), STR("box"));
    agattr(g, AGNODE, STR("fixedsize"), STR("true"));
    auto constraintAttr = agattr(g, AGEDGE, STR("constraint"), STR("1"));

    std::ostringstream stream;
    stream.imbue(std::locale::classic());
    auto setFloatingPointAttr = [&stream](void *obj, Agsym_t *sym, double value) {
        stream.str({});
        stream << std::fixed << std::setw(4) << value;
        auto str = stream.str();
        agxset(obj, sym, STR(str.c_str()));
    };

    std::set<std::pair<ut64, ut64>> loopEdges = SelectLoopEdges(blocks, entry);

    for (const auto &blockIt : blocks) {
        auto u = nodes[blockIt.first];
        auto &block = blockIt.second;

        for (auto &edge : block.edges) {
            auto v = nodes.find(edge.target);
            if (v == nodes.end()) {
                continue;
            }
            auto e = agedge(g, u, v->second, nullptr, TRUE);
            edges[{blockIt.first, edge.target}] = e;
            if (loopEdges.find({blockIt.first, edge.target}) != loopEdges.end()) {
                agxset(e, constraintAttr, STR("0"));
            }
        }
        setFloatingPointAttr(u, widhAttr, block.width / dpi);
        setFloatingPointAttr(u, heightAatr, block.height / dpi);
    }

    const char *layoutEngine = "dot";
    switch (layoutType) {
    case LayoutType::DotOrtho:
    case LayoutType::DotPolyline:
        layoutEngine = "dot";
        break;
    case LayoutType::Sfdp:
        layoutEngine = "sfdp";
        break;
    case LayoutType::Neato:
        layoutEngine = "neato";
        break;
    case LayoutType::TwoPi:
        layoutEngine = "twopi";
        break;
    case LayoutType::Circo:
        layoutEngine = "circo";
        break;
    }

    gvLayout(gvc, g, layoutEngine);

    for (auto &blockIt : blocks) {
        auto &block = blockIt.second;
        auto u = nodes[blockIt.first];

        auto pos = ND_coord(u);

        auto w = ND_width(u) * dpi;
        auto h = ND_height(u) * dpi;
        block.x = pos.x - w / 2.0;
        block.y = pos.y - h / 2.0;
        width = std::max(width, block.x + block.width);
        height = std::max(height, block.y + block.height);

        for (auto &edge : block.edges) {
            auto it = edges.find({blockIt.first, edge.target});
            if (it != edges.end()) {
                auto e = it->second;
                if (auto spl = ED_spl(e)) {
                    for (int i = 0; i < 1 && i < spl->size; i++) {
                        auto bz = spl->list[i];
                        edge.polyline.clear();
                        edge.polyline.reserve(bz.size + 1);
                        for (int j = 0; j < bz.size; j++) {
                            edge.polyline.push_back(QPointF(bz.list[j].x, bz.list[j].y));
                        }
                        QPointF last(0, 0);
                        if (!edge.polyline.empty()) {
                            last = edge.polyline.back();
                        }
                        if (bz.eflag) {
                            QPointF tip = QPointF(bz.ep.x, bz.ep.y);
                            edge.polyline.push_back(tip);
                        }

                        if (edge.polyline.size() >= 2) {
                            // make sure self loops go from bottom to top
                            if (edge.target == block.entry
                                && edge.polyline.first().y() < edge.polyline.last().y()) {
                                std::reverse(edge.polyline.begin(), edge.polyline.end());
                            }
                            auto it = std::prev(edge.polyline.end());
                            QPointF direction = *it;
                            direction -= *(--it);
                            edge.arrow
                                = getArrowDirection(direction, layoutType == LayoutType::DotPolyline);

                        } else {
                            edge.arrow = GraphEdge::Down;
                        }
                    }
                }
            }
        }
    }

    gvFreeLayout(gvc, g);
    agclose(g);
    gvFreeContext(gvc);
#undef STR
}
