#include "ui/diff_viewer.h"

#include "managers/avatar_cache.h"
#include "managers/git_manager.h"
#include "models/repository.h"

#include <IconsTabler.h>
#include <imgui.h>

#include <algorithm>
#include <cctype>
#include <cstring>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <initializer_list>
#include <numeric>
#include <sstream>
#include <string_view>

namespace {
float scaled(float value, float scale) { return value * scale; }

std::vector<std::string> splitLines(const std::string& text) {
    std::vector<std::string> lines;
    std::istringstream stream(text);
    std::string line;
    while (std::getline(stream, line)) {
        if (!line.empty() && line.back() == '\r') line.pop_back();
        lines.push_back(std::move(line));
    }
    return lines;
}

void parseRange(const std::string& header, char marker, int& line) {
    const size_t marker_position = header.find(marker);
    if (marker_position == std::string::npos) return;
    size_t cursor = marker_position + 1;
    line = 0;
    while (cursor < header.size() && std::isdigit(static_cast<unsigned char>(header[cursor]))) {
        line = line * 10 + (header[cursor] - '0');
        ++cursor;
    }
}

enum class SyntaxLanguage { plain, c, cpp, csharp, lua, python, javascript };
enum class SyntaxContinuation { none, block_comment, lua_comment, python_single, python_double, template_string };

struct SyntaxState {
    SyntaxContinuation continuation = SyntaxContinuation::none;
};

[[maybe_unused]] ImU32 blameColor(std::string_view hash, int alpha = 255) {
    static constexpr ImU32 colors[] = {
        IM_COL32(24, 181, 204, 255), IM_COL32(73, 123, 235, 255), IM_COL32(200, 64, 200, 255),
        IM_COL32(239, 79, 89, 255), IM_COL32(255, 122, 41, 255), IM_COL32(240, 186, 46, 255),
        IM_COL32(64, 186, 128, 255),
    };
    uint32_t value = 2166136261u;
    for (const unsigned char character : hash) value = (value ^ character) * 16777619u;
    return (colors[value % std::size(colors)] & ~IM_COL32_A_MASK) | (static_cast<ImU32>(alpha) << IM_COL32_A_SHIFT);
}

[[maybe_unused]] SyntaxLanguage languageForPath(const std::string& path) {
    std::string extension = std::filesystem::path(path).extension().string();
    std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value) {
        return static_cast<char>(std::tolower(value));
    });
    if (extension == ".c") return SyntaxLanguage::c;
    if (extension == ".h" || extension == ".hh" || extension == ".hpp" || extension == ".hxx" ||
            extension == ".cc" || extension == ".cpp" || extension == ".cxx") return SyntaxLanguage::cpp;
    if (extension == ".cs") return SyntaxLanguage::csharp;
    if (extension == ".lua") return SyntaxLanguage::lua;
    if (extension == ".py" || extension == ".pyw") return SyntaxLanguage::python;
    if (extension == ".js" || extension == ".jsx" || extension == ".mjs" || extension == ".cjs")
        return SyntaxLanguage::javascript;
    return SyntaxLanguage::plain;
}

bool wordIs(std::string_view word, std::initializer_list<std::string_view> values) {
    return std::find(values.begin(), values.end(), word) != values.end();
}

bool isKeyword(SyntaxLanguage language, std::string_view word) {
    static constexpr std::string_view common[] = {
        "break", "case", "continue", "default", "do", "else", "for", "if", "return", "switch", "while"
    };
    if (std::find(std::begin(common), std::end(common), word) != std::end(common)) return true;
    switch (language) {
    case SyntaxLanguage::c:
        return wordIs(word, {"const", "enum", "extern", "goto", "register", "sizeof", "static", "struct", "typedef", "union", "volatile"});
    case SyntaxLanguage::cpp:
        return wordIs(word, {"alignas", "auto", "class", "concept", "const", "constexpr", "consteval", "constinit", "co_await", "co_return", "co_yield", "decltype", "delete", "enum", "explicit", "export", "extern", "friend", "inline", "mutable", "namespace", "new", "noexcept", "operator", "override", "private", "protected", "public", "requires", "sizeof", "static", "struct", "template", "this", "thread_local", "throw", "try", "typedef", "typename", "union", "using", "virtual", "volatile"});
    case SyntaxLanguage::csharp:
        return wordIs(word, {"abstract", "as", "async", "await", "base", "class", "const", "delegate", "enum", "event", "explicit", "extern", "finally", "fixed", "foreach", "implicit", "in", "interface", "internal", "is", "lock", "namespace", "new", "operator", "out", "override", "params", "private", "protected", "public", "readonly", "record", "ref", "sealed", "sizeof", "stackalloc", "static", "struct", "this", "throw", "try", "typeof", "unchecked", "unsafe", "using", "virtual", "volatile", "where", "yield"});
    case SyntaxLanguage::lua:
        return wordIs(word, {"and", "elseif", "end", "false", "function", "goto", "in", "local", "nil", "not", "or", "repeat", "then", "true", "until"});
    case SyntaxLanguage::python:
        return wordIs(word, {"and", "as", "assert", "async", "await", "class", "def", "del", "elif", "except", "False", "finally", "from", "global", "import", "in", "is", "lambda", "None", "nonlocal", "not", "or", "pass", "raise", "True", "try", "with", "yield"});
    case SyntaxLanguage::javascript:
        return wordIs(word, {"async", "await", "catch", "class", "const", "debugger", "delete", "export", "extends", "false", "finally", "from", "function", "get", "import", "in", "instanceof", "let", "new", "null", "of", "set", "static", "super", "this", "throw", "true", "try", "typeof", "undefined", "var", "void", "with", "yield"});
    default:
        return false;
    }
}

bool isTypeWord(SyntaxLanguage language, std::string_view word) {
    if (language == SyntaxLanguage::python || language == SyntaxLanguage::lua || language == SyntaxLanguage::plain)
        return false;
    return wordIs(word, {"bool", "byte", "char", "decimal", "double", "dynamic", "float", "int", "int8_t", "int16_t", "int32_t", "int64_t", "long", "nint", "nuint", "object", "sbyte", "short", "signed", "size_t", "string", "uint", "uint8_t", "uint16_t", "uint32_t", "uint64_t", "ulong", "unsigned", "ushort", "var", "void", "wchar_t"});
}

bool isBuiltin(SyntaxLanguage language, std::string_view word) {
    switch (language) {
    case SyntaxLanguage::cpp:
        return wordIs(word, {"std", "move", "forward", "make_shared", "make_unique", "optional", "string_view", "vector"});
    case SyntaxLanguage::csharp:
        return wordIs(word, {"Console", "DateTime", "Dictionary", "Enumerable", "List", "Task", "ValueTask"});
    case SyntaxLanguage::lua:
        return wordIs(word, {"assert", "error", "ipairs", "io", "math", "os", "pairs", "pcall", "print", "require", "string", "table", "tonumber", "tostring", "type"});
    case SyntaxLanguage::python:
        return wordIs(word, {"abs", "all", "any", "dict", "enumerate", "filter", "float", "int", "len", "list", "map", "max", "min", "object", "open", "print", "range", "reversed", "set", "str", "sum", "super", "tuple", "zip"});
    case SyntaxLanguage::javascript:
        return wordIs(word, {"Array", "Boolean", "console", "Date", "Error", "JSON", "Map", "Math", "Number", "Object", "process", "Promise", "RegExp", "require", "Set", "String", "Symbol"});
    default:
        return false;
    }
}

