// textHelp.c -- Shared text editing infrastructure
//
// Provides clipboard, multi-click detection, word boundary logic,
// cross-widget selection clearing, and the single-line text editing
// engine. Shared across multiple widget DXEs (TextInput, TextArea,
// ComboBox, Spinner, AnsiTerm) via the texthelp.lib DXE.

#include "textHelp.h"

#include <ctype.h>
#include <time.h>

// ============================================================
// Static state
// ============================================================

// Cursor blink state
#define CURSOR_BLINK_MS 250
static clock_t sCursorBlinkTime = 0;

// Track the widget that last had an active selection so we can
// clear it in O(1) instead of walking every widget in every window.
static WidgetT *sLastSelectedWidget = NULL;

// ============================================================
// Static helper prototypes
// ============================================================

static bool clearSelectionOnWidget(WidgetT *w);
static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd);
static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize);
static int32_t wordBoundaryLeft(const char *buf, int32_t pos);
static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos);


// ============================================================
// wgtUpdateCursorBlink
// ============================================================

static void textHelpInit(void) __attribute__((constructor));
static void textHelpInit(void) {
    sCursorBlinkFn = wgtUpdateCursorBlink;
}


void wgtUpdateCursorBlink(void) {
    clock_t now      = clock();
    clock_t interval = (clock_t)CURSOR_BLINK_MS * CLOCKS_PER_SEC / 1000;

    if ((now - sCursorBlinkTime) >= interval) {
        sCursorBlinkTime = now;
        sCursorBlinkOn   = !sCursorBlinkOn;

        if (sFocusedWidget) {
            wgtInvalidatePaint(sFocusedWidget);
        }
    }
}


// ============================================================
// clearOtherSelections
// ============================================================
//
// Clears selection on the previously-selected widget (if different
// from the newly-focused one). Validates that the previous widget's
// window is still in the window stack before touching it -- the
// window may have been closed since sLastSelectedWidget was set.
// If the previous widget was in a different window, that window
// gets a full repaint to clear the stale selection highlight.

void clearOtherSelections(WidgetT *except) {
    if (!except || !except->window || !except->window->widgetRoot) {
        return;
    }

    WidgetT *prev = sLastSelectedWidget;
    sLastSelectedWidget = except;

    if (!prev || prev == except) {
        return;
    }

    // Verify the widget is still alive (its window still in the stack)
    WindowT *prevWin = prev->window;

    if (!prevWin) {
        return;
    }

    AppContextT *ctx = wgtGetContext(except);

    if (!ctx) {
        return;
    }

    bool found = false;

    for (int32_t i = 0; i < ctx->stack.count; i++) {
        if (ctx->stack.windows[i] == prevWin) {
            found = true;
            break;
        }
    }

    if (!found) {
        return;
    }

    if (clearSelectionOnWidget(prev) && prevWin != except->window) {
        dvxInvalidateWindow(ctx, prevWin);
    }
}


static bool clearSelectionOnWidget(WidgetT *w) {
    return wclsClearSelection(w);
}




bool isWordChar(char c) {
    return isalnum((unsigned char)c) || c == '_';
}


// ============================================================
// Shared undo/selection helpers
// ============================================================

static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd) {
    int32_t lo = *pSelStart < *pSelEnd ? *pSelStart : *pSelEnd;
    int32_t hi = *pSelStart < *pSelEnd ? *pSelEnd : *pSelStart;

    if (lo < 0) {
        lo = 0;
    }

    if (hi > *pLen) {
        hi = *pLen;
    }

    if (lo >= hi) {
        *pSelStart = -1;
        *pSelEnd   = -1;
        return;
    }

    memmove(buf + lo, buf + hi, *pLen - hi + 1);
    *pLen    -= (hi - lo);
    *pCursor  = lo;
    *pSelStart = -1;
    *pSelEnd   = -1;
}


static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize) {
    if (!undoBuf) {
        return;
    }

    int32_t copyLen = len < bufSize ? len : bufSize - 1;
    memcpy(undoBuf, buf, copyLen);
    undoBuf[copyLen] = '\0';
    *pUndoLen    = copyLen;
    *pUndoCursor = cursor;
}


// ============================================================
// widgetTextEditDragUpdateLine
// ============================================================
//
// Called during mouse drag to extend the selection for single-line
// text widgets. Auto-scrolls when the mouse moves past the visible
// text edges.

void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd) {
    int32_t rightEdge = leftEdge + maxChars * font->charWidth;

    if (vx < leftEdge && *pScrollOff > 0) {
        (*pScrollOff)--;
    } else if (vx >= rightEdge && *pScrollOff + maxChars < len) {
        (*pScrollOff)++;
    }

    int32_t relX    = vx - leftEdge;
    int32_t charPos = relX / font->charWidth + *pScrollOff;

    if (charPos < 0) {
        charPos = 0;
    }

    if (charPos > len) {
        charPos = len;
    }

    *pCursorPos = charPos;
    *pSelEnd    = charPos;
}


