#include "ui/graph_renderer.h"

#include "managers/avatar_cache.h"
#include "models/repository.h"
#include <IconsTabler.h>

#include <algorithm>
#include <array>
#include <cmath>
#include <limits>

namespace {

struct RoutedEdge {
    int child_row = -1;
    int parent_row = -1;
    int child_lane = 0;
    int parent_lane = 0;
    int route_slot = 0;
    int color_lane = 0;
    int detour_lane = -1;
};

template <size_t Count>
void drawRoundedPolyline(ImDrawList* draw, const std::array<ImVec2, Count>& points,
        float radius, ImU32 color, float thickness) {
    static_assert(Count >= 2);
    draw->PathClear();
    draw->PathLineTo(points.front());
    for (size_t index = 1; index + 1 < Count; ++index) {
        const ImVec2 previous = points[index - 1];
        const ImVec2 corner = points[index];
        const ImVec2 next = points[index + 1];
        const float incoming_length = std::hypot(corner.x - previous.x, corner.y - previous.y);
        const float outgoing_length = std::hypot(next.x - corner.x, next.y - corner.y);
        if (incoming_length < 0.01f || outgoing_length < 0.01f) continue;
        const float turn_radius = std::min({radius, incoming_length * 0.5f, outgoing_length * 0.5f});
        const ImVec2 incoming{
            (corner.x - previous.x) / incoming_length,
            (corner.y - previous.y) / incoming_length,
        };
        const ImVec2 outgoing{
            (next.x - corner.x) / outgoing_length,
            (next.y - corner.y) / outgoing_length,
        };
        const ImVec2 before{
            corner.x - incoming.x * turn_radius,
            corner.y - incoming.y * turn_radius,
        };
        const ImVec2 after{
            corner.x + outgoing.x * turn_radius,
            corner.y + outgoing.y * turn_radius,
        };
        constexpr float control = 0.55228475f;
        draw->PathLineTo(before);
        draw->PathBezierCubicCurveTo(
            {before.x + incoming.x * turn_radius * control,
                before.y + incoming.y * turn_radius * control},
            {after.x - outgoing.x * turn_radius * control,
                after.y - outgoing.y * turn_radius * control},
            after);
    }
    draw->PathLineTo(points.back());
    draw->PathStroke(color, ImDrawFlags_None, thickness);
}

void drawOrthogonalEdge(ImDrawList* draw, const ImVec2& child, const ImVec2& parent,
        ImU32 color, float scale, int route_slot, float detour_x) {
    const float thickness = 2.0f * scale;
    const float horizontal = parent.x - child.x;
    const float vertical = parent.y - child.y;
    if (vertical <= 0.0f) {
        draw->AddLine(child, parent, color, thickness);
        return;
    }

    if (std::isfinite(detour_x)) {
        if (std::abs(detour_x - child.x) < 0.5f * scale) {
            drawRoundedPolyline(draw, std::array{child, ImVec2{detour_x, parent.y}, parent},
                7.0f * scale, color, thickness);
        } else if (std::abs(detour_x - parent.x) < 0.5f * scale) {
            drawRoundedPolyline(draw, std::array{child, ImVec2{detour_x, child.y}, parent},
                7.0f * scale, color, thickness);
        } else {
            drawRoundedPolyline(draw,
                std::array{child, ImVec2{detour_x, child.y}, ImVec2{detour_x, parent.y}, parent},
                7.0f * scale, color, thickness);
        }
        return;
    }

    if (std::abs(horizontal) < 0.5f * scale) {
        draw->AddLine(child, parent, color, thickness);
        return;
    }

    // Bends remain anchored to commit rows, but use a compact quarter-round turn.
    if (route_slot == 0)
        drawRoundedPolyline(draw, std::array{child, ImVec2{child.x, parent.y}, parent},
            7.0f * scale, color, thickness);
    else
        drawRoundedPolyline(draw, std::array{child, ImVec2{parent.x, child.y}, parent},
            7.0f * scale, color, thickness);
}

} // namespace

ImU32 GraphRenderer::laneColor(int lane, int alpha) {
    static constexpr ImVec4 colors[] = {
        {0.08f, 0.70f, 0.83f, 1.0f},
        {0.23f, 0.45f, 0.92f, 1.0f},
        {0.78f, 0.25f, 0.78f, 1.0f},
        {0.94f, 0.31f, 0.35f, 1.0f},
        {1.00f, 0.48f, 0.16f, 1.0f},
        {0.94f, 0.73f, 0.18f, 1.0f},
        {0.25f, 0.73f, 0.50f, 1.0f},
    };
    ImVec4 color = colors[static_cast<size_t>(std::abs(lane)) % std::size(colors)];
    color.w = static_cast<float>(alpha) / 255.0f;
    return ImGui::ColorConvertFloat4ToU32(color);
}

