// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

#define DVX_WIDGET_IMPL
// widgetListBox.c  -- ListBox widget (single and multi-select)
//
// Scrollable list of text items with single-select or multi-select modes.
// Items are stored as external string pointers (not copied), with a vertical
// scrollbar appearing when the item count exceeds visible rows.
//
// Multi-select uses a parallel selBits array (one byte per item, 0 or 1)
// rather than a bitfield. Using a full byte per item wastes some memory but
// makes individual item toggle/test trivial without shift/mask operations,
// which matters when the selection code runs on every click and keyboard event.
//
// The selection model follows Windows explorer conventions:
//   - Plain click: select one item, clear others, set anchor
//   - Ctrl+click: toggle one item, update anchor
//   - Shift+click: range select from anchor to clicked item
//   - Shift+arrow: range select from anchor to cursor
//   - Space: toggle current item (multi-select)
//   - Ctrl+A: select all (multi-select)
//
// The "anchor" concept is key: it's the starting point for shift-select
// ranges. It's updated on non-shift clicks but stays fixed during shift
// operations, allowing the user to extend/shrink the selection by
// shift-clicking different endpoints.
//
// Drag-reorder support allows items to be rearranged by dragging. When
// enabled, a mouse-down initiates a drag (tracked via sDragReorder global),
// and a 2px horizontal line indicator shows the insertion point. The actual
// reordering is handled by the application's onReorder callback.

#include "dvxWgtP.h"
#include "../../../libs/kpunch/listhelp/listHelp.h"

#define LISTBOX_BORDER       2

static int32_t sTypeId = -1;

typedef struct {
    // Fields are ordered by alignment to minimize struct padding.
    const char **items;
    int32_t      itemCount;
    int32_t      selectedIdx;
    int32_t      scrollPos;
    int32_t      maxItemLen;
    int32_t      anchorIdx;
    uint8_t     *selBits;
    int32_t      dragIdx;
    int32_t      dropIdx;
    int32_t      sbDragOrient;
    int32_t      sbDragOff;
    // Owned item storage for AddItem/RemoveItem/Clear
    char       **ownedItems;    // stb_ds dynamic array of strdup'd strings
    bool         multiSelect;
    bool         reorderable;
} ListBoxDataT;

#include <stdlib.h>
#include <string.h>
#include "stb_ds_wrap.h"

#define LISTBOX_PAD      2
#define LISTBOX_MIN_ROWS 4

// BASIC wrapper: SetItems "Item1|Item2|Item3" -- bulk load from pipe-delimited string
// No item cap: items are added one-by-one via wgtListBoxAddItem.


// ============================================================
// Prototypes
// ============================================================

static void        basSetItems(WidgetT *w, const char *spec);
static void        ensureScrollVisible(WidgetT *w, int32_t idx);
static void        listBoxAdoptItems(WidgetT *w);
static void        listBoxAllocSelBits(WidgetT *w);
static void        listBoxSyncOwned(WidgetT *w);
static void        selectRange(WidgetT *w, int32_t from, int32_t to);
WidgetT           *wgtListBox(WidgetT *parent);
void               wgtListBoxAddItem(WidgetT *w, const char *text);
void               wgtListBoxClear(WidgetT *w);
void               wgtListBoxClearSelection(WidgetT *w);
const char        *wgtListBoxGetItem(const WidgetT *w, int32_t idx);
int32_t            wgtListBoxGetItemCount(const WidgetT *w);
int32_t            wgtListBoxGetSelected(const WidgetT *w);
bool               wgtListBoxIsItemSelected(const WidgetT *w, int32_t idx);
void               wgtListBoxRemoveItem(WidgetT *w, int32_t idx);
void               wgtListBoxSelectAll(WidgetT *w);
void               wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count);
void               wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected);
void               wgtListBoxSetMultiSelect(WidgetT *w, bool multi);
void               wgtListBoxSetReorderable(WidgetT *w, bool reorderable);
void               wgtListBoxSetSelected(WidgetT *w, int32_t idx);
void               wgtRegister(void);
void               widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font);
void               widgetListBoxDestroy(WidgetT *w);
static void        widgetListBoxOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
static void        widgetListBoxOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
void               widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod);
void               widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
void               widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
static void        widgetListBoxReorderDrop(WidgetT *w);
static void        widgetListBoxReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
static void        widgetListBoxScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY);