// ============================================================
// widgetTextEditMouseClick
// ============================================================
//
// Computes cursor position from pixel coordinates, handles multi-click
// (double = word select, triple = select all), and optionally starts
// drag-select.

void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect) {
    int32_t relX    = vx - textLeftX;
    int32_t charPos = relX / font->charWidth + scrollOff;

    if (charPos < 0) {
        charPos = 0;
    }

    if (charPos > len) {
        charPos = len;
    }

    int32_t clicks = multiClickDetect(vx, vy);

    if (clicks >= 3) {
        *pSelStart  = 0;
        *pSelEnd    = len;
        *pCursorPos = len;
        sDragWidget = NULL;
        return;
    }

    if (clicks == 2) {
        if (wordSelect && buf) {
            int32_t ws = wordStart(buf, charPos);
            int32_t we = wordEnd(buf, len, charPos);
            *pSelStart  = ws;
            *pSelEnd    = we;
            *pCursorPos = we;
        } else {
            *pSelStart  = 0;
            *pSelEnd    = len;
            *pCursorPos = len;
        }

        sDragWidget = NULL;
        return;
    }

    // Single click: place cursor
    *pCursorPos = charPos;
    *pSelStart  = charPos;
    *pSelEnd    = charPos;
    sDragWidget = dragSelect ? w : NULL;
}


// ============================================================
// widgetTextEditOnKey -- shared single-line text editing logic
// ============================================================
//
// This is the core single-line text editing engine, parameterized by
// pointer to allow reuse across TextInput, Spinner, and ComboBox.