bool isLiteral(std::string_view word) {
    return wordIs(word, {"false", "False", "nil", "null", "nullptr", "None", "true", "True", "undefined"});
}

bool isDeclarationKeyword(std::string_view word) {
    return wordIs(word, {"class", "concept", "delegate", "enum", "interface", "namespace", "record", "struct", "type"});
}

bool isFunctionDeclarationKeyword(std::string_view word) {
    return word == "def" || word == "function";
}

bool isMacroName(std::string_view word) {
    bool has_letter = false;
    for (const unsigned char character : word) {
        if (std::isalpha(character)) {
            has_letter = true;
            if (std::islower(character)) return false;
        } else if (!std::isdigit(character) && character != '_') {
            return false;
        }
    }
    return has_letter && word.size() > 1;
}

constexpr ImU32 syntax_normal = IM_COL32(218, 221, 226, 255);
constexpr ImU32 syntax_keyword = IM_COL32(198, 139, 230, 255);
constexpr ImU32 syntax_type = IM_COL32(91, 198, 190, 255);
constexpr ImU32 syntax_string = IM_COL32(226, 166, 140, 255);
constexpr ImU32 syntax_number = IM_COL32(181, 206, 126, 255);
constexpr ImU32 syntax_comment = IM_COL32(112, 153, 105, 255);
constexpr ImU32 syntax_function = IM_COL32(220, 199, 128, 255);
constexpr ImU32 syntax_preprocessor = IM_COL32(205, 157, 222, 255);
constexpr ImU32 syntax_member = IM_COL32(122, 184, 225, 255);
constexpr ImU32 syntax_builtin = IM_COL32(103, 172, 232, 255);
constexpr ImU32 syntax_constant = IM_COL32(214, 139, 102, 255);
constexpr ImU32 syntax_operator = IM_COL32(105, 180, 210, 255);
constexpr ImU32 syntax_decorator = IM_COL32(226, 190, 105, 255);
constexpr ImU32 syntax_macro = IM_COL32(215, 128, 180, 255);

[[maybe_unused]] void drawSyntaxText(ImDrawList* draw, ImVec2 position, const std::string& text,
        SyntaxLanguage language, SyntaxState& state) {
    const auto drawSpan = [&](size_t begin, size_t end, ImU32 color) {
        if (end <= begin) return;
        const char* first = text.data() + begin;
        const char* last = text.data() + end;
        draw->AddText(position, color, first, last);
        position.x += ImGui::CalcTextSize(first, last, false).x;
    };
    if (language == SyntaxLanguage::plain) {
        drawSpan(0, text.size(), syntax_normal);
        return;
    }

    const size_t first_non_space = text.find_first_not_of(" \t");
    if ((language == SyntaxLanguage::c || language == SyntaxLanguage::cpp || language == SyntaxLanguage::csharp) &&
            first_non_space != std::string::npos && text[first_non_space] == '#') {
        drawSpan(0, first_non_space, syntax_normal);
        drawSpan(first_non_space, text.size(), syntax_preprocessor);
        return;
    }

    size_t cursor = 0;
    std::string_view previous_word;
    while (cursor < text.size()) {
        if (state.continuation == SyntaxContinuation::block_comment) {
            const size_t end = text.find("*/", cursor);
            if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_comment); return; }
            drawSpan(cursor, end + 2, syntax_comment);
            cursor = end + 2;
            state.continuation = SyntaxContinuation::none;
            continue;
        }
        if (state.continuation == SyntaxContinuation::lua_comment) {
            const size_t end = text.find("]]", cursor);
            if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_comment); return; }
            drawSpan(cursor, end + 2, syntax_comment);
            cursor = end + 2;
            state.continuation = SyntaxContinuation::none;
            continue;
        }
        if (state.continuation == SyntaxContinuation::python_single ||
                state.continuation == SyntaxContinuation::python_double) {
            const std::string_view delimiter = state.continuation == SyntaxContinuation::python_single ? "'''" : "\"\"\"";
            const size_t end = text.find(delimiter, cursor);
            if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_string); return; }
            drawSpan(cursor, end + delimiter.size(), syntax_string);
            cursor = end + delimiter.size();
            state.continuation = SyntaxContinuation::none;
            continue;
        }
        if (state.continuation == SyntaxContinuation::template_string) {
            const size_t end = text.find('`', cursor);
            if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_string); return; }
            drawSpan(cursor, end + 1, syntax_string);
            cursor = end + 1;
            state.continuation = SyntaxContinuation::none;
            continue;
        }

        const bool slash_comments = language == SyntaxLanguage::c || language == SyntaxLanguage::cpp ||
            language == SyntaxLanguage::csharp || language == SyntaxLanguage::javascript;
        if (slash_comments && text.compare(cursor, 2, "//") == 0) {
            drawSpan(cursor, text.size(), syntax_comment);
            return;
        }
        if (slash_comments && text.compare(cursor, 2, "/*") == 0) {
            const size_t end = text.find("*/", cursor + 2);
            if (end == std::string::npos) {
                drawSpan(cursor, text.size(), syntax_comment);
                state.continuation = SyntaxContinuation::block_comment;
                return;
            }
            drawSpan(cursor, end + 2, syntax_comment);
            cursor = end + 2;
            continue;
        }
        if (language == SyntaxLanguage::python && text[cursor] == '#') {
            drawSpan(cursor, text.size(), syntax_comment);
            return;
        }
        if (language == SyntaxLanguage::lua && text.compare(cursor, 4, "--[[") == 0) {
            const size_t end = text.find("]]", cursor + 4);
            if (end == std::string::npos) {
                drawSpan(cursor, text.size(), syntax_comment);
                state.continuation = SyntaxContinuation::lua_comment;
                return;
            }
            drawSpan(cursor, end + 2, syntax_comment);
            cursor = end + 2;
            continue;
        }
        if (language == SyntaxLanguage::lua && text.compare(cursor, 2, "--") == 0) {
            drawSpan(cursor, text.size(), syntax_comment);
            return;
        }
        if (language == SyntaxLanguage::python &&
                (text.compare(cursor, 3, "'''") == 0 || text.compare(cursor, 3, "\"\"\"") == 0)) {
            const std::string_view delimiter(text.data() + cursor, 3);
            const size_t end = text.find(delimiter, cursor + 3);
            if (end == std::string::npos) {
                drawSpan(cursor, text.size(), syntax_string);
                state.continuation = delimiter[0] == '\'' ? SyntaxContinuation::python_single : SyntaxContinuation::python_double;
                return;
            }
            drawSpan(cursor, end + 3, syntax_string);
            cursor = end + 3;
            continue;
        }
        if (text[cursor] == '\'' || text[cursor] == '"' ||
                (language == SyntaxLanguage::javascript && text[cursor] == '`')) {
            const char quote = text[cursor];
            size_t end = cursor + 1;
            bool escaped = false;
            for (; end < text.size(); ++end) {
                if (!escaped && text[end] == quote) { ++end; break; }
                escaped = !escaped && text[end] == '\\';
                if (text[end] != '\\') escaped = false;
            }
            const bool closed = end <= text.size() && end > cursor + 1 && text[end - 1] == quote;
            drawSpan(cursor, end, syntax_string);
            if (quote == '`' && !closed) state.continuation = SyntaxContinuation::template_string;
            cursor = end;
            continue;
        }
        if (std::isdigit(static_cast<unsigned char>(text[cursor]))) {
            size_t end = cursor + 1;
            while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) ||
                    text[end] == '.' || text[end] == '_')) ++end;
            drawSpan(cursor, end, syntax_number);
            cursor = end;
            continue;
        }
        if (text[cursor] == '@' && cursor + 1 < text.size() &&
                (std::isalpha(static_cast<unsigned char>(text[cursor + 1])) || text[cursor + 1] == '_')) {
            size_t end = cursor + 2;
            while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) ||
                    text[end] == '_' || text[end] == '.')) ++end;
            drawSpan(cursor, end, syntax_decorator);
            cursor = end;
            continue;
        }
        if (std::isalpha(static_cast<unsigned char>(text[cursor])) || text[cursor] == '_') {
            size_t end = cursor + 1;
            while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) || text[end] == '_')) ++end;
            const std::string_view word(text.data() + cursor, end - cursor);
            size_t next = end;
            while (next < text.size() && std::isspace(static_cast<unsigned char>(text[next]))) ++next;
            size_t previous = cursor;
            while (previous > 0 && std::isspace(static_cast<unsigned char>(text[previous - 1]))) --previous;
            const bool member_access = previous > 0 && (text[previous - 1] == '.' ||
                (text[previous - 1] == '>' && previous > 1 && text[previous - 2] == '-') ||
                (text[previous - 1] == ':' && previous > 1 && text[previous - 2] == ':'));
            const bool declared_name = isDeclarationKeyword(previous_word);
            const bool declared_function = isFunctionDeclarationKeyword(previous_word);
            const bool capitalized_type =
                (language == SyntaxLanguage::cpp || language == SyntaxLanguage::csharp) &&
                std::isupper(static_cast<unsigned char>(word.front())) && !member_access &&
                (next >= text.size() || text[next] != '(');
            const bool likely_type = declared_name || isTypeWord(language, word) ||
                capitalized_type;
            const ImU32 color = isLiteral(word) ? syntax_constant : isKeyword(language, word) ? syntax_keyword :
                declared_function ? syntax_function : declared_name || likely_type ? syntax_type :
                isMacroName(word) ? syntax_macro : isBuiltin(language, word) ? syntax_builtin : member_access ?
                    (next < text.size() && text[next] == '(' ? syntax_function : syntax_member) :
                next < text.size() && text[next] == '(' ? syntax_function : syntax_normal;
            drawSpan(cursor, end, color);
            previous_word = word;
            cursor = end;
            continue;
        }
        if (std::string_view("+-*/%=!<>&|^~?:").find(text[cursor]) != std::string_view::npos) {
            size_t end = cursor + 1;
            while (end < text.size() &&
                    std::string_view("+-*/%=!<>&|^~?:").find(text[end]) != std::string_view::npos) ++end;
            drawSpan(cursor, end, syntax_operator);
            cursor = end;
            continue;
        }
        size_t end = cursor + 1;
        while (end < text.size()) {
            const unsigned char value = static_cast<unsigned char>(text[end]);
            const bool token_start = std::isalnum(value) || text[end] == '_' || text[end] == '\'' ||
                text[end] == '"' || (language == SyntaxLanguage::javascript && text[end] == '`') ||
                (slash_comments && text[end] == '/') || (language == SyntaxLanguage::python && text[end] == '#') ||
                (language == SyntaxLanguage::lua && text[end] == '-') || text[end] == '@' ||
                std::string_view("+-*/%=!<>&|^~?:").find(text[end]) != std::string_view::npos;
            if (token_start) break;
            ++end;
        }
        drawSpan(cursor, end, syntax_normal);
        cursor = end;
    }
}