static void basSetItems(WidgetT *w, const char *spec) {
    if (!spec || !*spec) {
        return;
    }

    // Reset existing items so SetItems replaces them (previous behavior).
    wgtListBoxClear(w);

    // Copy spec to a heap-alloc'd buffer so we can mutate it while tokenizing.
    char *buf = strdup(spec);

    if (!buf) {
        return;
    }

    char *tok = buf;

    while (*tok) {
        char *sep = strchr(tok, '|');

        if (sep) {
            *sep = '\0';
            wgtListBoxAddItem(w, tok);
            tok = sep + 1;
        } else {
            wgtListBoxAddItem(w, tok);
            break;
        }
    }

    free(buf);
}


// Adjust scroll position so the item at idx is within the visible viewport.
// If the item is above the viewport, scroll up to it. If below, scroll down
// to show it at the bottom. If already visible, do nothing.
static void ensureScrollVisible(WidgetT *w, int32_t idx) {
    if (idx < 0) {
        return;
    }

    ListBoxDataT      *d    = (ListBoxDataT *)w->data;
    AppContextT       *ctx  = wgtGetContext(w);
    const BitmapFontT *font = &ctx->font;
    int32_t innerH      = w->h - LISTBOX_BORDER * 2;
    int32_t visibleRows = innerH / font->charHeight;

    if (visibleRows < 1) {
        visibleRows = 1;
    }

    if (idx < d->scrollPos) {
        d->scrollPos = idx;
    } else if (idx >= d->scrollPos + visibleRows) {
        d->scrollPos = idx - visibleRows + 1;
    }
}


// Allocate (or re-allocate) the selection bits array for multi-select mode.
static void listBoxAllocSelBits(WidgetT *w) {
    ListBoxDataT *d = (ListBoxDataT *)w->data;

    free(d->selBits);
    d->selBits = NULL;

    int32_t count = d->itemCount;

    if (count > 0 && d->multiSelect) {
        d->selBits = (uint8_t *)calloc(count, 1);
    }
}


static void listBoxSyncOwned(WidgetT *w) {
    ListBoxDataT *d = (ListBoxDataT *)w->data;
    d->items     = (const char **)d->ownedItems;
    d->itemCount = (int32_t)arrlen(d->ownedItems);
    d->maxItemLen = widgetMaxItemLen(d->items, d->itemCount);
    listBoxAllocSelBits(w);
    wgtInvalidate(w);
}


// Copy the current (external) item strings into the owned storage so a
// reorderable list box can rearrange them without mutating a caller-owned
// SetItems array (which may be const/shared).  The strdup adoption sequence
// itself lives in the shared widgetAdoptOwnedStrings helper (listHelp).
// No-op when already owned.  Preserves the current item order and selection.
static void listBoxAdoptItems(WidgetT *w) {
    ListBoxDataT *d = (ListBoxDataT *)w->data;

    if (d->ownedItems && d->items == (const char **)d->ownedItems) {
        return;
    }

    int32_t count = d->itemCount;

    d->ownedItems = widgetAdoptOwnedStrings(d->ownedItems, d->items, count);
    d->items      = (const char **)d->ownedItems;
    d->itemCount  = count;
    d->maxItemLen = widgetMaxItemLen(d->items, count);
}


// Set selection bits for all items in the range [from, to] (inclusive,
// order-independent). Used for shift-click and shift-arrow range selection.
static void selectRange(WidgetT *w, int32_t from, int32_t to) {
    ListBoxDataT *d = (ListBoxDataT *)w->data;

    if (!d->selBits) {
        return;
    }

    int32_t lo = from < to ? from : to;
    int32_t hi = from > to ? from : to;

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

    if (hi >= d->itemCount) {
        hi = d->itemCount - 1;
    }

    for (int32_t i = lo; i <= hi; i++) {
        d->selBits[i] = 1;
    }
}


 WidgetT *wgtListBox(WidgetT *parent) {
    WidgetT *w = widgetAlloc(parent, sTypeId);

    if (w) {
        ListBoxDataT *d = (ListBoxDataT *)calloc(1, sizeof(ListBoxDataT));

        if (!d) {
            widgetAllocRollback(w);
            return NULL;
        }

        w->data        = d;
        d->selectedIdx = -1;
        d->anchorIdx   = -1;
        d->dragIdx     = -1;
        d->dropIdx     = -1;
    }

    return w;
}


