// 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
// widgetDropdown.c  -- Dropdown (select) widget
//
// A non-editable dropdown list (HTML <select> equivalent). Unlike ComboBox,
// the user cannot type a custom value  -- they can only choose from the
// predefined items. This simplifies the widget considerably: no text buffer,
// no undo, no cursor, no text editing key handling.
//
// The visual layout is identical to ComboBox (sunken display area + raised
// dropdown button), but the display area shows the selected item's text
// as read-only. A focus rect is drawn inside the display area when focused
// (since there's no cursor to indicate focus).
//
// Keyboard behavior when closed: Up arrow decrements selection immediately
// (inline cycling), Down/Space/Enter opens the popup. This matches the
// Win3.1 dropdown behavior where arrow keys cycle through values without
// opening the full list.
//
// Popup overlay painting and hit testing are shared with ComboBox via
// widgetDropdownPopupRect and widgetPaintPopupList in widgetCore.c.

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

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

// Focus-rect inset inside the dropdown's text area (pixels on each side).
#define DROPDOWN_FOCUS_INSET 3

static int32_t sTypeId = -1;

typedef struct {
    const char **items;
    int32_t      itemCount;
    int32_t      selectedIdx;
    bool         open;
    int32_t      hoverIdx;
    int32_t      scrollPos;
    int32_t      maxItemLen;
    // Owned item storage for AddItem/RemoveItem/Clear
    char       **ownedItems;    // stb_ds dynamic array of strdup'd strings
} DropdownDataT;


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

static void dropdownEnsureHoverVisible(DropdownDataT *d);
static void dropdownSyncOwned(WidgetT *w);
WidgetT *wgtDropdown(WidgetT *parent);
void wgtDropdownAddItem(WidgetT *w, const char *text);
void wgtDropdownClear(WidgetT *w);
const char *wgtDropdownGetItem(const WidgetT *w, int32_t idx);
int32_t wgtDropdownGetItemCount(const WidgetT *w);
int32_t wgtDropdownGetSelected(const WidgetT *w);
void wgtDropdownRemoveItem(WidgetT *w, int32_t idx);
void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count);
void wgtDropdownSetSelected(WidgetT *w, int32_t idx);
void wgtRegister(void);
void widgetDropdownAccelActivate(WidgetT *w, WidgetT *root);
void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetDropdownClosePopup(WidgetT *w);
void widgetDropdownDestroy(WidgetT *w);
static void widgetDropdownGetPopupRect(const WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH);
const char *widgetDropdownGetText(const WidgetT *w);
void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod);
void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetDropdownPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetDropdownPaintPopup(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);


// Adjust scrollPos so the current hover index sits within the visible popup
// window. Shared by every dropdown open/navigate path that moves hoverIdx.
// A negative hoverIdx (no selection) leaves scrollPos untouched so it is
// never driven negative.
static void dropdownEnsureHoverVisible(DropdownDataT *d) {
    if (d->hoverIdx < 0) {
        return;
    }

    if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) {
        d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
    }

    if (d->hoverIdx < d->scrollPos) {
        d->scrollPos = d->hoverIdx;
    }
}


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


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

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

        if (!d) {
            return w;
        }

        w->data        = d;
        d->selectedIdx = -1;
        d->hoverIdx    = -1;
    }

    return w;
}


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


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


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


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


int32_t wgtDropdownGetSelected(const WidgetT *w) {
    VALIDATE_WIDGET(w, sTypeId, -1);
    const DropdownDataT *d = (const DropdownDataT *)w->data;

    return d->selectedIdx;
}


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


void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    DropdownDataT *d = (DropdownDataT *)w->data;

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

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

    wgtInvalidate(w);
}


void wgtDropdownSetSelected(WidgetT *w, int32_t idx) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    DropdownDataT *d = (DropdownDataT *)w->data;

    d->selectedIdx = idx;
    wgtInvalidatePaint(w);
}


void widgetDropdownAccelActivate(WidgetT *w, WidgetT *root) {
    (void)root;
    DropdownDataT *d = (DropdownDataT *)w->data;
    d->open     = true;
    d->hoverIdx = d->selectedIdx;
    sOpenPopup  = w;

    // Scroll the selected item into view, mirroring the keyboard-open path.
    dropdownEnsureHoverVisible(d);

    wgtInvalidatePaint(w);
}


void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    DropdownDataT *d = (DropdownDataT *)w->data;

    // Width: widest item + button width + border
    int32_t maxItemW = d->maxItemLen * font->charWidth;
    int32_t minW     = font->charWidth * 8;

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

    w->calcMinW = maxItemW + DROPDOWN_BTN_WIDTH + TEXT_INPUT_PAD * 2 + 4;
    w->calcMinH = font->charHeight + TEXT_INPUT_PAD * 2;
}


void widgetDropdownClosePopup(WidgetT *w) {
    DropdownDataT *d = (DropdownDataT *)w->data;
    d->open = false;
    wgtInvalidatePaint(w);
}