void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t fieldWidth) {
    bool shift   = (mod & KEY_MOD_SHIFT) != 0;
    bool hasSel  = (pSelStart && pSelEnd && *pSelStart >= 0 && *pSelEnd >= 0 && *pSelStart != *pSelEnd);
    int32_t selLo = hasSel ? (*pSelStart < *pSelEnd ? *pSelStart : *pSelEnd) : -1;
    int32_t selHi = hasSel ? (*pSelStart < *pSelEnd ? *pSelEnd : *pSelStart) : -1;

    // Clamp selection to buffer bounds
    if (hasSel) {
        if (selLo < 0) {
            selLo = 0;
        }

        if (selHi > *pLen) {
            selHi = *pLen;
        }

        if (selLo >= selHi) {
            hasSel = false;
            selLo  = -1;
            selHi  = -1;
        }
    }

    // Ctrl+A  -- select all
    if (key == 1 && pSelStart && pSelEnd) {
        *pSelStart = 0;
        *pSelEnd   = *pLen;
        *pCursor   = *pLen;
        goto adjustScroll;
    }

    // Ctrl+C  -- copy
    if (key == 3) {
        if (hasSel) {
            clipboardCopy(buf + selLo, selHi - selLo);
        }

        return;
    }

    // Ctrl+V  -- paste
    if (key == 22) {
        int32_t cbLen     = 0;
        const char *cb    = clipboardGet(&cbLen);

        if (cbLen > 0) {
            if (undoBuf) {
                textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
            }

            if (hasSel) {
                textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
            }

            int32_t canFit = bufSize - 1 - *pLen;
            // For single-line, skip newlines in clipboard
            int32_t paste = 0;

            for (int32_t i = 0; i < cbLen && paste < canFit; i++) {
                if (cb[i] != '\n' && cb[i] != '\r') {
                    paste++;
                }
            }

            if (paste > 0) {
                int32_t pos = *pCursor;
                memmove(buf + pos + paste, buf + pos, *pLen - pos + 1);

                int32_t j = 0;

                for (int32_t i = 0; i < cbLen && j < paste; i++) {
                    if (cb[i] != '\n' && cb[i] != '\r') {
                        buf[pos + j] = cb[i];
                        j++;
                    }
                }

                *pLen    += paste;
                *pCursor += paste;
            }

            if (w->onChange) {
                w->onChange(w);
            }
        }

        goto adjustScroll;
    }

    // Ctrl+X  -- cut
    if (key == 24) {
        if (hasSel) {
            clipboardCopy(buf + selLo, selHi - selLo);

            if (undoBuf) {
                textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
            }

            textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);

            if (w->onChange) {
                w->onChange(w);
            }
        }

        goto adjustScroll;
    }

    // Ctrl+Z  -- undo
    if (key == 26 && undoBuf && pUndoLen && pUndoCursor) {
        // Swap current and undo
        char tmpBuf[*pLen + 1];
        int32_t tmpLen    = *pLen;
        int32_t tmpCursor = *pCursor;
        int32_t copyLen   = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1;

        memcpy(tmpBuf, buf, copyLen);
        tmpBuf[copyLen] = '\0';

        int32_t restLen = *pUndoLen < bufSize - 1 ? *pUndoLen : bufSize - 1;
        memcpy(buf, undoBuf, restLen);
        buf[restLen] = '\0';
        *pLen = restLen;

        int32_t restoreCursor = *pUndoCursor < *pLen ? *pUndoCursor : *pLen;

        // Save old as new undo
        int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1;
        memcpy(undoBuf, tmpBuf, saveLen);
        undoBuf[saveLen] = '\0';
        *pUndoLen    = saveLen;
        *pUndoCursor = tmpCursor;

        *pCursor = restoreCursor;

        if (pSelStart) {
            *pSelStart = -1;
        }

        if (pSelEnd) {
            *pSelEnd = -1;
        }

        if (w->onChange) {
            w->onChange(w);
        }

        goto adjustScroll;
    }

    if (key >= 32 && key < 127) {
        // Printable character
        if (undoBuf) {
            textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
        }

        if (hasSel) {
            textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
        }

        if (*pLen < bufSize - 1) {
            int32_t pos = *pCursor;
            memmove(buf + pos + 1, buf + pos, *pLen - pos + 1);
            buf[pos] = (char)key;
            (*pLen)++;
            (*pCursor)++;

            if (w->onChange) {
                w->onChange(w);
            }
        }
    } else if (key == 8) {
        // Backspace
        if (hasSel) {
            if (undoBuf) {
                textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
            }

            textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);

            if (w->onChange) {
                w->onChange(w);
            }
        } else if (*pCursor > 0) {
            if (undoBuf) {
                textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
            }

            int32_t pos = *pCursor;
            memmove(buf + pos - 1, buf + pos, *pLen - pos + 1);
            (*pLen)--;
            (*pCursor)--;

            if (w->onChange) {
                w->onChange(w);
            }
        }
    } else if (key == (0x4B | 0x100)) {
        // Left arrow
        if (shift && pSelStart && pSelEnd) {
            if (*pSelStart < 0) {
                *pSelStart = *pCursor;
                *pSelEnd   = *pCursor;
            }

            if (*pCursor > 0) {
                (*pCursor)--;
            }

            *pSelEnd = *pCursor;
        } else {
            if (pSelStart) {
                *pSelStart = -1;
            }

            if (pSelEnd) {
                *pSelEnd = -1;
            }

            if (*pCursor > 0) {
                (*pCursor)--;
            }
        }
    } else if (key == (0x4D | 0x100)) {
        // Right arrow
        if (shift && pSelStart && pSelEnd) {
            if (*pSelStart < 0) {
                *pSelStart = *pCursor;
                *pSelEnd   = *pCursor;
            }

            if (*pCursor < *pLen) {
                (*pCursor)++;
            }

            *pSelEnd = *pCursor;
        } else {
            if (pSelStart) {
                *pSelStart = -1;
            }

            if (pSelEnd) {
                *pSelEnd = -1;
            }

            if (*pCursor < *pLen) {
                (*pCursor)++;
            }
        }
    } else if (key == (0x73 | 0x100)) {
        // Ctrl+Left  -- word left
        int32_t newPos = wordBoundaryLeft(buf, *pCursor);

        if (shift && pSelStart && pSelEnd) {
            if (*pSelStart < 0) {
                *pSelStart = *pCursor;
                *pSelEnd   = *pCursor;
            }

            *pCursor = newPos;
            *pSelEnd = newPos;
        } else {
            if (pSelStart) {
                *pSelStart = -1;
            }

            if (pSelEnd) {
                *pSelEnd = -1;
            }

            *pCursor = newPos;
        }
    } else if (key == (0x74 | 0x100)) {
        // Ctrl+Right  -- word right
        int32_t newPos = wordBoundaryRight(buf, *pLen, *pCursor);

        if (shift && pSelStart && pSelEnd) {
            if (*pSelStart < 0) {
                *pSelStart = *pCursor;
                *pSelEnd   = *pCursor;
            }

            *pCursor = newPos;
            *pSelEnd = newPos;
        } else {
            if (pSelStart) {
                *pSelStart = -1;
            }

            if (pSelEnd) {
                *pSelEnd = -1;
            }

            *pCursor = newPos;
        }
    } else if (key == (0x47 | 0x100)) {
        // Home
        if (shift && pSelStart && pSelEnd) {
            if (*pSelStart < 0) {
                *pSelStart = *pCursor;
                *pSelEnd   = *pCursor;
            }

            *pCursor = 0;
            *pSelEnd = 0;
        } else {
            if (pSelStart) {
                *pSelStart = -1;
            }

            if (pSelEnd) {
                *pSelEnd = -1;
            }

            *pCursor = 0;
        }
    } else if (key == (0x4F | 0x100)) {
        // End
        if (shift && pSelStart && pSelEnd) {
            if (*pSelStart < 0) {
                *pSelStart = *pCursor;
                *pSelEnd   = *pCursor;
            }

            *pCursor = *pLen;
            *pSelEnd = *pLen;
        } else {
            if (pSelStart) {
                *pSelStart = -1;
            }

            if (pSelEnd) {
                *pSelEnd = -1;
            }

            *pCursor = *pLen;
        }
    } else if (key == (0x53 | 0x100)) {
        // Delete
        if (hasSel) {
            if (undoBuf) {
                textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
            }

            textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);

            if (w->onChange) {
                w->onChange(w);
            }
        } else if (*pCursor < *pLen) {
            if (undoBuf) {
                textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
            }

            int32_t pos = *pCursor;
            memmove(buf + pos, buf + pos + 1, *pLen - pos);
            (*pLen)--;

            if (w->onChange) {
                w->onChange(w);
            }
        }
    } else {
        return;
    }