void wgtListBoxAddItem(WidgetT *w, const char *text) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    ListBoxDataT *d = (ListBoxDataT *)w->data;
    arrput(d->ownedItems, strdup(text ? text : ""));
    listBoxSyncOwned(w);
}


void wgtListBoxClear(WidgetT *w) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    ListBoxDataT *d = (ListBoxDataT *)w->data;
    for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) {
        free(d->ownedItems[i]);
    }
    arrsetlen(d->ownedItems, 0);
    listBoxSyncOwned(w);
    d->selectedIdx = -1;
    d->scrollPos   = 0;
}


void wgtListBoxClearSelection(WidgetT *w) {
    if (!w || w->type != sTypeId) {
        return;
    }

    ListBoxDataT *d = (ListBoxDataT *)w->data;

    if (!d->selBits) {
        return;
    }

    memset(d->selBits, 0, d->itemCount);
    wgtInvalidatePaint(w);
}


const char *wgtListBoxGetItem(const WidgetT *w, int32_t idx) {
    if (!w || w->type != sTypeId) { return ""; }
    const ListBoxDataT *d = (const ListBoxDataT *)w->data;
    if (idx < 0 || idx >= d->itemCount) { return ""; }
    return d->items[idx] ? d->items[idx] : "";
}


int32_t wgtListBoxGetItemCount(const WidgetT *w) {
    if (!w || w->type != sTypeId) { return 0; }
    const ListBoxDataT *d = (const ListBoxDataT *)w->data;
    return d->itemCount;
}


int32_t wgtListBoxGetSelected(const WidgetT *w) {
    VALIDATE_WIDGET(w, sTypeId, -1);

    ListBoxDataT *d = (ListBoxDataT *)w->data;
    return d->selectedIdx;
}


bool wgtListBoxIsItemSelected(const WidgetT *w, int32_t idx) {
    VALIDATE_WIDGET(w, sTypeId, false);

    ListBoxDataT *d = (ListBoxDataT *)w->data;

    if (!d->multiSelect) {
        return idx == d->selectedIdx;
    }

    if (!d->selBits || idx < 0 || idx >= d->itemCount) {
        return false;
    }

    return d->selBits[idx] != 0;
}


void wgtListBoxRemoveItem(WidgetT *w, int32_t idx) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    ListBoxDataT *d = (ListBoxDataT *)w->data;
    if (idx < 0 || idx >= (int32_t)arrlen(d->ownedItems)) { return; }
    free(d->ownedItems[idx]);
    arrdel(d->ownedItems, idx);
    listBoxSyncOwned(w);
    if (d->selectedIdx >= d->itemCount) {
        d->selectedIdx = d->itemCount > 0 ? d->itemCount - 1 : -1;
    }
}


void wgtListBoxSelectAll(WidgetT *w) {
    if (!w || w->type != sTypeId) {
        return;
    }

    ListBoxDataT *d = (ListBoxDataT *)w->data;

    if (!d->selBits) {
        return;
    }

    memset(d->selBits, 1, d->itemCount);
    wgtInvalidatePaint(w);
}


void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) {
    VALIDATE_WIDGET_VOID(w, sTypeId);

    ListBoxDataT *d = (ListBoxDataT *)w->data;

    d->items      = items;
    d->itemCount  = count;
    d->maxItemLen = widgetMaxItemLen(items, count);

    if (d->selectedIdx >= count) {
        d->selectedIdx = count > 0 ? 0 : -1;
    }

    if (d->selectedIdx < 0 && count > 0) {
        d->selectedIdx = 0;
    }

    d->anchorIdx = d->selectedIdx;

    // Reallocate selection bits
    listBoxAllocSelBits(w);

    // Pre-select the cursor item
    if (d->selBits && d->selectedIdx >= 0) {
        d->selBits[d->selectedIdx] = 1;
    }

    // A reorderable list box must own its items (see listBoxAdoptItems).
    if (d->reorderable) {
        listBoxAdoptItems(w);
    }

    wgtInvalidate(w);
}


void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected) {
    VALIDATE_WIDGET_VOID(w, sTypeId);

    ListBoxDataT *d = (ListBoxDataT *)w->data;

    if (!d->selBits || idx < 0 || idx >= d->itemCount) {
        return;
    }

    d->selBits[idx] = selected ? 1 : 0;
    wgtInvalidatePaint(w);
}