void widgetDropdownDestroy(WidgetT *w) {
    DropdownDataT *d = (DropdownDataT *)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);
        w->data = NULL;
    }
}


static void widgetDropdownGetPopupRect(const WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) {
    const DropdownDataT *d = (const DropdownDataT *)w->data;
    widgetDropdownPopupRect((WidgetT *)w, font, contentH, d->itemCount, popX, popY, popW, popH);
}


const char *widgetDropdownGetText(const WidgetT *w) {
    const DropdownDataT *d = (const DropdownDataT *)w->data;

    if (d->selectedIdx >= 0 && d->selectedIdx < d->itemCount) {
        return d->items[d->selectedIdx];
    }

    return "";
}


// Key handling: when popup is open, Up/Down navigate the hover index and
// Enter/Space confirms selection. When closed, Down/Space/Enter opens the
// popup, while Up decrements the selection inline (without opening). This
// two-mode behavior matches the standard Windows combo box UX where quick
// arrow-key cycling doesn't require the popup to be visible.
void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
    (void)mod;
    DropdownDataT *d = (DropdownDataT *)w->data;

    if (d->open) {
        // Popup is open -- navigate items
        if (key == KEY_UP) {
            if (d->hoverIdx > 0) {
                d->hoverIdx--;
                dropdownEnsureHoverVisible(d);
            }
        } else if (key == KEY_DOWN) {
            if (d->hoverIdx < d->itemCount - 1) {
                d->hoverIdx++;
                dropdownEnsureHoverVisible(d);
            }
        } else if (key == 0x0D || key == ' ') {
            d->selectedIdx = d->hoverIdx;
            d->open        = false;
            sOpenPopup     = NULL;

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

            wgtInvalidate(w);
        } else if (key >= 0x20 && key < 0x7F) {
            int32_t found = widgetTypeAheadSearch((char)key, d->items, d->itemCount, d->hoverIdx);

            if (found >= 0) {
                d->hoverIdx = found;
                dropdownEnsureHoverVisible(d);
            }
        }
    } else {
        // Popup is closed
        if (key == ' ' || key == 0x0D) {
            d->open     = true;
            d->hoverIdx = d->selectedIdx;
            sOpenPopup = w;
            dropdownEnsureHoverVisible(d);
        } else if (key == KEY_DOWN) {
            // Down arrow: cycle selection forward (wheel-friendly)
            if (d->selectedIdx < d->itemCount - 1) {
                d->selectedIdx++;

                if (w->onChange) {
                    w->onChange(w);
                }
            }
        } else if (key == KEY_UP) {
            if (d->selectedIdx > 0) {
                d->selectedIdx--;

                if (w->onChange) {
                    w->onChange(w);
                }
            }
        } else if (key >= 0x21 && key < 0x7F) {
            // Type-ahead when closed (skip space — it opens the popup)
            int32_t found = widgetTypeAheadSearch((char)key, d->items, d->itemCount, d->selectedIdx);

            if (found >= 0) {
                d->selectedIdx = found;

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

    wgtInvalidatePaint(w);
}


// Mouse click toggles the popup open/closed. The sClosedPopup guard prevents
// a re-open race condition: when a click outside the popup closes it, the
// widget event dispatcher first closes the popup (setting sClosedPopup),
// then dispatches the click to whatever widget is under the cursor. If that
// happens to be this dropdown's body, we'd immediately re-open without this
// check. The sClosedPopup reference is cleared on the next event cycle.
void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
    (void)root;
    (void)vx;
    sFocusedWidget = w;
    DropdownDataT *d = (DropdownDataT *)w->data;

    // If popup is open, this click is on a popup item or scrollbar
    if (d->open) {
        AppContextT *ctx = wgtGetContext(w);
        const BitmapFontT *font = &ctx->font;
        int32_t popX;
        int32_t popY;
        int32_t popW;
        int32_t popH;

        widgetDropdownPopupRect(w, font, w->window->contentH, d->itemCount, &popX, &popY, &popW, &popH);

        int32_t visibleItems = popH / font->charHeight;

        // Check scrollbar click first
        if (widgetPopupScrollbarClick(vx, vy, popX, popY, popW, popH, d->itemCount, visibleItems, &d->scrollPos)) {
            wgtInvalidatePaint(w);
            return;
        }

        int32_t itemIdx = d->scrollPos + (vy - popY - 2) / font->charHeight;

        if (itemIdx >= 0 && itemIdx < d->itemCount) {
            d->selectedIdx = itemIdx;

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

        d->open    = false;
        sOpenPopup = NULL;
        return;
    }

    // If this dropdown's popup was just closed by click-outside, don't re-open
    if (w == sClosedPopup) {
        return;
    }

    d->open     = true;
    d->hoverIdx = d->selectedIdx;
    sOpenPopup  = w;

    // Scroll the selected item into view, mirroring the keyboard-open path.
    dropdownEnsureHoverVisible(d);

    // Repaint so the popup overlay appears immediately; the mouse
    // dispatcher does not invalidate the hit widget on open.
    wgtInvalidatePaint(w);
}


void widgetDropdownPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    DropdownDataT *d = (DropdownDataT *)w->data;
    uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
    uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;

    // Sunken text area
    int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH;
    BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, 2);
    drawBevel(disp, ops, w->x, w->y, textAreaW, w->h, &bevel);

    // Draw selected item text
    if (d->selectedIdx >= 0 && d->selectedIdx < d->itemCount) {
        int32_t textX = w->x + TEXT_INPUT_PAD;
        int32_t textY = w->y + (w->h - font->charHeight) / 2;

        if (!w->enabled) {
            drawTextEmbossed(disp, ops, font, textX, textY, d->items[d->selectedIdx], colors);
        } else {
            drawText(disp, ops, font, textX, textY, d->items[d->selectedIdx], fg, bg, true);
        }
    }

    // Drop button
    BevelStyleT btnBevel;
    btnBevel.highlight = d->open ? colors->windowShadow : colors->windowHighlight;
    btnBevel.shadow    = d->open ? colors->windowHighlight : colors->windowShadow;
    btnBevel.face      = colors->buttonFace;
    btnBevel.width     = 2;
    drawBevel(disp, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);

    // Down arrow glyph in button
    uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow;
    int32_t  arrowX  = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
    int32_t  arrowY  = w->y + w->h / 2 - 1;
    widgetDrawDropdownArrow(disp, ops, arrowX, arrowY, arrowFg);

    if (w == sFocusedWidget) {
        drawFocusRect(disp, ops, w->x + DROPDOWN_FOCUS_INSET, w->y + DROPDOWN_FOCUS_INSET, textAreaW - 2 * DROPDOWN_FOCUS_INSET, w->h - 2 * DROPDOWN_FOCUS_INSET, fg);
    }
}