bool compactButton(const char* label, bool active = false) {
    if (active) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.13f, 0.25f, 0.43f, 1.0f));
    const bool icon_only = label && std::strlen(label) <= 4;
    const bool clicked = icon_only
        ? ImGui::Button(label, {ImGui::GetFrameHeight(), ImGui::GetFrameHeight()})
        : ImGui::SmallButton(label);
    if (active) ImGui::PopStyleColor();
    return clicked;
}

bool toolbarSegmentButton(const char* label, bool active, float width) {
    ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
    if (active) {
        ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.13f, 0.25f, 0.43f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.15f, 0.29f, 0.49f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.12f, 0.22f, 0.39f, 1.0f));
    }
    const bool clicked = ImGui::Button(label, {width, 0.0f});
    if (active) ImGui::PopStyleColor(3);
    ImGui::PopStyleVar();
    return clicked;
}

int wrappedLineCount(std::string_view text, int wrap_columns) {
    if (wrap_columns <= 0 || static_cast<int>(text.size()) <= wrap_columns) return 1;
    int lines = 0;
    size_t cursor = 0;
    while (cursor < text.size()) {
        size_t remaining = text.size() - cursor;
        if (remaining <= static_cast<size_t>(wrap_columns)) {
            ++lines;
            break;
        }
        size_t split = cursor + static_cast<size_t>(wrap_columns);
        size_t break_at = text.rfind(' ', split);
        if (break_at == std::string_view::npos || break_at < cursor + static_cast<size_t>(wrap_columns / 3))
            break_at = split;
        cursor = break_at;
        while (cursor < text.size() && text[cursor] == ' ') ++cursor;
        ++lines;
    }
    return std::max(lines, 1);
}

struct MinimapSegment {
    float weight = 0.0f;
    ImU32 color = syntax_normal;
};

struct MinimapEntry {
    std::vector<MinimapSegment> segments;
    float units = 1.0f;
};

void addMinimapSegment(std::vector<MinimapSegment>& segments, float weight, ImU32 color) {
    if (weight <= 0.0f) return;
    if (!segments.empty() && segments.back().color == color) {
        segments.back().weight += weight;
        return;
    }
    segments.push_back({weight, color});
}