void wgtListBoxSetMultiSelect(WidgetT *w, bool multi) {
    VALIDATE_WIDGET_VOID(w, sTypeId);

    ListBoxDataT *d = (ListBoxDataT *)w->data;

    d->multiSelect = multi;
    listBoxAllocSelBits(w);

    // Sync: mark current selection
    if (d->selBits && d->selectedIdx >= 0) {
        d->selBits[d->selectedIdx] = 1;
    }
}


void wgtListBoxSetReorderable(WidgetT *w, bool reorderable) {
    VALIDATE_WIDGET_VOID(w, sTypeId);

    ListBoxDataT *d = (ListBoxDataT *)w->data;
    d->reorderable = reorderable;

    // Own the items so drag-reorder can rearrange them even when supplied
    // externally via wgtListBoxSetItems.
    if (reorderable) {
        listBoxAdoptItems(w);
    }
}


void wgtListBoxSetSelected(WidgetT *w, int32_t idx) {
    VALIDATE_WIDGET_VOID(w, sTypeId);

    ListBoxDataT *d = (ListBoxDataT *)w->data;

    d->selectedIdx = idx;
    d->anchorIdx   = idx;
    d->scrollPos   = 0;

    // In multi-select, clear all then select just this one
    if (d->selBits) {
        memset(d->selBits, 0, d->itemCount);

        if (idx >= 0 && idx < d->itemCount) {
            d->selBits[idx] = 1;
        }
    }

    // Scroll to make the selected item visible
    ensureScrollVisible(w, idx);

    wgtInvalidatePaint(w);
}


// Min size accounts for the widest item, a scrollbar, padding, and border.
// Height is based on LISTBOX_MIN_ROWS (4 rows) so the listbox has a usable
// minimum height even when empty. The 8-character minimum width prevents
// the listbox from collapsing too narrow when items are short or absent.
void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    ListBoxDataT *d        = (ListBoxDataT *)w->data;
    int32_t       maxItemW = d->maxItemLen * font->charWidth;
    int32_t       minW     = font->charWidth * 8;

    if (maxItemW < minW) {
        maxItemW = minW;
    }

    w->calcMinW = maxItemW + LISTBOX_PAD * 2 + LISTBOX_BORDER * 2 + WGT_SB_W;
    w->calcMinH = LISTBOX_MIN_ROWS * font->charHeight + LISTBOX_BORDER * 2;
}


void widgetListBoxDestroy(WidgetT *w) {
    ListBoxDataT *d = (ListBoxDataT *)w->data;

    if (d) {
        for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) {
            free(d->ownedItems[i]);
        }
        arrfree(d->ownedItems);
        free(d->selBits);
        free(d);
        w->data = NULL;
    }
}


static void widgetListBoxOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
    (void)root;
    (void)x;
    (void)y;
    ListBoxDataT *d = (ListBoxDataT *)w->data;

    if (d->dragIdx >= 0) {
        widgetListBoxReorderDrop(w);
        d->dragIdx = -1;
    }

    wgtInvalidatePaint(w);
}


static void widgetListBoxOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
    ListBoxDataT *d = (ListBoxDataT *)w->data;

    if (d->dragIdx >= 0) {
        widgetListBoxReorderUpdate(w, root, x, y);
    } else {
        widgetListBoxScrollDragUpdate(w, d->sbDragOrient, d->sbDragOff, x, y);
    }
}