// Paint the popup overlay list. Delegates to widgetDropdownPopupRect (which
// computes position, handling screen-edge flip to ensure the popup stays
// on-screen) and widgetPaintPopupList (which renders the bordered, scrollable
// item list with hover highlighting). These shared helpers are also used by
// ComboBox, keeping the popup rendering consistent between both widgets.
void widgetDropdownPaintPopup(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    DropdownDataT *d = (DropdownDataT *)w->data;
    int32_t popX;
    int32_t popY;
    int32_t popW;
    int32_t popH;

    widgetDropdownPopupRect(w, font, disp->clipH, d->itemCount, &popX, &popY, &popW, &popH);
    widgetPaintPopupList(disp, ops, font, colors, popX, popY, popW, popH, d->items, d->itemCount, d->hoverIdx, d->scrollPos);
}


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


static const WidgetClassT sClassDropdown = {
    .version = WGT_CLASS_VERSION,
    .flags   = WCLASS_FOCUSABLE | WCLASS_HAS_POPUP | WCLASS_SCROLLABLE,
    .handlers = {
        [WGT_METHOD_PAINT]             = (void *)widgetDropdownPaint,
        [WGT_METHOD_PAINT_OVERLAY]     = (void *)widgetDropdownPaintPopup,
        [WGT_METHOD_CALC_MIN_SIZE]     = (void *)widgetDropdownCalcMinSize,
        [WGT_METHOD_ON_MOUSE]          = (void *)widgetDropdownOnMouse,
        [WGT_METHOD_ON_KEY]            = (void *)widgetDropdownOnKey,
        [WGT_METHOD_ON_ACCEL_ACTIVATE] = (void *)widgetDropdownAccelActivate,
        [WGT_METHOD_DESTROY]           = (void *)widgetDropdownDestroy,
        [WGT_METHOD_GET_TEXT]          = (void *)widgetDropdownGetText,
        [WGT_METHOD_CLOSE_POPUP]       = (void *)widgetDropdownClosePopup,
        [WGT_METHOD_GET_POPUP_RECT]    = (void *)widgetDropdownGetPopupRect,
    }
};

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 index);
    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      = wgtDropdown,
    .setItems    = wgtDropdownSetItems,
    .getSelected = wgtDropdownGetSelected,
    .setSelected = wgtDropdownSetSelected,
    .addItem     = wgtDropdownAddItem,
    .removeItem  = wgtDropdownRemoveItem,
    .clear       = wgtDropdownClear,
    .getItem     = wgtDropdownGetItem,
    .getItemCount = wgtDropdownGetItemCount
};

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

static const WgtMethodDescT sMethods[] = {
    { "AddItem",    WGT_SIG_STR,         (void *)wgtDropdownAddItem },
    { "Clear",      WGT_SIG_VOID,        (void *)wgtDropdownClear },
    { "List",       WGT_SIG_RET_STR_INT, (void *)wgtDropdownGetItem },
    { "ListCount",  WGT_SIG_RET_INT,     (void *)wgtDropdownGetItemCount },
    { "RemoveItem", WGT_SIG_INT,         (void *)wgtDropdownRemoveItem },
};

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

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