std::vector<MinimapSegment> buildMinimapSegments(const std::string& text, SyntaxLanguage language) {
    std::vector<MinimapSegment> segments;
    if (text.empty()) {
        segments.push_back({0.10f, IM_COL32(0, 0, 0, 0)});
        return segments;
    }
    size_t cursor = 0;
    bool seen_identifier = false;
    while (cursor < text.size()) {
        const unsigned char value = static_cast<unsigned char>(text[cursor]);
        if (std::isspace(value)) {
            size_t end = cursor + 1;
            while (end < text.size() && std::isspace(static_cast<unsigned char>(text[end]))) ++end;
            addMinimapSegment(segments, std::max(0.04f, static_cast<float>(end - cursor) * 0.18f), IM_COL32(0, 0, 0, 0));
            cursor = end;
            continue;
        }

        if ((language == SyntaxLanguage::c || language == SyntaxLanguage::cpp || language == SyntaxLanguage::csharp) &&
                cursor + 1 < text.size() && text[cursor] == '/' && text[cursor + 1] == '/') {
            addMinimapSegment(segments, std::max(0.18f, static_cast<float>(text.size() - cursor) * 0.42f), syntax_comment);
            break;
        }
        if (language == SyntaxLanguage::python && text[cursor] == '#') {
            addMinimapSegment(segments, std::max(0.18f, static_cast<float>(text.size() - cursor) * 0.42f), syntax_comment);
            break;
        }
        if (language == SyntaxLanguage::lua && cursor + 1 < text.size() && text[cursor] == '-' && text[cursor + 1] == '-') {
            addMinimapSegment(segments, std::max(0.18f, static_cast<float>(text.size() - cursor) * 0.42f), syntax_comment);
            break;
        }

        if (text[cursor] == '"' || text[cursor] == '\'' || (language == SyntaxLanguage::javascript && text[cursor] == '`')) {
            const char delimiter = text[cursor];
            size_t end = cursor + 1;
            while (end < text.size()) {
                if (text[end] == delimiter && text[end - 1] != '\\') {
                    ++end;
                    break;
                }
                ++end;
            }
            addMinimapSegment(segments, std::max(0.10f, static_cast<float>(end - cursor) * 0.34f), syntax_string);
            cursor = end;
            continue;
        }

        if (std::isdigit(value)) {
            size_t end = cursor + 1;
            while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) || text[end] == '.' || text[end] == '_')) ++end;
            addMinimapSegment(segments, std::max(0.08f, static_cast<float>(end - cursor) * 0.28f), syntax_number);
            cursor = end;
            continue;
        }

        if (std::isalpha(value) || text[cursor] == '_') {
            size_t end = cursor + 1;
            while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) || text[end] == '_')) ++end;
            const std::string_view word(text.data() + cursor, end - cursor);
            ImU32 color = syntax_normal;
            if (isKeyword(language, word)) color = syntax_keyword;
            else if (isTypeWord(language, word)) color = syntax_type;
            else if (isBuiltin(language, word)) color = syntax_builtin;
            else if (isLiteral(word)) color = syntax_constant;
            else if (word.front() == '@') color = syntax_decorator;
            else if (isMacroName(word)) color = syntax_macro;
            else if (!seen_identifier && (isDeclarationKeyword(word) || isFunctionDeclarationKeyword(word)))
                color = syntax_keyword;
            else if (!seen_identifier && cursor > 0 && text[cursor - 1] == '.') color = syntax_member;
            else if (end < text.size() && text[end] == '(') color = syntax_function;
            addMinimapSegment(segments, std::max(0.08f, static_cast<float>(end - cursor) * 0.26f), color);
            seen_identifier = true;
            cursor = end;
            continue;
        }

        if (std::string_view("+-*/%=!<>&|^~?:#").find(text[cursor]) != std::string_view::npos) {
            size_t end = cursor + 1;
            while (end < text.size() && std::string_view("+-*/%=!<>&|^~?:#").find(text[end]) != std::string_view::npos) ++end;
            const ImU32 color = text[cursor] == '#' ? syntax_preprocessor : syntax_operator;
            addMinimapSegment(segments, std::max(0.06f, static_cast<float>(end - cursor) * 0.22f), color);
            cursor = end;
            continue;
        }

        addMinimapSegment(segments, 0.05f, syntax_normal);
        ++cursor;
    }
    if (segments.empty()) segments.push_back({0.10f, syntax_normal});
    return segments;
}

ImU32 dimMinimapColor(ImU32 color) {
    ImVec4 rgba = ImGui::ColorConvertU32ToFloat4(color);
    rgba.x *= 0.58f;
    rgba.y *= 0.58f;
    rgba.z *= 0.58f;
    rgba.w *= 0.82f;
    return ImGui::ColorConvertFloat4ToU32(rgba);
}

float minimapTotalUnits(const std::vector<MinimapEntry>& entries) {
    float total_units = 0.0f;
    for (const MinimapEntry& entry : entries) total_units += std::max(0.10f, entry.units);
    return std::max(1.0f, total_units);
}

void drawMinimap(const std::vector<MinimapEntry>& entries, const ImVec2& viewport_size, float scale,
        float rendered_content_height, float visible_start, float visible_height, float max_scroll_y,
        float content_height_override = 0.0f) {
    const ImVec2 minimum = ImGui::GetCursorScreenPos();
    const float total_units = minimapTotalUnits(entries);
    const float min_unit_height = scaled(0.72f, scale);
    const float ideal_unit_height = scaled(2.05f, scale);
    const float unit_height = content_height_override > 0.0f
        ? std::max(min_unit_height, content_height_override / total_units)
        : ideal_unit_height;
    const float content_height = std::max(content_height_override, total_units * unit_height);
    const ImVec2 content_size{viewport_size.x, content_height};
    ImGui::InvisibleButton("##code_minimap", content_size);
    const bool hovered = ImGui::IsItemHovered();
    const bool active = ImGui::IsItemActive();
    ImDrawList* draw = ImGui::GetWindowDrawList();
    draw->AddRectFilled(minimum, {minimum.x + viewport_size.x, minimum.y + content_size.y},
        IM_COL32(26, 28, 33, 188), scaled(3.0f, scale));
    draw->AddRect(minimum, {minimum.x + viewport_size.x, minimum.y + content_size.y},
        hovered || active ? IM_COL32(70, 76, 88, 180) : IM_COL32(46, 50, 58, 140), scaled(3.0f, scale));

    if (entries.empty() || content_size.y <= 1.0f) return;

    const float content_left = minimum.x + scaled(5.0f, scale);
    const float content_right = minimum.x + viewport_size.x - scaled(5.0f, scale);
    const float content_width = std::max(1.0f, content_right - content_left);
    const float line_height = std::max(scaled(0.65f, scale), std::min(scaled(2.1f, scale), unit_height));
    float y = minimum.y;
    for (const MinimapEntry& entry : entries) {
        const float entry_units = std::max(0.10f, entry.units);
        const float total_weight = std::max(0.10f, std::accumulate(entry.segments.begin(), entry.segments.end(), 0.0f,
            [](float sum, const MinimapSegment& segment) { return sum + std::max(0.0f, segment.weight); }));
        float x = content_left;
        for (const MinimapSegment& segment : entry.segments) {
            if (segment.color == IM_COL32(0, 0, 0, 0)) {
                x += content_width * (segment.weight / total_weight);
                continue;
            }
            const float width = std::max(scaled(1.0f, scale), content_width * (segment.weight / total_weight));
            draw->AddRectFilled({x, y}, {std::min(content_right, x + width), y + line_height},
                dimMinimapColor(segment.color), scaled(1.0f, scale));
            x += width;
        }
        y += entry_units * unit_height;
    }

    const float rendered_height = std::max(rendered_content_height, 1.0f);
    const float clamped_visible_height = std::clamp(visible_height, 0.0f, rendered_height);
    const float clamped_visible_start = std::clamp(visible_start, 0.0f, std::max(0.0f, rendered_height - clamped_visible_height));
    const float viewport_height = std::clamp(content_size.y * (clamped_visible_height / rendered_height),
        scaled(14.0f, scale), content_size.y);
    const float viewport_y = minimum.y + content_size.y * (clamped_visible_start / rendered_height);
    if ((hovered || active) && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
        const float mouse_ratio = std::clamp((ImGui::GetIO().MousePos.y - minimum.y) / std::max(1.0f, content_size.y), 0.0f, 1.0f);
        const float target = std::clamp(mouse_ratio * rendered_height - clamped_visible_height * 0.5f, 0.0f, max_scroll_y);
        ImGui::SetScrollY(target);
    }
    draw->AddRectFilled({minimum.x + scaled(1.0f, scale), viewport_y},
        {minimum.x + viewport_size.x - scaled(1.0f, scale), viewport_y + viewport_height},
        IM_COL32(109, 129, 170, hovered || active ? 44 : 28), scaled(2.0f, scale));
    draw->AddRect({minimum.x + scaled(1.0f, scale), viewport_y},
        {minimum.x + viewport_size.x - scaled(1.0f, scale), viewport_y + viewport_height},
        IM_COL32(120, 138, 170, 120), scaled(2.0f, scale));
}