// Key handling: delegates to widgetNavigateIndex for cursor movement
// (Up, Down, Home, End, PgUp, PgDn) which returns the new index after
// navigation. This shared helper ensures consistent keyboard navigation
// across ListBox, ListView, and other scrollable widgets.
void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
    if (!w || w->type != sTypeId || !w->data) {
        return;
    }

    ListBoxDataT *d = (ListBoxDataT *)w->data;

    if (d->itemCount == 0) {
        return;
    }

    bool    multi = d->multiSelect;
    bool    shift = (mod & KEY_MOD_SHIFT) != 0;
    bool    ctrl  = (mod & KEY_MOD_CTRL) != 0;
    int32_t sel   = d->selectedIdx;

    // Ctrl+A  -- select all (multi-select only)
    if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) {
        wgtListBoxSelectAll(w);
        return;
    }

    // Space  -- toggle current item (multi-select only)
    if (multi && key == ' ') {
        if (sel >= 0 && d->selBits) {
            d->selBits[sel] ^= 1;
            d->anchorIdx = sel;
        }

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

        wgtInvalidatePaint(w);
        return;
    }

    // Enter  -- activate selected item (same as double-click)
    if (key == '\r' || key == '\n') {
        if (w->onDblClick && sel >= 0) {
            w->onDblClick(w);
        }

        return;
    }

    AppContextT       *ctx  = wgtGetContext(w);
    const BitmapFontT *font = &ctx->font;
    int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight;

    if (visibleRows < 1) {
        visibleRows = 1;
    }

    int32_t newSel = widgetNavigateIndex(key, sel, d->itemCount, visibleRows);

    // Type-ahead: printable character searches for next matching item
    if (newSel < 0 && key >= 0x20 && key < 0x7F) {
        newSel = widgetTypeAheadSearch((char)key, d->items, d->itemCount, sel);
    }

    if (newSel < 0) {
        return;
    }

    if (newSel == sel) {
        return;
    }

    d->selectedIdx = newSel;
    d->dragIdx     = -1;
    d->dropIdx     = -1;

    // Update selection
    if (multi && d->selBits) {
        if (shift) {
            // Shift+arrow: range from anchor to new cursor
            memset(d->selBits, 0, d->itemCount);
            selectRange(w, d->anchorIdx, newSel);
        }
        // Plain arrow: just move cursor, leave selections untouched
    }

    ensureScrollVisible(w, newSel);

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

    wgtInvalidatePaint(w);
}


// Mouse handling: first checks if the click is on the scrollbar (right edge),
// then falls through to item click handling. The scrollbar hit-test uses
// widgetScrollbarHitTest which divides the scrollbar into arrow buttons, page
// regions, and thumb based on the click position. Item clicks are translated
// from pixel coordinates to item index using integer division by charHeight.
void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
    ListBoxDataT      *d    = (ListBoxDataT *)hit->data;
    AppContextT       *ctx  = (AppContextT *)root->userData;
    const BitmapFontT *font = &ctx->font;

    int32_t innerH      = hit->h - LISTBOX_BORDER * 2;
    int32_t visibleRows = innerH / font->charHeight;
    bool    needSb      = (d->itemCount > visibleRows);

    // Clamp scroll
    int32_t maxScroll = d->itemCount - visibleRows;

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

    d->scrollPos = clampInt(d->scrollPos, 0, maxScroll);

    // Check if click is on the scrollbar
    if (needSb) {
        int32_t sbX = hit->x + hit->w - LISTBOX_BORDER - WGT_SB_W;

        if (vx >= sbX) {
            int32_t    relY = vy - (hit->y + LISTBOX_BORDER);
            ScrollHitE sh   = widgetScrollbarHitTest(innerH, relY, d->itemCount, visibleRows, d->scrollPos);

            if (sh == ScrollHitArrowDecE) {
                if (d->scrollPos > 0) {
                    d->scrollPos--;
                }
            } else if (sh == ScrollHitArrowIncE) {
                if (d->scrollPos < maxScroll) {
                    d->scrollPos++;
                }
            } else if (sh == ScrollHitPageDecE) {
                d->scrollPos -= visibleRows;
                d->scrollPos = clampInt(d->scrollPos, 0, maxScroll);
            } else if (sh == ScrollHitPageIncE) {
                d->scrollPos += visibleRows;
                d->scrollPos = clampInt(d->scrollPos, 0, maxScroll);
            } else if (sh == ScrollHitThumbE) {
                int32_t trackLen  = innerH - WGT_SB_W * 2;
                int32_t thumbPos;
                int32_t thumbSize;
                widgetScrollbarThumb(trackLen, d->itemCount, visibleRows, d->scrollPos, &thumbPos, &thumbSize);

                sDragWidget      = hit;
                d->sbDragOrient = 0;
                d->sbDragOff   = relY - WGT_SB_W - thumbPos;
            }

            sFocusedWidget = hit;
            return;
        }
    }

    // Click on item area
    int32_t innerY = hit->y + LISTBOX_BORDER;
    int32_t relY   = vy - innerY;

    if (relY < 0) {
        return;
    }

    int32_t clickedRow = relY / font->charHeight;
    int32_t idx        = d->scrollPos + clickedRow;

    if (idx < 0 || idx >= d->itemCount) {
        return;
    }

    bool multi = d->multiSelect;
    bool shift = (ctx->keyModifiers & KEY_MOD_SHIFT) != 0;
    bool ctrl  = (ctx->keyModifiers & KEY_MOD_CTRL) != 0;

    d->selectedIdx = idx;
    sFocusedWidget = hit;

    if (multi && d->selBits) {
        if (ctrl) {
            // Ctrl+click: toggle item, update anchor
            d->selBits[idx] ^= 1;
            d->anchorIdx = idx;
        } else if (shift) {
            // Shift+click: range from anchor to clicked
            memset(d->selBits, 0, d->itemCount);
            selectRange(hit, d->anchorIdx, idx);
        } else {
            // Plain click: select only this item, update anchor
            memset(d->selBits, 0, d->itemCount);
            d->selBits[idx] = 1;
            d->anchorIdx = idx;
        }
    }

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

    // onDblClick is handled by the central dispatcher in widgetEvent.c

    // Initiate drag-reorder if enabled (not from modifier clicks)
    if (d->reorderable && !shift && !ctrl) {
        d->dragIdx = idx;
        d->dropIdx = idx;
        sDragWidget = hit;
    }
}