float GraphRenderer::requiredWidth(const std::vector<CommitInfo>& commits, float scale) {
    int maximum_lane = 0;
    for (const auto& commit : commits) maximum_lane = std::max(maximum_lane, commit.lane);
    // Keep one spare lane available for edges that must route around occupied commit lanes.
    return std::max((68.0f + maximum_lane * 22.0f) * scale, 82.0f * scale);
}

void GraphRenderer::drawRow(int row, const CommitInfo& commit,
        const std::vector<CommitInfo>& commits, const std::vector<float>& row_heights,
        const std::vector<std::vector<int>>& parent_rows, AvatarCache* avatars,
        const ImVec2* ref_connector_start) const {
    ImDrawList* draw = ImGui::GetWindowDrawList();
    const ImVec2 origin = ImGui::GetCursorScreenPos();
    const float row_height = row_heights[static_cast<size_t>(row)];
    const float content_height = std::max(px(1.0f), row_height - ImGui::GetStyle().CellPadding.y * 2.0f);
    const float lane_spacing = px(22.0f);
    const float x = origin.x + px(17.0f) + lane_spacing * commit.lane;
    const float y = origin.y + content_height * 0.5f;
    const float cell_right = origin.x + ImGui::GetContentRegionAvail().x;
    const float row_clip_padding = ImGui::GetStyle().CellPadding.y + px(1.0f);

    // Start the lane ribbon at the profile circle's centerline. The opaque node backing
    // masks its left edge, while the vertical inset keeps neighboring row tints separate.
    const float ribbon_left = std::min(x, cell_right);
    const float ribbon_top = origin.y + px(1.0f);
    const float ribbon_bottom = origin.y + content_height - px(1.0f);
    draw->AddRectFilled(
        {ribbon_left, ribbon_top},
        {cell_right, ribbon_bottom},
        laneColor(commit.graph_color, 38));
    draw->AddLine(
        {cell_right - px(1.0f), ribbon_top},
        {cell_right - px(1.0f), ribbon_bottom},
        laneColor(commit.graph_color, 215), px(2.0f));
    if (ref_connector_start) {
        const ImVec2 end{x, y};
        const float turn_x = end.x - px(8.0f);
        draw->PushClipRectFullScreen();
        drawRoundedPolyline(draw,
            std::array{*ref_connector_start, ImVec2{turn_x, ref_connector_start->y},
                ImVec2{turn_x, end.y}, end},
            px(5.0f), laneColor(commit.graph_color, 190), px(1.8f));
        draw->PopClipRect();
    }

    std::vector<float> row_offsets(row_heights.size() + 1, 0.0f);
    for (size_t index = 0; index < row_heights.size(); ++index)
        row_offsets[index + 1] = row_offsets[index] + row_heights[index];
    const float graph_top = origin.y - row_offsets[static_cast<size_t>(row)];
    const auto center_y = [&](int index) {
        return graph_top + row_offsets[static_cast<size_t>(index)] +
            (row_heights[static_cast<size_t>(index)] - ImGui::GetStyle().CellPadding.y * 2.0f) * 0.5f;
    };

    int maximum_lane = 0;
    for (const auto& item : commits) maximum_lane = std::max(maximum_lane, item.lane);

    std::vector<RoutedEdge> routed_edges;
    routed_edges.reserve(commits.size() * 2);
    const auto route_spans_overlap = [](const RoutedEdge& left, const RoutedEdge& right) {
        const int top = std::max(left.child_row, right.child_row);
        const int bottom = std::min(left.parent_row, right.parent_row);
        return top < bottom;
    };
    const auto commit_blocks_lane = [&](int candidate_lane, int child_row, int parent_row) {
        for (int intermediate = child_row + 1; intermediate < parent_row; ++intermediate) {
            if (row_heights[static_cast<size_t>(intermediate)] > 0.0f &&
                    commits[static_cast<size_t>(intermediate)].lane == candidate_lane)
                return true;
        }
        return false;
    };
    const auto route_blocks_lane = [&](int candidate_lane, int child_row, int parent_row) {
        const RoutedEdge probe{child_row, parent_row, 0, 0, 0, 0, candidate_lane};
        return std::any_of(routed_edges.begin(), routed_edges.end(), [&](const RoutedEdge& edge) {
            return edge.detour_lane == candidate_lane && route_spans_overlap(edge, probe);
        });
    };
    const auto lane_available = [&](int candidate_lane, int child_row, int parent_row) {
        return !commit_blocks_lane(candidate_lane, child_row, parent_row) &&
            !route_blocks_lane(candidate_lane, child_row, parent_row);
    };

    for (int child_row = 0; child_row < static_cast<int>(commits.size()); ++child_row) {
        const auto& child_parents = parent_rows[static_cast<size_t>(child_row)];
        for (size_t parent_index = 0; parent_index < child_parents.size(); ++parent_index) {
            const int parent_row = child_parents[parent_index];
            if (parent_row <= child_row) continue;

            const CommitInfo& child_commit = commits[static_cast<size_t>(child_row)];
            const CommitInfo& parent_commit = commits[static_cast<size_t>(parent_row)];
            const int preferred_lane = parent_index == 0 ? child_commit.lane : parent_commit.lane;
            int detour_lane = -1;

            if (!lane_available(preferred_lane, child_row, parent_row)) {
                for (int distance = 1; distance <= maximum_lane + 2; ++distance) {
                    const int left = preferred_lane - distance;
                    const int right = preferred_lane + distance;
                    if (left >= 0 && lane_available(left, child_row, parent_row)) {
                        detour_lane = left;
                        break;
                    }
                    if (lane_available(right, child_row, parent_row)) {
                        detour_lane = right;
                        maximum_lane = std::max(maximum_lane, right);
                        break;
                    }
                }
                if (detour_lane < 0) {
                    detour_lane = maximum_lane + 1;
                    maximum_lane = detour_lane;
                }
            }

            RoutedEdge route;
            route.child_row = child_row;
            route.parent_row = parent_row;
            route.child_lane = child_commit.lane;
            route.parent_lane = parent_commit.lane;
            route.route_slot = static_cast<int>(parent_index);
            route.detour_lane = detour_lane;
            route.color_lane = detour_lane >= 0 ? detour_lane : preferred_lane;
            routed_edges.push_back(route);
        }
    }

    // Every row redraws edges crossing its clip rectangle. This keeps long paths continuous
    // without allowing table row clipping to cut out intermediate segments.
    draw->PushClipRect(
        {origin.x, origin.y - row_clip_padding},
        {origin.x + ImGui::GetContentRegionAvail().x, origin.y + content_height + row_clip_padding}, true);
    for (const RoutedEdge& route : routed_edges) {
        if (row < route.child_row || row > route.parent_row ||
                row_heights[static_cast<size_t>(route.child_row)] <= 0.0f ||
                row_heights[static_cast<size_t>(route.parent_row)] <= 0.0f)
            continue;

        const float child_x = origin.x + px(17.0f) + lane_spacing * route.child_lane;
        const float parent_x = origin.x + px(17.0f) + lane_spacing * route.parent_lane;
        const float child_y = center_y(route.child_row);
        const float parent_y = center_y(route.parent_row);
        const float detour_x = route.detour_lane >= 0
            ? origin.x + px(17.0f) + lane_spacing * route.detour_lane
            : std::numeric_limits<float>::quiet_NaN();

            drawOrthogonalEdge(draw, {child_x, child_y}, {parent_x, parent_y},
                laneColor(route.color_lane, 235), scale_, route.route_slot, detour_x);
    }
    draw->PopClipRect();

    const ImU32 lane_color = laneColor(commit.graph_color);
    const auto show_identity_tooltip = [&](float hit_radius) {
        if (!ImGui::IsWindowHovered()) return;
        const ImVec2 mouse = ImGui::GetIO().MousePos;
        const float delta_x = mouse.x - x;
        const float delta_y = mouse.y - y;
        if (delta_x * delta_x + delta_y * delta_y > hit_radius * hit_radius) return;
        if (commit.email.empty()) ImGui::SetTooltip("%s", commit.author.c_str());
        else ImGui::SetTooltip("%s <%s>", commit.author.c_str(), commit.email.c_str());
    };
    if (commit.parents > 1) {
        draw->AddCircleFilled({x, y}, px(6.0f), lane_color);
        draw->AddCircle({x, y}, px(7.5f), laneColor(commit.graph_color, 130), 0, px(1.2f));
        show_identity_tooltip(px(9.0f));
        ImGui::Dummy({0.0f, content_height});
        return;
    }

    const float radius = px(9.6f);
    // The opaque backing masks every lane segment before the avatar is painted.
    draw->AddCircleFilled({x, y}, radius + px(1.8f), IM_COL32(19, 24, 31, 255));
    draw->AddCircle({x, y}, radius + px(0.8f), lane_color, 0, px(2.0f));
    draw->AddCircleFilled({x, y}, radius - px(1.0f), IM_COL32(232, 238, 242, 255));

    const unsigned int texture = avatars ? avatars->textureFor(commit.email) : 0;
    if (texture) {
        draw->AddImageRounded(ImTextureRef(static_cast<ImTextureID>(texture)),
            {x - radius + px(1.5f), y - radius + px(1.5f)},
            {x + radius - px(1.5f), y + radius - px(1.5f)},
            {0, 0}, {1, 1}, IM_COL32_WHITE, radius);
    } else {
        const ImVec2 icon_size = ImGui::CalcTextSize(ICON_TB_USER);
        draw->AddText({x - icon_size.x * 0.5f, y - icon_size.y * 0.5f}, lane_color, ICON_TB_USER);
    }
    show_identity_tooltip(radius + px(2.0f));
    ImGui::Dummy({0.0f, content_height});
}