adjustScroll:
    // Adjust scroll offset to keep cursor visible
    {
        AppContextT *ctx = wgtGetContext(w);
        const BitmapFontT *font = &ctx->font;
        int32_t fieldW = fieldWidth > 0 ? fieldWidth : w->w;

        int32_t visibleChars = (fieldW - TEXT_INPUT_PAD * 2) / font->charWidth;

        if (*pCursor < *pScrollOff) {
            *pScrollOff = *pCursor;
        }

        if (*pCursor >= *pScrollOff + visibleChars) {
            *pScrollOff = *pCursor - visibleChars + 1;
        }
    }

    wgtInvalidatePaint(w);
}


// ============================================================
// widgetTextEditPaintLine
// ============================================================
//
// Renders a single line of text with optional selection highlighting
// and a blinking cursor.

void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX) {
    // Normalize selection to low/high
    int32_t selLo = -1;
    int32_t selHi = -1;

    if (selStart >= 0 && selEnd >= 0 && selStart != selEnd) {
        selLo = selStart < selEnd ? selStart : selEnd;
        selHi = selStart < selEnd ? selEnd : selStart;
    }

    // Map selection to visible range
    int32_t visSelLo = selLo - scrollOff;
    int32_t visSelHi = selHi - scrollOff;

    if (visSelLo < 0) { visSelLo = 0; }
    if (visSelHi > visLen) { visSelHi = visLen; }

    if (selLo >= 0 && visSelLo < visSelHi) {
        if (visSelLo > 0) {
            drawTextN(d, ops, font, textX, textY, buf, visSelLo, fg, bg, true);
        }

        drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, buf + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true);

        if (visSelHi < visLen) {
            drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, buf + visSelHi, visLen - visSelHi, fg, bg, true);
        }
    } else if (visLen > 0) {
        drawTextN(d, ops, font, textX, textY, buf, visLen, fg, bg, true);
    }

    // Blinking cursor
    if (showCursor && sCursorBlinkOn) {
        int32_t cursorX = textX + (cursorPos - scrollOff) * font->charWidth;

        if (cursorX >= cursorMinX && cursorX < cursorMaxX) {
            drawVLine(d, ops, cursorX, textY, font->charHeight, fg);
        }
    }
}


// ============================================================
// Word boundary helpers
// ============================================================

static int32_t wordBoundaryLeft(const char *buf, int32_t pos) {
    if (pos <= 0) {
        return 0;
    }

    // Skip non-word characters
    while (pos > 0 && !isalnum((unsigned char)buf[pos - 1]) && buf[pos - 1] != '_') {
        pos--;
    }

    // Skip word characters
    while (pos > 0 && (isalnum((unsigned char)buf[pos - 1]) || buf[pos - 1] == '_')) {
        pos--;
    }

    return pos;
}


static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos) {
    if (pos >= len) {
        return len;
    }

    // Skip word characters
    while (pos < len && (isalnum((unsigned char)buf[pos]) || buf[pos] == '_')) {
        pos++;
    }

    // Skip non-word characters
    while (pos < len && !isalnum((unsigned char)buf[pos]) && buf[pos] != '_') {
        pos++;
    }

    return pos;
}


int32_t wordEnd(const char *buf, int32_t len, int32_t pos) {
    while (pos < len && isWordChar(buf[pos])) {
        pos++;
    }

    return pos;
}


int32_t wordStart(const char *buf, int32_t pos) {
    while (pos > 0 && isWordChar(buf[pos - 1])) {
        pos--;
    }

    return pos;
}