// Paint: sunken bevel border, then iterate visible rows drawing text.
// Selected items get highlight background (full row width) with contrasting
// text color. In multi-select mode, the cursor item (selectedIdx) gets a
// dotted focus rect overlay  -- this is separate from the selection highlight
// so the user can see which item the keyboard cursor is on even when multiple
// items are selected. The scrollbar is drawn only when needed (item count
// exceeds visible rows), and the content width is reduced accordingly.
void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    ListBoxDataT *lb = (ListBoxDataT *)w->data;
    uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
    uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;

    int32_t innerH      = w->h - LISTBOX_BORDER * 2;
    int32_t visibleRows = innerH / font->charHeight;
    bool    needSb      = (lb->itemCount > visibleRows);
    int32_t contentW    = w->w - LISTBOX_BORDER * 2;

    if (needSb) {
        contentW -= WGT_SB_W;
    }

    // Sunken border
    BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, 2);
    drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);

    // Clamp scroll position
    int32_t maxScroll = lb->itemCount - visibleRows;

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

    lb->scrollPos = clampInt(lb->scrollPos, 0, maxScroll);

    // Draw items
    int32_t  innerX    = w->x + LISTBOX_BORDER + LISTBOX_PAD;
    int32_t  innerY    = w->y + LISTBOX_BORDER;
    int32_t  scrollPos = lb->scrollPos;
    bool     multi     = lb->multiSelect;
    uint8_t *selBits   = lb->selBits;

    for (int32_t i = 0; i < visibleRows && (scrollPos + i) < lb->itemCount; i++) {
        int32_t  idx = scrollPos + i;
        int32_t  iy  = innerY + i * font->charHeight;
        uint32_t ifg = fg;
        uint32_t ibg = bg;

        bool isSelected;

        if (multi && selBits) {
            isSelected = selBits[idx] != 0;
        } else {
            isSelected = (idx == lb->selectedIdx);
        }

        if (isSelected) {
            ifg = colors->menuHighlightFg;
            ibg = colors->menuHighlightBg;
            rectFill(d, ops, w->x + LISTBOX_BORDER, iy, contentW, font->charHeight, ibg);
        }

        // Clamp item text to the content width so long items don't overpaint
        // the scrollbar and neighboring widgets (no per-widget paint clip).
        // strnlen bounds the scan to the visible width instead of walking the
        // whole (possibly long) item string on every repaint.
        int32_t availW   = contentW - LISTBOX_PAD;
        int32_t maxChars = availW > 0 ? availW / font->charWidth : 0;
        int32_t itemLen  = (int32_t)strnlen(lb->items[idx], (size_t)maxChars);

        drawTextN(d, ops, font, innerX, iy, lb->items[idx], itemLen, ifg, ibg, false);

        // Draw cursor indicator in multi-select (dotted focus rect on cursor item)
        if (multi && idx == lb->selectedIdx && w == sFocusedWidget) {
            drawFocusRect(d, ops, w->x + LISTBOX_BORDER, iy, contentW, font->charHeight, fg);
        }
    }

    // Draw drag-reorder insertion indicator: a 2px horizontal line at the
    // drop position. The line is drawn between items (not on top of them)
    // so it's clear where the dragged item will be inserted.
    if (lb->reorderable && lb->dragIdx >= 0 && lb->dropIdx >= 0) {
        int32_t drop  = lb->dropIdx;
        int32_t lineY = innerY + (drop - scrollPos) * font->charHeight;

        if (lineY >= innerY && lineY <= innerY + innerH) {
            drawHLine(d, ops, w->x + LISTBOX_BORDER, lineY, contentW, colors->contentFg);
            drawHLine(d, ops, w->x + LISTBOX_BORDER, lineY + 1, contentW, colors->contentFg);
        }
    }

    // Draw scrollbar
    if (needSb) {
        int32_t sbX = w->x + w->w - LISTBOX_BORDER - WGT_SB_W;
        int32_t sbY = w->y + LISTBOX_BORDER;
        widgetDrawScrollbarV(d, ops, colors, sbX, sbY, innerH, lb->itemCount, visibleRows, lb->scrollPos);
    }

    if (w == sFocusedWidget) {
        drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
    }
}


