#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 "dvxWidgetPlugin.h"
#include "../texthelp/textHelp.h"
#include "../listhelp/listHelp.h"

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;
} DropdownDataT;


// ============================================================
// widgetDropdownDestroy
// ============================================================

void widgetDropdownDestroy(WidgetT *w) {
    free(w->data);
}


// ============================================================
// widgetDropdownGetText
// ============================================================

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 "";
}


// ============================================================
// widgetDropdownCalcMinSize
// ============================================================

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;
}


// ============================================================
// widgetDropdownOnKey
// ============================================================

// 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 == (0x48 | 0x100)) {
            if (d->hoverIdx > 0) {
                d->hoverIdx--;

                if (d->hoverIdx < d->scrollPos) {
                    d->scrollPos = d->hoverIdx;
                }
            }
        } else if (key == (0x50 | 0x100)) {
            if (d->hoverIdx < d->itemCount - 1) {
                d->hoverIdx++;

                if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) {
                    d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
                }
            }
        } else if (key == 0x0D || key == ' ') {
            d->selectedIdx = d->hoverIdx;
            d->open        = false;
            sOpenPopup = NULL;

            if (w->onChange) {
                w->onChange(w);
            }
        }
    } else {
        // Popup is closed
        if (key == ' ' || key == 0x0D) {
            d->open     = true;
            d->hoverIdx = d->selectedIdx;
            sOpenPopup = w;

            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;
            }
        } else if (key == (0x50 | 0x100)) {
            // Down arrow: cycle selection forward (wheel-friendly)
            if (d->selectedIdx < d->itemCount - 1) {
                d->selectedIdx++;

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

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

    wgtInvalidatePaint(w);
}


// ============================================================
// widgetDropdownOnMouse
// ============================================================

// 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 -- select it
    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 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;
}


// ============================================================
// widgetDropdownPaint
// ============================================================

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.highlight = colors->windowShadow;
    bevel.shadow    = colors->windowHighlight;
    bevel.face      = bg;
    bevel.width     = 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 + 3, w->y + 3, textAreaW - 6, w->h - 6, fg);
    }
}


// ============================================================
// widgetDropdownPaintPopup
// ============================================================

// 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);
}


// ============================================================
// widgetDropdownAccelActivate
// ============================================================

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


// ============================================================
// widgetDropdownClosePopup
// ============================================================

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


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


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);
}


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,
    }
};

// ============================================================
// Widget creation functions
// ============================================================


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;
}


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

    return d->selectedIdx;
}


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);
}


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


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);
} sApi = {
    .create      = wgtDropdown,
    .setItems    = wgtDropdownSetItems,
    .getSelected = wgtDropdownGetSelected,
    .setSelected = wgtDropdownSetSelected
};

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

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

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