#include "hittest.h"
#include "clay.h"
#include "utf8.h"
#include <string.h>
#include <float.h>

#define MEASURE_BUF_SIZE 2048

static float measure_width(SDL2_Font *fonts, int fontId, int fontSize, const char *text, int start, int n) {
    if (n <= 0) return 0.0f;
    char buf[MEASURE_BUF_SIZE];
    if (n >= MEASURE_BUF_SIZE) n = MEASURE_BUF_SIZE - 1;
    memcpy(buf, text + start, (size_t)n);
    buf[n] = '\0';
    TTF_Font *font = fonts[fontId].font;
    TTF_SetFontSize(font, fontSize);
    int w = 0, h = 0;
    TTF_SizeUTF8(font, buf, &w, &h);
    return (float)w;
}

static Clay_ElementData get_line_data(int globalLineIndex) {
    return Clay_GetElementData(CLAY_IDI("Line", globalLineIndex));
}

static Clay_ElementData get_seg_data(int globalSegIndex) {
    return Clay_GetElementData(CLAY_IDI("Seg", globalSegIndex));
}

/* Finds the array index (into cache->lines) of the line that should own `offset` within `blockIndex`.
   When `offset` sits exactly on the boundary between two lines, prefers the later one (start of the
   next visual line), matching how a caret naturally rests after a wrapped word. */
static int find_line_for_offset(LayoutCache *cache, int blockIndex, int offset) {
    int best = -1;
    for (int i = 0; i < cache->lineCount; i++) {
        CachedLine *l = &cache->lines[i];
        if (l->blockIndex != blockIndex) continue;
        if (l->srcStart <= offset) best = i;
    }
    return best;
}

/* Finds the byte offset within `lineIdx` whose glyph is closest to screen-space `targetX`. */
static int find_offset_in_line(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int lineIdx, float targetX) {
    CachedLine *line = &cache->lines[lineIdx];
    if (line->segStart == line->segEnd) return line->srcStart;

    const char *text = doc->blocks[line->blockIndex].text.data;

    int bestSeg = -1;
    Clay_BoundingBox bestBox = {0};
    for (int i = line->segStart; i < line->segEnd; i++) {
        Clay_ElementData d = get_seg_data(cache->segs[i].globalSegIndex);
        if (!d.found) continue;
        if (bestSeg < 0) { bestSeg = i; bestBox = d.boundingBox; }
        if (targetX >= d.boundingBox.x && targetX < d.boundingBox.x + d.boundingBox.width) {
            bestSeg = i;
            bestBox = d.boundingBox;
            break;
        }
        if (targetX >= d.boundingBox.x) { bestSeg = i; bestBox = d.boundingBox; }
    }
    if (bestSeg < 0) return line->srcStart;

    CachedSegment *seg = &cache->segs[bestSeg];
    float localTarget = targetX - bestBox.x;
    if (localTarget <= 0.0f) return seg->srcStart;

    int pos = seg->srcStart;
    while (pos < seg->srcEnd) {
        int clen = utf8_next_len(text, pos, seg->srcEnd);
        float wBefore = measure_width(fonts, seg->fontId, seg->fontSize, text, seg->srcStart, pos - seg->srcStart);
        float wAfter = measure_width(fonts, seg->fontId, seg->fontSize, text, seg->srcStart, pos + clen - seg->srcStart);
        if (wAfter >= localTarget) {
            float mid = (wBefore + wAfter) * 0.5f;
            return (localTarget < mid) ? pos : pos + clen;
        }
        pos += clen;
    }
    return seg->srcEnd;
}

HitResult hittest_point(LayoutCache *cache, Document *doc, SDL2_Font *fonts, Clay_Vector2 point) {
    HitResult r = {0, 0};
    if (cache->lineCount == 0 || doc->count == 0) return r;

    int bestLine = -1;
    float bestDist = FLT_MAX;
    for (int i = 0; i < cache->lineCount; i++) {
        Clay_ElementData d = get_line_data(cache->lines[i].globalLineIndex);
        if (!d.found) continue;
        float top = d.boundingBox.y, bot = top + d.boundingBox.height;
        if (point.y >= top && point.y < bot) { bestLine = i; bestDist = 0.0f; break; }
        float dist = point.y < top ? (top - point.y) : (point.y - bot);
        if (dist < bestDist) { bestDist = dist; bestLine = i; }
    }
    if (bestLine < 0) {
        r.blockIndex = doc->count - 1;
        r.offset = doc->blocks[r.blockIndex].text.len;
        return r;
    }

    r.blockIndex = cache->lines[bestLine].blockIndex;
    r.offset = find_offset_in_line(cache, doc, fonts, bestLine, point.x);
    return r;
}