static void widgetListBoxReorderDrop(WidgetT *w) {
    ListBoxDataT *d = (ListBoxDataT *)w->data;
    int32_t from    = d->dragIdx;
    int32_t to      = d->dropIdx;

    d->dragIdx = -1;
    d->dropIdx = -1;

    if (from < 0 || to < 0 || from >= d->itemCount || from == to || from == to - 1) {
        return;
    }

    // Move the item by shifting the pointer array
    const char *moving = d->items[from];

    if (to > from) {
        // Moving down: shift items up
        for (int32_t i = from; i < to - 1; i++) {
            d->items[i] = d->items[i + 1];
        }

        d->items[to - 1] = moving;
        d->selectedIdx   = to - 1;
    } else {
        // Moving up: shift items down
        for (int32_t i = from; i > to; i--) {
            d->items[i] = d->items[i - 1];
        }

        d->items[to]   = moving;
        d->selectedIdx = to;
    }

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


static void widgetListBoxReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
    (void)root;
    (void)x;
    ListBoxDataT *d    = (ListBoxDataT *)w->data;
    AppContextT *ctx   = wgtGetContext(w);
    const BitmapFontT *font = &ctx->font;
    int32_t innerY     = w->y + LISTBOX_BORDER;
    int32_t relY       = y - innerY;
    int32_t dropIdx    = d->scrollPos + relY / font->charHeight;

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

    if (dropIdx > d->itemCount) {
        dropIdx = d->itemCount;
    }

    d->dropIdx = dropIdx;

    // Auto-scroll when dragging near edges
    int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight;

    if (relY < font->charHeight && d->scrollPos > 0) {
        d->scrollPos--;
    } else if (relY >= (visibleRows - 1) * font->charHeight && d->scrollPos < d->itemCount - visibleRows) {
        d->scrollPos++;
    }
}


// Handle scrollbar thumb drag for vertical scrollbar.
static void widgetListBoxScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) {
    (void)mouseX;
    ListBoxDataT      *d    = (ListBoxDataT *)w->data;
    AppContextT       *ctx  = wgtGetContext(w);
    const BitmapFontT *font = &ctx->font;

    if (orient != 0) {
        return;
    }

    int32_t innerH      = w->h - LISTBOX_BORDER * 2;
    int32_t visibleRows = innerH / font->charHeight;
    int32_t maxScroll   = d->itemCount - visibleRows;

    if (maxScroll <= 0) {
        return;
    }

    int32_t trackLen = innerH - WGT_SB_W * 2;
    int32_t sbY      = w->y + LISTBOX_BORDER;
    int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff;

    d->scrollPos = widgetScrollbarThumbDragScroll(trackLen, d->itemCount, visibleRows, relMouse, maxScroll);
}


// ============================================================
// DXE registration
// ============================================================

