// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Pure implementation of the console wrap layout / hit-test / selection. No ImGui — the
// algorithm mirrors the former inline code in ConsoleTab exactly, with text measurement
// injected via ConsoleTextMeasure.

#include "console_text_layout.h"

#include <algorithm>

namespace dragonx {
namespace ui {

ConsoleLayout BuildConsoleLayout(int visibleCount, const ConsoleLineAccessor& getVisibleText,
                                 float wrapWidth, float lineHeight, float interLineGap,
                                 const ConsoleTextMeasure& measure)
{
    ConsoleLayout L;
    if (visibleCount < 0) visibleCount = 0;
    L.segments.resize(visibleCount);
    L.heights.resize(visibleCount);
    L.cumulativeY.resize(visibleCount);
    L.totalHeight = 0.0f;

    for (int vi = 0; vi < visibleCount; vi++) {
        const std::string& text = getVisibleText(vi);
        auto& segs = L.segments[vi];
        segs.clear();

        if (text.empty()) {
            segs.push_back({0, 0, 0.0f, lineHeight});
            L.cumulativeY[vi] = L.totalHeight;
            L.heights[vi] = lineHeight + interLineGap;
            L.totalHeight += L.heights[vi];
            continue;
        }

        const char* textStart = text.c_str();
        const char* textEnd   = textStart + text.size();
        const char* cur       = textStart;
        float segY = 0.0f;

        while (cur < textEnd) {
            const char* wrapPos = measure.wrapPosition(cur, textEnd, wrapWidth);
            if (wrapPos <= cur) wrapPos = cur + 1;         // ensure forward progress
            if (*cur == '\n') { cur++; continue; }         // skip a newline ending the prior seg

            int byteStart = static_cast<int>(cur - textStart);
            int byteEnd   = static_cast<int>(wrapPos - textStart);
            if (byteEnd > byteStart && text[byteEnd - 1] == '\n') byteEnd--;  // trim trailing '\n'

            segs.push_back({byteStart, byteEnd, segY, lineHeight});
            segY += lineHeight;
            cur = wrapPos;
        }

        if (segs.empty()) {
            segs.push_back({0, 0, 0.0f, lineHeight});
            segY = lineHeight;
        }

        L.cumulativeY[vi] = L.totalHeight;
        L.heights[vi] = segY + interLineGap;
        L.totalHeight += L.heights[vi];
    }

    return L;
}

ConsoleHit HitTestConsoleLayout(const ConsoleLayout& layout, int visibleCount,
                                const ConsoleLineAccessor& getVisibleText,
                                float relX, float relY,
                                const ConsoleTextMeasure& measure)
{
    ConsoleHit hit{0, 0};
    if (visibleCount <= 0 || layout.cumulativeY.empty()) return hit;

    // Binary search for the visible line containing relY.
    int visibleLine = 0;
    {
        int lo = 0, hi = static_cast<int>(layout.cumulativeY.size()) - 1;
        while (lo < hi) {
            int mid = (lo + hi + 1) / 2;
            if (layout.cumulativeY[mid] <= relY) lo = mid;
            else hi = mid - 1;
        }
        visibleLine = lo;
    }

    if (visibleLine < 0) visibleLine = 0;
    if (visibleLine >= visibleCount) {
        visibleLine = visibleCount - 1;
        hit.visibleRow = visibleLine;
        hit.col = static_cast<int>(getVisibleText(visibleLine).size());
        return hit;
    }

    hit.visibleRow = visibleLine;
    const std::string& text = getVisibleText(visibleLine);
    if (text.empty()) { hit.col = 0; return hit; }

    // Which wrap sub-row does relY fall into.
    const auto& segs = layout.segments[visibleLine];
    float lineRelY = relY - layout.cumulativeY[visibleLine];
    int segIdx = 0;
    for (int s = 0; s < static_cast<int>(segs.size()); s++)
        if (lineRelY >= segs[s].yOffset) segIdx = s;
    const auto& seg = segs[segIdx];

    if (relX <= 0.0f) { hit.col = seg.byteStart; return hit; }

    const char* segStart = text.c_str() + seg.byteStart;
    int segLen = seg.byteEnd - seg.byteStart;
    hit.col = seg.byteEnd;  // default: past the end of the segment
    for (int c = 0; c < segLen; c++) {
        float wCur  = measure.width(segStart, segStart + c + 1);
        float wPrev = (c > 0) ? measure.width(segStart, segStart + c) : 0.0f;
        float charMid = (wPrev + wCur) * 0.5f;
        if (relX < charMid) { hit.col = seg.byteStart + c; return hit; }
    }
    return hit;
}

std::string ExtractConsoleSelection(int lineCount, const ConsoleLineAccessor& getLine,
                                    const std::vector<int>& visibleIndices,
                                    ConsoleTextPos start, ConsoleTextPos end)
{
    if (start.line < 0 || start.line >= lineCount) return "";

    // Only restrict to visible lines when a filter is active (fewer visible than total).
    const bool hasFilter = !visibleIndices.empty() &&
                           static_cast<int>(visibleIndices.size()) < lineCount;
    std::vector<bool> visible;
    if (hasFilter) {
        visible.assign(lineCount, false);
        for (int idx : visibleIndices)
            if (idx >= 0 && idx < lineCount) visible[idx] = true;
    }

    std::string result;
    bool firstLine = true;
    for (int i = start.line; i <= end.line && i < lineCount; i++) {
        if (hasFilter && !visible[i]) continue;

        const std::string& text = getLine(i);
        int colStart = 0;
        int colEnd = static_cast<int>(text.size());
        if (i == start.line) colStart = std::min(start.col, static_cast<int>(text.size()));
        if (i == end.line)   colEnd   = std::min(end.col,   static_cast<int>(text.size()));

        if (!firstLine) result += "\n";
        firstLine = false;

        if (colStart < colEnd) result += text.substr(colStart, colEnd - colStart);
    }
    return result;
}

} // namespace ui
} // namespace dragonx