void drawCodeLine(const std::string& text, SyntaxLanguage language, SyntaxState& syntax,
        float scale, ImU32 background = IM_COL32(0, 0, 0, 0), float left_gutter = 0.0f,
        float minimum_width = 0.0f, float custom_row_height = 0.0f, int wrap_columns = 0) {
    const float base_row_height = scaled(21.0f, scale);
    const int visual_lines = wrappedLineCount(text, wrap_columns);
    const float row_height = custom_row_height > 0.0f ? custom_row_height : base_row_height * static_cast<float>(visual_lines);
    const ImVec2 start = ImGui::GetCursorScreenPos();
    const float width = minimum_width > 0.0f ? minimum_width :
        std::max(minimum_width, ImGui::CalcTextSize(text.c_str()).x + left_gutter + scaled(12.0f, scale));
    ImGui::InvisibleButton("##code_line", {width, row_height});
    const ImVec2 minimum = ImGui::GetItemRectMin();
    const ImVec2 maximum = ImGui::GetItemRectMax();
    ImDrawList* draw = ImGui::GetWindowDrawList();
    if ((background & IM_COL32_A_MASK) != 0)
        draw->AddRectFilled(minimum, maximum, background);
    if (wrap_columns <= 0 || static_cast<int>(text.size()) <= wrap_columns) {
        drawSyntaxText(draw, {start.x + left_gutter, start.y + scaled(2.0f, scale)}, text, language, syntax);
        return;
    }

    size_t cursor = 0;
    int line_index = 0;
    while (cursor < text.size()) {
        size_t remaining = text.size() - cursor;
        size_t chunk_end = text.size();
        if (remaining > static_cast<size_t>(wrap_columns)) {
            chunk_end = cursor + static_cast<size_t>(wrap_columns);
            size_t break_at = text.rfind(' ', chunk_end);
            if (break_at == std::string::npos || break_at < cursor + static_cast<size_t>(wrap_columns / 3))
                break_at = chunk_end;
            chunk_end = break_at;
        }
        const std::string chunk = text.substr(cursor, chunk_end - cursor);
        drawSyntaxText(draw, {start.x + left_gutter, start.y + scaled(2.0f, scale) + base_row_height * static_cast<float>(line_index)},
            chunk, language, syntax);
        cursor = chunk_end;
        while (cursor < text.size() && text[cursor] == ' ') ++cursor;
        ++line_index;
    }
}

void drawCodeLineNumber(int value, float x, float y, ImU32 color) {
    if (value <= 0) return;
    const std::string text = std::to_string(value);
    ImGui::GetWindowDrawList()->AddText({x, y}, color, text.c_str());
}

std::string ellipsizeText(std::string_view text, float max_width) {
    if (text.empty() || max_width <= 0.0f) return {};

    std::string display{text};
    if (ImGui::CalcTextSize(display.c_str()).x <= max_width) return display;

    constexpr const char* overflow = "...";
    const float overflow_width = ImGui::CalcTextSize(overflow).x;
    if (overflow_width >= max_width) return overflow;

    while (!display.empty() && ImGui::CalcTextSize(display.c_str()).x + overflow_width > max_width) {
        display.pop_back();
    }
    display += overflow;
    return display;
}

void drawEllipsizedText(ImDrawList* draw, ImVec2 position, ImU32 color, std::string_view text,
        float max_width, const ImVec2& clip_min, const ImVec2& clip_max) {
    const std::string display = ellipsizeText(text, max_width);
    if (display.empty()) return;
    draw->PushClipRect(clip_min, clip_max, true);
    draw->AddText(position, color, display.c_str());
    draw->PopClipRect();
}
}

void DiffViewer::open(RepositoryView& repository, GitManager& manager, const std::string& path,
        bool staged, std::string& notice) {
    path_ = path;
    commit_id_.clear();
    staged_ = staged;
    mode_ = Mode::diff;
    scroll_to_top_ = true;
    reload(repository, manager, notice);
}

void DiffViewer::openCommit(RepositoryView& repository, GitManager& manager,
        const std::string& path, const std::string& commit_id, std::string& notice) {
    path_ = path;
    commit_id_ = commit_id;
    staged_ = false;
    mode_ = Mode::diff;
    scroll_to_top_ = true;
    reload(repository, manager, notice);
    if (hunks_.empty()) {
        mode_ = Mode::file;
        loadSupplement(repository, manager, mode_, notice);
        scroll_to_top_ = true;
    }
}

void DiffViewer::close() {
    path_.clear();
    commit_id_.clear();
    file_header_.clear();
    hunks_.clear();
    file_lines_.clear();
    blame_lines_.clear();
    history_entries_.clear();
}

void DiffViewer::parseDiff(const std::string& text) {
    file_header_.clear();
    hunks_.clear();
    Hunk* current = nullptr;
    int old_line = 0;
    int new_line = 0;
    for (const std::string& raw : splitLines(text)) {
        if (raw.starts_with("@@")) {
            hunks_.push_back({});
            current = &hunks_.back();
            current->header = raw;
            parseRange(raw, '-', old_line);
            parseRange(raw, '+', new_line);
            current->patch = file_header_ + raw + "\n";
            continue;
        }
        if (!current) {
            file_header_ += raw + "\n";
            continue;
        }
        Line line;
        line.raw = raw;
        line.text = raw.empty() ? std::string{} : raw.substr(1);
        if (!raw.empty() && raw[0] == '+') {
            line.kind = LineKind::added;
            line.new_number = new_line++;
        } else if (!raw.empty() && raw[0] == '-') {
            line.kind = LineKind::removed;
            line.old_number = old_line++;
        } else if (!raw.starts_with("\\ No newline")) {
            line.old_number = old_line++;
            line.new_number = new_line++;
        }
        current->lines.push_back(std::move(line));
        current->patch += raw + "\n";
    }
}

void DiffViewer::parseBlame(const std::string& text) {
    blame_lines_.clear();
    const std::vector<std::string> lines = splitLines(text);
    std::string previous_hash;
    for (size_t index = 0; index < lines.size();) {
        std::istringstream header(lines[index++]);
        BlameLine line;
        int original_line = 0;
        if (!(header >> line.hash >> original_line >> line.line_number)) continue;
        if (line.hash.size() < 8 || !std::all_of(line.hash.begin(), line.hash.end(), [](unsigned char value) {
                return std::isxdigit(value) != 0;
            })) continue;

        std::time_t author_time = 0;
        while (index < lines.size()) {
            const std::string& field = lines[index++];
            if (!field.empty() && field.front() == '\t') {
                line.text = field.substr(1);
                break;
            }
            const size_t separator = field.find(' ');
            const std::string_view key(field.data(), separator == std::string::npos ? field.size() : separator);
            const std::string value = separator == std::string::npos ? std::string{} : field.substr(separator + 1);
            if (key == "author") line.author = value;
            else if (key == "author-mail") {
                line.email = value;
                if (line.email.size() >= 2 && line.email.front() == '<' && line.email.back() == '>')
                    line.email = line.email.substr(1, line.email.size() - 2);
            }
            else if (key == "author-time") {
                try { author_time = static_cast<std::time_t>(std::stoll(value)); }
                catch (...) { author_time = 0; }
            } else if (key == "summary") line.summary = value;
        }
        if (author_time != 0) {
            std::tm date{};
#ifdef _WIN32
            localtime_s(&date, &author_time);
#else
            localtime_r(&author_time, &date);
#endif
            std::ostringstream formatted;
            formatted << std::put_time(&date, "%Y-%m-%d");
            line.date = formatted.str();
        }
        line.show_attribution = line.hash != previous_hash;
        previous_hash = line.hash;
        blame_lines_.push_back(std::move(line));
    }
}