VerticalMoveResult hittest_vertical(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int blockIndex, int offset, float preferredX, int dir) {
    VerticalMoveResult r = {0, 0, 0};
    int curLine = find_line_for_offset(cache, blockIndex, offset);
    if (curLine < 0) return r;
    int targetLine = curLine + (dir < 0 ? -1 : 1);
    if (targetLine < 0 || targetLine >= cache->lineCount) return r;

    r.found = 1;
    r.blockIndex = cache->lines[targetLine].blockIndex;
    r.offset = find_offset_in_line(cache, doc, fonts, targetLine, preferredX);
    return r;
}

int hittest_offset_for_block_x(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int blockIndex, float targetX) {
    for (int i = 0; i < cache->lineCount; i++) {
        if (cache->lines[i].blockIndex == blockIndex) {
            return find_offset_in_line(cache, doc, fonts, i, targetX);
        }
    }
    return 0;
}

/* Screen-space x of byte `offset` within visual line `line` (whose Clay element data,
   `lineData`, the caller already has). Shared by hittest_caret and hittest_line_range_box. */
static float x_at_offset_in_line(LayoutCache *cache, Document *doc, SDL2_Font *fonts, CachedLine *line, Clay_ElementData lineData, int offset) {
    if (line->segStart == line->segEnd) return lineData.boundingBox.x;

    int bestSeg = -1;
    for (int i = line->segStart; i < line->segEnd; i++) {
        if (cache->segs[i].srcStart <= offset) bestSeg = i;
    }
    if (bestSeg < 0) bestSeg = line->segStart;

    CachedSegment *seg = &cache->segs[bestSeg];
    Clay_ElementData sd = get_seg_data(seg->globalSegIndex);
    if (!sd.found) return lineData.boundingBox.x;

    int within = offset - seg->srcStart;
    if (within < 0) within = 0;
    if (within > seg->srcEnd - seg->srcStart) within = seg->srcEnd - seg->srcStart;

    const char *text = doc->blocks[line->blockIndex].text.data;
    float w = measure_width(fonts, seg->fontId, seg->fontSize, text, seg->srcStart, within);
    return sd.boundingBox.x + w;
}

int hittest_caret(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int blockIndex, int offset, float *outX, float *outY, float *outHeight) {
    int lineIdx = find_line_for_offset(cache, blockIndex, offset);
    if (lineIdx < 0) return 0;
    CachedLine *line = &cache->lines[lineIdx];
    Clay_ElementData ld = get_line_data(line->globalLineIndex);
    if (!ld.found) return 0;

    *outY = ld.boundingBox.y;
    *outHeight = ld.boundingBox.height;
    *outX = x_at_offset_in_line(cache, doc, fonts, line, ld, offset);
    return 1;
}

int hittest_line_range_box(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int lineIdx,
                            int rangeStart, int rangeEnd, float *outX, float *outY, float *outW, float *outHeight) {
    if (lineIdx < 0 || lineIdx >= cache->lineCount) return 0;
    CachedLine *line = &cache->lines[lineIdx];
    int lo = rangeStart < line->srcStart ? line->srcStart : rangeStart;
    int hi = rangeEnd > line->srcEnd ? line->srcEnd : rangeEnd;
    if (hi < lo) return 0;

    Clay_ElementData ld = get_line_data(line->globalLineIndex);
    if (!ld.found) return 0;

    float x0 = x_at_offset_in_line(cache, doc, fonts, line, ld, lo);
    float x1 = x_at_offset_in_line(cache, doc, fonts, line, ld, hi);
    float w = x1 - x0;
    if (w < 4.0f) w = 4.0f; /* visible sliver for a fully-selected blank line */

    *outX = x0;
    *outY = ld.boundingBox.y;
    *outW = w;
    *outHeight = ld.boundingBox.height;
    return 1;
}