static const WidgetClassT sClassListBox = {
    .version = WGT_CLASS_VERSION,
    .flags   = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE,
    .handlers = {
        [WGT_METHOD_PAINT]          = (void *)widgetListBoxPaint,
        [WGT_METHOD_CALC_MIN_SIZE]  = (void *)widgetListBoxCalcMinSize,
        [WGT_METHOD_ON_MOUSE]       = (void *)widgetListBoxOnMouse,
        [WGT_METHOD_ON_KEY]         = (void *)widgetListBoxOnKey,
        [WGT_METHOD_DESTROY]        = (void *)widgetListBoxDestroy,
        [WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetListBoxOnDragUpdate,
        [WGT_METHOD_ON_DRAG_END]    = (void *)widgetListBoxOnDragEnd,
    }
};


static const struct {
    WidgetT *(*create)(WidgetT *parent);
    void     (*setItems)(WidgetT *w, const char **items, int32_t count);
    int32_t  (*getSelected)(const WidgetT *w);
    void     (*setSelected)(WidgetT *w, int32_t idx);
    void     (*setMultiSelect)(WidgetT *w, bool multi);
    bool     (*isItemSelected)(const WidgetT *w, int32_t idx);
    void     (*setItemSelected)(WidgetT *w, int32_t idx, bool selected);
    void     (*selectAll)(WidgetT *w);
    void     (*clearSelection)(WidgetT *w);
    void     (*setReorderable)(WidgetT *w, bool reorderable);
    void     (*addItem)(WidgetT *w, const char *text);
    void     (*removeItem)(WidgetT *w, int32_t idx);
    void     (*clear)(WidgetT *w);
    const char *(*getItem)(const WidgetT *w, int32_t idx);
    int32_t  (*getItemCount)(const WidgetT *w);
} sApi = {
    .create          = wgtListBox,
    .setItems        = wgtListBoxSetItems,
    .getSelected     = wgtListBoxGetSelected,
    .setSelected     = wgtListBoxSetSelected,
    .setMultiSelect  = wgtListBoxSetMultiSelect,
    .isItemSelected  = wgtListBoxIsItemSelected,
    .setItemSelected = wgtListBoxSetItemSelected,
    .selectAll       = wgtListBoxSelectAll,
    .clearSelection  = wgtListBoxClearSelection,
    .setReorderable  = wgtListBoxSetReorderable,
    .addItem         = wgtListBoxAddItem,
    .removeItem      = wgtListBoxRemoveItem,
    .clear           = wgtListBoxClear,
    .getItem         = wgtListBoxGetItem,
    .getItemCount    = wgtListBoxGetItemCount
};


static const WgtPropDescT sProps[] = {
    { "ListIndex", WGT_IFACE_INT, (void *)wgtListBoxGetSelected, (void *)wgtListBoxSetSelected, NULL }
};

static const WgtMethodDescT sMethods[] = {
    { "AddItem",         WGT_SIG_STR,          (void *)wgtListBoxAddItem },
    { "Clear",           WGT_SIG_VOID,         (void *)wgtListBoxClear },
    { "ClearSelection",  WGT_SIG_VOID,         (void *)wgtListBoxClearSelection },
    { "IsItemSelected",  WGT_SIG_RET_BOOL_INT, (void *)wgtListBoxIsItemSelected },
    { "List",            WGT_SIG_RET_STR_INT,  (void *)wgtListBoxGetItem },
    { "ListCount",       WGT_SIG_RET_INT,      (void *)wgtListBoxGetItemCount },
    { "RemoveItem",      WGT_SIG_INT,          (void *)wgtListBoxRemoveItem },
    { "SelectAll",       WGT_SIG_VOID,         (void *)wgtListBoxSelectAll },
    { "SetItemSelected", WGT_SIG_INT_BOOL,     (void *)wgtListBoxSetItemSelected },
    { "SetItems",        WGT_SIG_STR,          (void *)basSetItems },
    { "SetMultiSelect",  WGT_SIG_BOOL,         (void *)wgtListBoxSetMultiSelect },
    { "SetReorderable",  WGT_SIG_BOOL,         (void *)wgtListBoxSetReorderable },
};

static const WgtIfaceT sIface = {
    .basName      = "ListBox",
    .props        = sProps,
    .propCount    = 1,
    .methods      = sMethods,
    .methodCount  = 12,
    .events       = NULL,
    .eventCount   = 0,
    .createSig    = WGT_CREATE_PARENT,
    .defaultEvent = "Click"
};

void wgtRegister(void) {
    sTypeId = wgtRegisterClass(&sClassListBox);
    wgtRegisterApi("listbox", &sApi);
    wgtRegisterIface("listbox", &sIface);
}