void DiffViewer::parseHistory(const std::string& text) {
    history_entries_.clear();
    for (const std::string& line : splitLines(text)) {
        if (line.empty()) continue;
        HistoryEntry entry;
        size_t cursor = 0;
        auto consume_field = [&](std::string& out) {
            const size_t separator = line.find('\x1f', cursor);
            if (separator == std::string::npos) {
                out = line.substr(cursor);
                cursor = line.size();
                return;
            }
            out = line.substr(cursor, separator - cursor);
            cursor = separator + 1;
        };
        consume_field(entry.hash);
        consume_field(entry.date);
        consume_field(entry.author);
        consume_field(entry.summary);
        if (!entry.hash.empty() || !entry.summary.empty()) history_entries_.push_back(std::move(entry));
    }
}

void DiffViewer::reload(RepositoryView& repository, GitManager& manager, std::string& notice) {
    std::vector<std::string> arguments;
    if (commit_id_.empty()) {
        arguments = {"diff", "--no-ext-diff", "--no-color", "--unified=3"};
        if (staged_) arguments.push_back("--cached");
    } else {
        arguments = {"show", "--first-parent", "--format=", "--no-ext-diff", "--no-color",
            "--unified=3", commit_id_};
    }
    arguments.insert(arguments.end(), {"--", path_});
    std::string output;
    std::string error;
    if (!manager.captureGit(repository, arguments, output, error)) notice = error;
    parseDiff(output);

    if (hunks_.empty() && commit_id_.empty()) {
        const auto file = std::find_if(repository.working_files.begin(), repository.working_files.end(),
            [this](const WorkingFile& item) { return item.path == path_; });
        if (file != repository.working_files.end() && file->kind == FileChangeKind::added && !staged_) {
            std::ifstream stream(std::filesystem::path(repository.path) / path_, std::ios::binary);
            std::ostringstream contents;
            contents << stream.rdbuf();
            const auto lines = splitLines(contents.str());
            file_header_ = "diff --git a/" + path_ + " b/" + path_ + "\nnew file mode 100644\n--- /dev/null\n+++ b/" + path_ + "\n";
            Hunk hunk;
            hunk.header = "@@ -0,0 +1," + std::to_string(lines.size()) + " @@";
            hunk.patch = file_header_ + hunk.header + "\n";
            int number = 1;
            for (const auto& text : lines) {
                hunk.lines.push_back({0, number++, LineKind::added, text, "+" + text});
                hunk.patch += "+" + text + "\n";
            }
            hunks_.push_back(std::move(hunk));
        }
    }
    file_lines_.clear();
    blame_lines_.clear();
    history_entries_.clear();
    if (mode_ != Mode::diff) loadSupplement(repository, manager, mode_, notice);
}

void DiffViewer::loadSupplement(RepositoryView& repository, GitManager& manager, Mode mode,
        std::string& notice) {
    std::string output;
    std::string error;
    if (mode == Mode::file) {
        if (!commit_id_.empty()) {
            if (!manager.captureGit(repository, {"show", commit_id_ + ":" + path_}, output, error))
                notice = error;
        } else if (staged_) {
            if (!manager.captureGit(repository, {"show", ":" + path_}, output, error)) notice = error;
        } else {
            std::ifstream stream(std::filesystem::path(repository.path) / path_, std::ios::binary);
            std::ostringstream contents;
            contents << stream.rdbuf();
            output = contents.str();
        }
        file_lines_ = splitLines(output);
    } else if (mode == Mode::blame) {
        std::vector<std::string> arguments{"blame", "--line-porcelain"};
        if (!commit_id_.empty()) arguments.push_back(commit_id_);
        arguments.insert(arguments.end(), {"--", path_});
        if (!manager.captureGit(repository, arguments, output, error)) notice = error;
        parseBlame(output);
    } else if (mode == Mode::history) {
        std::vector<std::string> arguments{
            "log", "--follow", "--date=short", "--pretty=format:%h%x1f%ad%x1f%an%x1f%s"};
        if (!commit_id_.empty()) arguments.push_back(commit_id_);
        arguments.insert(arguments.end(), {"--", path_});
        if (!manager.captureGit(repository, arguments, output, error)) notice = error;
        parseHistory(output);
    }
}

void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCache* avatars,
        float scale, ImFont* code_font, std::string& notice) {
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {scaled(8, scale), scaled(5, scale)});
    ImGui::BeginChild("diff_viewer", {-1, -1}, ImGuiChildFlags_None,
        ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
    ImGui::TextColored(ImVec4(0.94f, 0.66f, 0.25f, 1), ICON_TB_PEN);
    ImGui::SameLine(0, scaled(7, scale));
    ImGui::TextUnformatted(path_.c_str());
    const bool historical = !commit_id_.empty();
    const float top_right_width = scaled(72.0f, scale);
    ImGui::SameLine(std::max(scaled(160.0f, scale), ImGui::GetWindowWidth() - top_right_width));
    ImGui::TextDisabled("UTF-8");
    ImGui::SameLine(0, scaled(10.0f, scale));
    if (compactButton(ICON_TB_XMARK)) close();
    ImGui::Separator();

    const float segment_width = scaled(86.0f, scale);
    const float file_group_width = segment_width * 2.0f;
    const float blame_group_width = segment_width * 2.0f;
    const float wrap_width = scaled(34.0f, scale);
    const float between_groups = scaled(22.0f, scale);
    const float wrap_gap = scaled(12.0f, scale);
    const float toolbar_width = file_group_width + between_groups + blame_group_width + wrap_gap + wrap_width;
    ImGui::SetCursorPosX(std::max(0.0f, (ImGui::GetWindowWidth() - toolbar_width) * 0.5f));
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {0.0f, ImGui::GetStyle().ItemSpacing.y});
    if (toolbarSegmentButton("File View", mode_ == Mode::file, segment_width)) {
        mode_ = Mode::file;
        loadSupplement(repository, manager, mode_, notice);
        scroll_to_top_ = true;
    }
    ImGui::SameLine();
    if (toolbarSegmentButton("Diff View", mode_ == Mode::diff, segment_width)) {
        mode_ = Mode::diff;
        scroll_to_top_ = true;
    }
    ImGui::SameLine(0, between_groups);
    if (toolbarSegmentButton("Blame", mode_ == Mode::blame, segment_width)) {
        mode_ = Mode::blame;
        loadSupplement(repository, manager, mode_, notice);
    }
    ImGui::SameLine();
    if (toolbarSegmentButton("History", mode_ == Mode::history, segment_width)) {
        mode_ = Mode::history;
        loadSupplement(repository, manager, mode_, notice);
    }
    ImGui::PopStyleVar();
    ImGui::SameLine(0, wrap_gap);
    toolbarSegmentButton(ICON_TB_BARS, line_wrap_, wrap_width);
    if (ImGui::IsItemClicked()) line_wrap_ = !line_wrap_;
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort))
        ImGui::SetTooltip("%s", line_wrap_ ? "Disable line wrap" : "Enable line wrap");
    ImGui::Separator();

    const bool show_minimap = mode_ == Mode::diff || mode_ == Mode::file || mode_ == Mode::blame || mode_ == Mode::history;
    const float minimap_width = scaled(168.0f, scale);
    const float minimap_gap = scaled(10.0f, scale);
    const float scrollbar_width = ImGui::GetStyle().ScrollbarSize;
    const SyntaxLanguage language = languageForPath(path_);
    float main_scroll_y = 0.0f;
    float main_window_height = 0.0f;
    float rendered_content_height = 0.0f;
    std::vector<MinimapEntry> minimap_entries;
    if (show_minimap) {
        minimap_entries.push_back({{{0.10f, IM_COL32(0, 0, 0, 0)}}, 0.65f});
        if (mode_ == Mode::diff) {
            for (size_t hunk_index = 0; hunk_index < hunks_.size(); ++hunk_index) {
                if (hunk_index > 0) {
                    minimap_entries.push_back({{{0.10f, IM_COL32(0, 0, 0, 0)}}, 0.55f});
                    minimap_entries.push_back({{{0.10f, IM_COL32(0, 0, 0, 0)}}, 0.45f});
                }
                for (const Line& line : hunks_[hunk_index].lines) {
                    minimap_entries.push_back({buildMinimapSegments(line.text, language),
                        line.kind == LineKind::context ? 1.0f : 1.0f});
                }
                minimap_entries.push_back({{{0.10f, IM_COL32(0, 0, 0, 0)}}, 0.30f});
            }
        } else if (mode_ == Mode::file) {
            minimap_entries.reserve(file_lines_.size());
            for (const std::string& line : file_lines_)
                minimap_entries.push_back({buildMinimapSegments(line, language), 1.0f});
        } else if (mode_ == Mode::blame) {
            minimap_entries.reserve(blame_lines_.size());
            for (const BlameLine& line : blame_lines_)
                minimap_entries.push_back({buildMinimapSegments(line.text, language), line.show_attribution ? 1.35f : 1.0f});
        } else {
            minimap_entries.reserve(history_entries_.size());
            for (const HistoryEntry& entry : history_entries_)
                minimap_entries.push_back({buildMinimapSegments(entry.summary, SyntaxLanguage::plain), 1.7f});
        }
    }

    ImGuiWindowFlags content_flags = line_wrap_ ? ImGuiWindowFlags_None : ImGuiWindowFlags_HorizontalScrollbar;
    const bool request_scroll_to_top = scroll_to_top_;
    if (request_scroll_to_top) ImGui::SetNextWindowScroll({0.0f, 0.0f});
    ImGui::BeginChild("diff_content_main", ImVec2{-1, -1}, ImGuiChildFlags_None, content_flags);
    scroll_to_top_ = false;
    const bool use_code_font = code_font && mode_ != Mode::history;
    if (use_code_font) ImGui::PushFont(code_font, 0.0f);
    const float row_height = scaled(21, scale);
    const ImVec2 child_minimum = ImGui::GetWindowPos();
    const ImVec2 child_size = ImGui::GetWindowSize();
    const float content_width = std::max(
        ImGui::GetContentRegionAvail().x - (show_minimap ? minimap_width + minimap_gap + scrollbar_width + scaled(6.0f, scale) : 0.0f),
        scaled(200.0f, scale));
    const float wrapable_text_width = std::max(scaled(32.0f, scale), content_width - scaled(12.0f, scale));
    const float code_character_width = ImGui::CalcTextSize("M").x;
    const int wrap_columns = line_wrap_ && code_character_width > 0.0f
        ? std::max(12, static_cast<int>(wrapable_text_width / code_character_width))
        : 0;

    // Keep the first source row clear of the toolbar and aligned with a normal row inset.
    ImGui::Dummy({0.0f, row_height});

    if (mode_ == Mode::diff) {
        if (hunks_.empty()) ImGui::TextDisabled("No textual diff is available for this file.");
        int line_id = 0;
        int pending_hunk = -1;
        enum class HunkAction { none, stage, discard, unstage };
        HunkAction pending_action = HunkAction::none;
        for (size_t hunk_index = 0; hunk_index < hunks_.size(); ++hunk_index) {
            SyntaxState old_syntax;
            SyntaxState new_syntax;
            ImGui::PushID(static_cast<int>(hunk_index));
            if (hunk_index > 0) {
                ImGui::Dummy({0.0f, scaled(11.0f, scale)});
                ImGui::Separator();
                ImGui::Dummy({0.0f, scaled(8.0f, scale)});
            }
            ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.62f, 0.64f, 0.68f, 1.0f));
            ImGui::TextUnformatted(hunks_[hunk_index].header.c_str());
            ImGui::PopStyleColor();
            ImGui::SameLine(std::max(scaled(220, scale), ImGui::GetWindowWidth() - scaled(205, scale)));
            if (!historical && !staged_) {
                ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.94f, 0.44f, 0.44f, 1));
                if (ImGui::SmallButton("Discard Hunk")) {
                    pending_hunk = static_cast<int>(hunk_index);
                    pending_action = HunkAction::discard;
                }
                ImGui::PopStyleColor();
                ImGui::SameLine();
                ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.43f, 0.90f, 0.51f, 1));
                if (ImGui::SmallButton("Stage Hunk")) {
                    pending_hunk = static_cast<int>(hunk_index);
                    pending_action = HunkAction::stage;
                }
                ImGui::PopStyleColor();
            } else if (!historical && ImGui::SmallButton("Unstage Hunk")) {
                pending_hunk = static_cast<int>(hunk_index);
                pending_action = HunkAction::unstage;
            }
            ImGui::Separator();
            ImGui::Dummy({0.0f, scaled(4.0f, scale)});
            const float number_column = scaled(44.0f, scale);
            const float code_gutter = number_column * 2.0f + scaled(14.0f, scale);
            for (const Line& line : hunks_[hunk_index].lines) {
                ImGui::PushID(line_id++);
                const ImVec2 line_minimum = ImGui::GetCursorScreenPos();
                const ImU32 background = line.kind == LineKind::added ? IM_COL32(30, 68, 46, 170) :
                    line.kind == LineKind::removed ? IM_COL32(86, 38, 42, 170) : IM_COL32(0, 0, 0, 0);
                drawCodeLine(line.text, language,
                    line.kind == LineKind::removed ? old_syntax : new_syntax,
                    scale, background, code_gutter, content_width, 0.0f, wrap_columns);
                const float text_y = line_minimum.y + scaled(2.0f, scale);
                drawCodeLineNumber(line.old_number, line_minimum.x + scaled(6.0f, scale), text_y,
                    IM_COL32(126, 132, 142, 255));
                drawCodeLineNumber(line.new_number, line_minimum.x + number_column + scaled(6.0f, scale), text_y,
                    IM_COL32(126, 132, 142, 255));
                ImGui::PopID();
            }
            ImGui::PopID();
        }
        if (pending_hunk >= 0 && pending_hunk < static_cast<int>(hunks_.size())) {
            const bool cached = pending_action != HunkAction::discard;
            const bool reverse = pending_action != HunkAction::stage;
            if (manager.applyPatch(repository, hunks_[pending_hunk].patch, cached, reverse, notice)) {
                if (pending_action == HunkAction::stage) close();
                reload(repository, manager, notice);
            }
        }
    } else if (mode_ == Mode::blame) {
        if (blame_lines_.empty()) {
            ImGui::TextDisabled("No blame data is available for this file.");
        } else {
            SyntaxState syntax;
            const float info_width = scaled(270.0f, scale);
            const float line_number_width = scaled(48.0f, scale);
            const float code_gutter = info_width + line_number_width + scaled(14.0f, scale);
            for (size_t index = 0; index < blame_lines_.size(); ++index) {
                ImGui::PushID(static_cast<int>(index));
                const BlameLine& line = blame_lines_[index];
                const ImVec2 line_minimum = ImGui::GetCursorScreenPos();
                const ImU32 accent = blameColor(line.hash, line.show_attribution ? 255 : 190);
                const ImU32 background = line.show_attribution ? IM_COL32(39, 42, 50, 150) : IM_COL32(31, 34, 40, 105);
                const float blame_row_height = line.show_attribution ? scaled(28.0f, scale) : scaled(21.0f, scale);
                drawCodeLine(line.text, language, syntax, scale, background, code_gutter, content_width,
                    blame_row_height, wrap_columns);
                ImDrawList* draw = ImGui::GetWindowDrawList();
                const ImVec2 line_maximum = ImGui::GetItemRectMax();
                draw->AddRectFilled({line_minimum.x, line_minimum.y},
                    {line_minimum.x + scaled(3.0f, scale), line_maximum.y}, accent);
                drawCodeLineNumber(line.line_number, line_minimum.x + info_width + scaled(6.0f, scale),
                    line_minimum.y + scaled(2.0f, scale), IM_COL32(126, 132, 142, 255));
                if (line.show_attribution) {
                    const float avatar_size = scaled(16.0f, scale);
                    const ImVec2 avatar_min{line_minimum.x + scaled(8.0f, scale), line_minimum.y + scaled(2.0f, scale)};
                    const unsigned int avatar_texture = avatars ? avatars->textureFor(line.email) : 0;
                    if (avatar_texture) {
                        draw->AddImageRounded(ImTextureRef(static_cast<ImTextureID>(avatar_texture)),
                            avatar_min, {avatar_min.x + avatar_size, avatar_min.y + avatar_size},
                            {0, 0}, {1, 1}, IM_COL32_WHITE, scaled(3.0f, scale));
                    } else {
                        draw->AddRectFilled(avatar_min, {avatar_min.x + avatar_size, avatar_min.y + avatar_size},
                            accent, scaled(3.0f, scale));
                    }
                    const float info_x = avatar_min.x + avatar_size + scaled(8.0f, scale);
                    const float info_right = line_minimum.x + info_width - scaled(10.0f, scale);
                    const float info_text_width = std::max(0.0f, info_right - info_x);
                    const ImVec2 info_clip_min{info_x, line_minimum.y};
                    const ImVec2 info_clip_max{info_right, line_maximum.y};
                    const std::string author = line.author.empty() ? "Unknown author" : line.author;
                    std::string summary = line.summary.empty() ? "(no summary)" : line.summary;
                    if (!line.date.empty()) summary += "  " + line.date;
                    drawEllipsizedText(draw, {info_x, line_minimum.y + scaled(1.0f, scale)},
                        IM_COL32(216, 220, 226, 255), author, info_text_width, info_clip_min, info_clip_max);
                    drawEllipsizedText(draw, {info_x, line_minimum.y + scaled(10.0f, scale)},
                        IM_COL32(134, 140, 151, 255), summary, info_text_width, info_clip_min, info_clip_max);
                } else {
                    draw->AddText({line_minimum.x + scaled(16.0f, scale), line_minimum.y + scaled(2.0f, scale)},
                        IM_COL32(92, 99, 110, 255), "...");
                }
                ImGui::PopID();
            }
        }
    } else if (mode_ == Mode::file) {
        const std::vector<std::string>* lines = &file_lines_;
        if (mode_ == Mode::file) {
            SyntaxState syntax;
            const float code_gutter = scaled(52.0f, scale);
            for (size_t index = 0; index < lines->size(); ++index) {
                ImGui::PushID(static_cast<int>(index));
                const ImVec2 line_minimum = ImGui::GetCursorScreenPos();
                drawCodeLine((*lines)[index], language, syntax, scale, IM_COL32(0, 0, 0, 0),
                    code_gutter, content_width, 0.0f, wrap_columns);
                drawCodeLineNumber(static_cast<int>(index + 1), line_minimum.x + scaled(6.0f, scale),
                    line_minimum.y + scaled(2.0f, scale), IM_COL32(126, 132, 142, 255));
                ImGui::PopID();
            }
        }
        if (lines->empty()) ImGui::TextDisabled("No data is available for this view.");
    } else {
        if (history_entries_.empty()) {
            ImGui::TextDisabled("No history data is available for this file.");
        } else {
            for (size_t index = 0; index < history_entries_.size(); ++index) {
                ImGui::PushID(static_cast<int>(index));
                const HistoryEntry& entry = history_entries_[index];
                const ImVec2 row_min = ImGui::GetCursorScreenPos();
                const float history_row_height = scaled(36.0f, scale);
                ImGui::InvisibleButton("##history_row", {content_width, history_row_height});
                const ImVec2 row_max = ImGui::GetItemRectMax();
                ImDrawList* draw = ImGui::GetWindowDrawList();
                const bool hovered = ImGui::IsItemHovered();
                draw->AddRectFilled(row_min, row_max,
                    hovered ? IM_COL32(49, 53, 61, 180) : IM_COL32(37, 40, 47, 130), scaled(4.0f, scale));
                draw->AddRectFilled(row_min, {row_min.x + scaled(3.0f, scale), row_max.y},
                    IM_COL32(23, 181, 204, 220), scaled(3.0f, scale));

                const std::string hash_text = entry.hash.empty() ? "-" : entry.hash;
                draw->AddText({row_min.x + scaled(10.0f, scale), row_min.y + scaled(4.0f, scale)},
                    IM_COL32(96, 184, 235, 255), hash_text.c_str());

                const std::string author_date = entry.author +
                    (entry.date.empty() ? std::string{} : std::string("  ") + entry.date);
                draw->AddText({row_min.x + scaled(78.0f, scale), row_min.y + scaled(4.0f, scale)},
                    IM_COL32(188, 192, 198, 255), author_date.c_str());

                const std::string summary = entry.summary.empty() ? "(no summary)" : entry.summary;
                draw->AddText({row_min.x + scaled(10.0f, scale), row_min.y + scaled(18.0f, scale)},
                    IM_COL32(219, 223, 229, 255), summary.c_str());
                ImGui::PopID();
            }
        }
    }
    main_scroll_y = ImGui::GetScrollY();
    main_window_height = ImGui::GetWindowHeight();
    rendered_content_height = ImGui::GetCursorPosY();
    const float max_scroll_y = ImGui::GetScrollMaxY();
    if (show_minimap) {
        const float total_units = minimapTotalUnits(minimap_entries);
        const float min_unit_height = scaled(0.72f, scale);
        const float ideal_unit_height = scaled(2.05f, scale);
        const float max_minimap_height = std::max(scaled(24.0f, scale), child_size.y - scaled(8.0f, scale));
        const float fitted_unit_height = std::clamp(max_minimap_height / total_units, min_unit_height, ideal_unit_height);
        const float minimap_content_height = total_units * fitted_unit_height;
        const float minimap_height = std::min(max_minimap_height, minimap_content_height);
        const float minimap_x = child_minimum.x + child_size.x - scrollbar_width - minimap_width - scaled(4.0f, scale);
        const float minimap_y = child_minimum.y + scaled(4.0f, scale);
        ImGui::SetCursorScreenPos({minimap_x, minimap_y});
        ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
        ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
        ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, ImVec4(0.28f, 0.31f, 0.37f, 0.55f));
        ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(0.36f, 0.40f, 0.47f, 0.72f));
        ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(0.44f, 0.49f, 0.57f, 0.86f));
        if (request_scroll_to_top) ImGui::SetNextWindowScroll({0.0f, 0.0f});
        ImGui::BeginChild("diff_content_minimap", {minimap_width, minimap_height},
            ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove);
        drawMinimap(minimap_entries, {minimap_width - scaled(1.0f, scale), minimap_height}, scale,
            rendered_content_height, main_scroll_y, main_window_height, max_scroll_y, minimap_content_height);
        ImGui::EndChild();
        ImGui::PopStyleColor(5);
    }
    if (use_code_font) ImGui::PopFont();
    ImGui::EndChild();
    ImGui::EndChild();
    ImGui::PopStyleVar();
}
