// 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
// widgetSpinner.c  -- Spinner (numeric up/down) widget
//
// A hybrid widget combining a single-line text editor with up/down
// arrow buttons for numeric value entry. The user can either click
// the arrows, use Up/Down keys, or type a number directly.
//
// Supports two numeric modes:
//   Integer mode (default): int32_t value, step, min, max
//   Real mode (useReal=true): double value, step, min, max with
//     configurable decimal places
//
// Design: the widget has two modes -- display mode (showing the
// formatted value) and edit mode (allowing free-form text input).
// Edit mode is entered on the first text-modifying keystroke and
// committed on Enter or when arrows are clicked. Escape cancels
// the edit and reverts to the pre-edit value. This two-mode design
// keeps the display clean (always showing a properly formatted
// number) while still allowing direct keyboard entry.
//
// The text editing delegates to widgetTextEditOnKey() -- the same
// shared single-line editing logic used by TextInput. This gives
// the spinner cursor movement, selection, cut/copy/paste, and undo
// for free. Input validation filters non-numeric characters before
// they reach the editor.
//
// Undo uses a single-level swap buffer (same as TextInput): the
// current state is copied to undoBuf before each mutation, and
// Ctrl+Z swaps current<->undo. This is simpler and cheaper than
// a multi-level undo stack for the small buffers involved.
//
// Rendering: sunken border enclosing the text area + two stacked
// raised-bevel arrow buttons on the right. The buttons extend to
// the widget's right edge (including the border width) so they
// look like they're part of the border chrome. The up/down buttons
// split the widget height evenly.

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

static int32_t sTypeId = -1;

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// ============================================================
// Constants
// ============================================================

#define SPINNER_BTN_W        14
#define SPINNER_BORDER       2
#define SPINNER_PAD          3
#define SPINNER_BUF_SIZE     32
#define SPINNER_DEFAULT_DECIMALS 2

// ============================================================
// Per-instance data
// ============================================================

typedef struct {
    // Integer mode
    int32_t value;
    int32_t minValue;
    int32_t maxValue;
    int32_t step;

    // Real mode
    double  realValue;
    double  realMin;
    double  realMax;
    double  realStep;
    int32_t decimals;

    // Text editing state (fields ordered by alignment to minimize padding)
    int32_t len;
    int32_t cursorPos;
    int32_t scrollOff;
    int32_t selStart;
    int32_t selEnd;
    int32_t undoLen;
    int32_t undoCursor;
    char    buf[SPINNER_BUF_SIZE];
    char    undoBuf[SPINNER_BUF_SIZE];
    bool    useReal;
    bool    editing;
} SpinnerDataT;


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

static int32_t      ifaceGetDecimals(const WidgetT *w);
static bool         ifaceGetRealMode(const WidgetT *w);
static int32_t      ifaceGetValue(const WidgetT *w);
static void         ifaceSetDecimals(WidgetT *w, int32_t v);
static void         ifaceSetRealMode(WidgetT *w, bool v);
static void         ifaceSetValue(WidgetT *w, int32_t v);
static bool         spinnerAllowMinus(const SpinnerDataT *d);
static void         spinnerApplyStep(SpinnerDataT *d, int32_t direction, int32_t multiplier);
static void         spinnerClampAndFormat(SpinnerDataT *d);
static void         spinnerCommitEdit(SpinnerDataT *d);
static void         spinnerFormat(SpinnerDataT *d);
static void         spinnerStartEdit(SpinnerDataT *d);
static bool         spinnerValidateBuffer(const SpinnerDataT *d);
void                wgtRegister(void);
WidgetT            *wgtSpinner(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t step);
double              wgtSpinnerGetRealValue(const WidgetT *w);
int32_t             wgtSpinnerGetValue(const WidgetT *w);
void                wgtSpinnerSetDecimals(WidgetT *w, int32_t decimals);
void                wgtSpinnerSetRange(WidgetT *w, int32_t minVal, int32_t maxVal);
void                wgtSpinnerSetRealMode(WidgetT *w, bool enable);
void                wgtSpinnerSetRealRange(WidgetT *w, double minVal, double maxVal);
void                wgtSpinnerSetRealStep(WidgetT *w, double step);
void                wgtSpinnerSetRealValue(WidgetT *w, double value);
void                wgtSpinnerSetStep(WidgetT *w, int32_t step);
void                wgtSpinnerSetValue(WidgetT *w, int32_t value);
static void         widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font);
static const char  *widgetSpinnerGetText(const WidgetT *w);
static void         widgetSpinnerOnBlur(WidgetT *w);
static void         widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod);
static void         widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
static void         widgetSpinnerPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
static void         widgetSpinnerSetText(WidgetT *w, const char *text);


static int32_t ifaceGetDecimals(const WidgetT *w) {
    return ((SpinnerDataT *)w->data)->decimals;
}


static bool ifaceGetRealMode(const WidgetT *w) {
    return ((SpinnerDataT *)w->data)->useReal;
}


static int32_t ifaceGetValue(const WidgetT *w) {
    return wgtSpinnerGetValue(w);
}


static void ifaceSetDecimals(WidgetT *w, int32_t v) {
    wgtSpinnerSetDecimals(w, v);
}


static void ifaceSetRealMode(WidgetT *w, bool v) {
    wgtSpinnerSetRealMode(w, v);
}


static void ifaceSetValue(WidgetT *w, int32_t v) {
    wgtSpinnerSetValue(w, v);
}


static bool spinnerAllowMinus(const SpinnerDataT *d) {
    if (d->useReal) {
        return d->realMin < 0.0;
    }

    return d->minValue < 0;
}


static void spinnerApplyStep(SpinnerDataT *d, int32_t direction, int32_t multiplier) {
    if (d->useReal) {
        d->realValue += d->realStep * direction * multiplier;
    } else {
        d->value += d->step * direction * multiplier;
    }

    spinnerClampAndFormat(d);
}


static void spinnerClampAndFormat(SpinnerDataT *d) {
    if (d->useReal) {
        if (d->realValue < d->realMin) {
            d->realValue = d->realMin;
        }

        if (d->realValue > d->realMax) {
            d->realValue = d->realMax;
        }
    } else {
        if (d->value < d->minValue) {
            d->value = d->minValue;
        }

        if (d->value > d->maxValue) {
            d->value = d->maxValue;
        }
    }

    spinnerFormat(d);
}


static void spinnerCommitEdit(SpinnerDataT *d) {
    if (!d->editing) {
        return;
    }

    d->editing = false;
    d->buf[d->len] = '\0';

    if (d->useReal) {
        d->realValue = strtod(d->buf, NULL);
    } else {
        d->value = (int32_t)strtol(d->buf, NULL, 10);
    }

    spinnerClampAndFormat(d);
}


// Format always places the cursor at the end and resets scroll/selection.
// This is called after any value change to synchronize the text buffer
// with the numeric value. The cursor-at-end position matches user
// expectation after arrow-key increment/decrement.
static void spinnerFormat(SpinnerDataT *d) {
    if (d->useReal) {
        d->len = snprintf(d->buf, sizeof(d->buf), "%.*f", (int)d->decimals, d->realValue);
    } else {
        d->len = snprintf(d->buf, sizeof(d->buf), "%d", (int)d->value);
    }

    d->cursorPos = d->len;
    d->scrollOff = 0;
    d->selStart  = -1;
    d->selEnd    = -1;
}


// Entering edit mode snapshots the buffer for undo so the user can
// revert to the pre-edit formatted value. The snapshot is only taken
// on the transition to editing, not on every keystroke, so repeated
// typing within one edit session can be undone all at once.
static void spinnerStartEdit(SpinnerDataT *d) {
    if (!d->editing) {
        d->editing = true;

        // Snapshot for undo
        memcpy(d->undoBuf, d->buf, sizeof(d->buf));
        d->undoLen    = d->len;
        d->undoCursor = d->cursorPos;
    }
}


static bool spinnerValidateBuffer(const SpinnerDataT *d) {
    bool    allowMin = spinnerAllowMinus(d);
    bool    hadDot   = false;

    for (int32_t i = 0; i < d->len; i++) {
        char c = d->buf[i];

        if (c == '-' && i == 0 && allowMin) {
            continue;
        }

        if (c == '.' && d->useReal && !hadDot) {
            hadDot = true;
            continue;
        }

        if (c < '0' || c > '9') {
            return false;
        }
    }

    return true;
}


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

static const WidgetClassT sClassSpinner = {
    .version = WGT_CLASS_VERSION,
    .flags   = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE,
    .handlers = {
        [WGT_METHOD_PAINT]         = (void *)widgetSpinnerPaint,
        [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetSpinnerCalcMinSize,
        [WGT_METHOD_ON_MOUSE]      = (void *)widgetSpinnerOnMouse,
        [WGT_METHOD_ON_KEY]        = (void *)widgetSpinnerOnKey,
        [WGT_METHOD_ON_BLUR]       = (void *)widgetSpinnerOnBlur,
        [WGT_METHOD_GET_TEXT]      = (void *)widgetSpinnerGetText,
        [WGT_METHOD_SET_TEXT]      = (void *)widgetSpinnerSetText,
    }
};


static const struct {
    WidgetT *(*create)(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t step);
    void     (*setValue)(WidgetT *w, int32_t value);
    int32_t  (*getValue)(const WidgetT *w);
    void     (*setRange)(WidgetT *w, int32_t minVal, int32_t maxVal);
    void     (*setStep)(WidgetT *w, int32_t step);
    void     (*setRealMode)(WidgetT *w, bool enable);
    double   (*getRealValue)(const WidgetT *w);
    void     (*setRealValue)(WidgetT *w, double value);
    void     (*setRealRange)(WidgetT *w, double minVal, double maxVal);
    void     (*setRealStep)(WidgetT *w, double step);
    void     (*setDecimals)(WidgetT *w, int32_t decimals);
} sApi = {
    .create       = wgtSpinner,
    .setValue      = wgtSpinnerSetValue,
    .getValue      = wgtSpinnerGetValue,
    .setRange      = wgtSpinnerSetRange,
    .setStep       = wgtSpinnerSetStep,
    .setRealMode   = wgtSpinnerSetRealMode,
    .getRealValue  = wgtSpinnerGetRealValue,
    .setRealValue  = wgtSpinnerSetRealValue,
    .setRealRange  = wgtSpinnerSetRealRange,
    .setRealStep   = wgtSpinnerSetRealStep,
    .setDecimals   = wgtSpinnerSetDecimals,
};

static const WgtPropDescT sProps[] = {
    { "Value",    WGT_IFACE_INT,  (void *)ifaceGetValue,    (void *)ifaceSetValue,    NULL },
    { "RealMode", WGT_IFACE_BOOL, (void *)ifaceGetRealMode, (void *)ifaceSetRealMode, NULL },
    { "Decimals", WGT_IFACE_INT,  (void *)ifaceGetDecimals, (void *)ifaceSetDecimals, NULL },
};

static const WgtMethodDescT sMethods[] = {
    { "SetRange", WGT_SIG_INT_INT, (void *)wgtSpinnerSetRange },
    { "SetStep",  WGT_SIG_INT,     (void *)wgtSpinnerSetStep }
};

static const WgtIfaceT sIface = {
    .basName      = "SpinButton",
    .props        = sProps,
    .propCount    = 3,
    .methods      = sMethods,
    .methodCount  = 2,
    .events       = NULL,
    .eventCount   = 0,
    .createSig    = WGT_CREATE_PARENT_INT_INT_INT,
    .createArgs   = { 0, 100, 1 },
    .defaultEvent = "Change",
    .namePrefix   = "Spin",
};


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


WidgetT *wgtSpinner(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t step) {
    WidgetT *w = widgetAlloc(parent, sTypeId);

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

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

        w->data      = d;
        d->minValue  = minVal;
        d->maxValue  = maxVal;
        d->step      = step > 0 ? step : 1;
        d->value     = minVal;
        d->decimals  = SPINNER_DEFAULT_DECIMALS;
        d->realStep  = 1.0;
        d->selStart  = -1;
        d->selEnd    = -1;
        spinnerFormat(d);
    }

    return w;
}


double wgtSpinnerGetRealValue(const WidgetT *w) {
    VALIDATE_WIDGET(w, sTypeId, 0.0);
    SpinnerDataT *d = (SpinnerDataT *)w->data;

    if (d->editing) {
        spinnerCommitEdit(d);
    }

    return d->realValue;
}


int32_t wgtSpinnerGetValue(const WidgetT *w) {
    VALIDATE_WIDGET(w, sTypeId, 0);
    SpinnerDataT *d = (SpinnerDataT *)w->data;

    // Commit any in-progress edit so the returned value is current
    if (d->editing) {
        spinnerCommitEdit(d);
    }

    // In real mode the integer accessor must reflect the real value
    // (truncated toward zero), not the stale d->value slot.
    if (d->useReal) {
        return (int32_t)d->realValue;
    }

    return d->value;
}


void wgtSpinnerSetDecimals(WidgetT *w, int32_t decimals) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    SpinnerDataT *d = (SpinnerDataT *)w->data;

    d->decimals = (decimals >= 0 && decimals <= 10) ? decimals : SPINNER_DEFAULT_DECIMALS;
    spinnerFormat(d);
    wgtInvalidate(w);
}


void wgtSpinnerSetRange(WidgetT *w, int32_t minVal, int32_t maxVal) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    SpinnerDataT *d = (SpinnerDataT *)w->data;

    d->minValue = minVal;
    d->maxValue = maxVal;
    spinnerClampAndFormat(d);
    wgtInvalidate(w);
}


void wgtSpinnerSetRealMode(WidgetT *w, bool enable) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    SpinnerDataT *d = (SpinnerDataT *)w->data;

    d->useReal = enable;
    spinnerClampAndFormat(d);
    wgtInvalidate(w);
}


void wgtSpinnerSetRealRange(WidgetT *w, double minVal, double maxVal) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    SpinnerDataT *d = (SpinnerDataT *)w->data;

    d->realMin = minVal;
    d->realMax = maxVal;
    spinnerClampAndFormat(d);
    wgtInvalidate(w);
}


void wgtSpinnerSetRealStep(WidgetT *w, double step) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    SpinnerDataT *d = (SpinnerDataT *)w->data;

    d->realStep = step > 0.0 ? step : 0.01;
}


void wgtSpinnerSetRealValue(WidgetT *w, double value) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    SpinnerDataT *d = (SpinnerDataT *)w->data;

    d->realValue = value;
    spinnerClampAndFormat(d);
    wgtInvalidate(w);
}


void wgtSpinnerSetStep(WidgetT *w, int32_t step) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    SpinnerDataT *d = (SpinnerDataT *)w->data;

    d->step = step > 0 ? step : 1;
}


void wgtSpinnerSetValue(WidgetT *w, int32_t value) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    SpinnerDataT *d = (SpinnerDataT *)w->data;

    d->value = value;
    spinnerClampAndFormat(d);
    wgtInvalidate(w);
}


static void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    w->calcMinW = font->charWidth * 6 + SPINNER_PAD * 2 + SPINNER_BORDER * 2 + SPINNER_BTN_W;
    w->calcMinH = font->charHeight + SPINNER_PAD * 2 + SPINNER_BORDER * 2;
}


static const char *widgetSpinnerGetText(const WidgetT *w) {
    const SpinnerDataT *d = (const SpinnerDataT *)w->data;
    return d->buf;
}


// Key handling has two distinct paths: navigation keys (Up/Down/PgUp/
// PgDn) always commit any pending edit first, then adjust the numeric
// value directly. Text keys enter edit mode and are forwarded to the
// shared text editor. This split ensures arrow-key nudging always
// operates on the committed value, not on partially typed text.
//
// Page Up/Down use step*10 for coarser adjustment, matching the
// convention used by Windows spin controls.
static void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
    SpinnerDataT *d = (SpinnerDataT *)w->data;

    // Up arrow  -- increment
    if (key == KEY_UP) {
        spinnerCommitEdit(d);
        spinnerApplyStep(d, 1, 1);

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

        wgtInvalidatePaint(w);
        return;
    }

    // Down arrow  -- decrement
    if (key == KEY_DOWN) {
        spinnerCommitEdit(d);
        spinnerApplyStep(d, -1, 1);

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

        wgtInvalidatePaint(w);
        return;
    }

    // Page Up  -- increment by step * 10
    if (key == KEY_PGUP) {
        spinnerCommitEdit(d);
        spinnerApplyStep(d, 1, 10);

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

        wgtInvalidatePaint(w);
        return;
    }

    // Page Down  -- decrement by step * 10
    if (key == KEY_PGDN) {
        spinnerCommitEdit(d);
        spinnerApplyStep(d, -1, 10);

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

        wgtInvalidatePaint(w);
        return;
    }

    // Enter  -- commit edit
    if (key == '\r' || key == '\n') {
        if (d->editing) {
            spinnerCommitEdit(d);

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

            wgtInvalidatePaint(w);
        }

        return;
    }

    // Escape  -- cancel edit, revert to current value
    if (key == 27) {
        if (d->editing) {
            d->editing = false;
            spinnerFormat(d);
            wgtInvalidatePaint(w);
        }

        return;
    }

    // Filter: only allow digits, minus, dot (real mode), and control keys
    bool isDigit   = (key >= '0' && key <= '9');
    bool isMinus   = (key == '-');
    bool isDot     = (key == '.' && d->useReal);
    bool isControl = (key < 0x20) || (key & 0x100);

    if (!isDigit && !isMinus && !isDot && !isControl) {
        return;
    }

    // Minus only at position 0 (and only if min is negative)
    if (isMinus && (d->cursorPos != 0 || !spinnerAllowMinus(d))) {
        return;
    }

    // Dot only once (check if buffer already has one)
    if (isDot) {
        for (int32_t i = 0; i < d->len; i++) {
            if (d->buf[i] == '.') {
                return;
            }
        }
    }

    // Enter edit mode on first text-modifying key
    if (isDigit || isMinus || isDot || key == '\b' || key == KEY_ASCII_DEL || key == KEY_DELETE) {
        spinnerStartEdit(d);
    }

    // Delegate to shared text editing logic (handles cursor movement,
    // selection, cut/copy/paste, undo/redo, backspace, delete, etc.)
    widgetTextEditOnKey(w, key, mod,
                        d->buf, (int32_t)sizeof(d->buf),
                        &d->len, &d->cursorPos,
                        &d->scrollOff,
                        &d->selStart, &d->selEnd,
                        d->undoBuf, &d->undoLen,
                        &d->undoCursor,
                        w->w - SPINNER_BORDER * 2 - SPINNER_BTN_W);

    // Validate buffer after paste  -- reject non-numeric content.
    if (!spinnerValidateBuffer(d)) {
        // Revert to the undo buffer (pre-paste state)
        memcpy(d->buf, d->undoBuf, sizeof(d->buf));
        d->len       = d->undoLen;
        d->cursorPos = d->undoCursor;
        d->selStart  = -1;
        d->selEnd    = -1;
    }

    wgtInvalidatePaint(w);
}


// Focus loss: commit and clamp any in-progress edit, so typed-but-uncommitted
// text (e.g. an out-of-range number the user never pressed Enter on) is parsed
// and clamped to [min,max] instead of lingering. Fires the Change callback only
// when the committed value actually differs, so a focus-out with no real edit
// does not spuriously trigger the app's handler.
static void widgetSpinnerOnBlur(WidgetT *w) {
    SpinnerDataT *d = (SpinnerDataT *)w->data;

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

    int32_t oldValue     = d->value;
    double  oldRealValue = d->realValue;

    spinnerCommitEdit(d);

    bool changed = d->useReal ? (d->realValue != oldRealValue) : (d->value != oldValue);

    // Mark dirty before onChange so it lands even if the handler
    // destroys the widget.
    wgtInvalidatePaint(w);

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


// Mouse click regions: button area (right side) vs text area (left side).
// Button area is split vertically at the midpoint -- top half increments,
// bottom half decrements. Clicking a button commits any pending edit
// before adjusting the value, same as arrow keys.
//
// Text area clicks compute cursor position from pixel offset using the
// fixed-width font. Double-click selects all text (select-word doesn't
// make sense for numbers), entering edit mode to allow replacement.
static void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
    sFocusedWidget = hit;
    SpinnerDataT *d = (SpinnerDataT *)hit->data;

    int32_t btnX = hit->x + hit->w - SPINNER_BORDER - SPINNER_BTN_W;
    int32_t midY = hit->y + hit->h / 2;

    if (vx >= btnX) {
        // Click on button area
        spinnerCommitEdit(d);
        spinnerApplyStep(d, (vy < midY) ? 1 : -1, 1);

        if (hit->onChange) {
            hit->onChange(hit);
        }
    } else {
        // Click on text area
        AppContextT *ctx = (AppContextT *)root->userData;
        widgetTextEditMouseClick(hit, vx, vy, hit->x + SPINNER_BORDER + SPINNER_PAD, &ctx->font, d->buf, d->len, d->scrollOff, &d->cursorPos, &d->selStart, &d->selEnd, false, false);
        spinnerStartEdit(d);
    }

    wgtInvalidatePaint(hit);
}


// Paint uses the same 3-run text rendering approach as TextInput:
// before-selection, selection (highlighted), after-selection. This
// avoids overdraw and gives correct selection highlighting with only
// one pass over the visible text. The scroll offset ensures the
// cursor is always visible even when the number is wider than the
// text area.
//
// The two buttons (up/down) extend SPINNER_BORDER pixels past the
// button area into the widget's right border so they visually merge
// with the outer bevel -- this is why btnW is btnW + SPINNER_BORDER
// in the drawBevel calls.
static void widgetSpinnerPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    SpinnerDataT *d = (SpinnerDataT *)w->data;
    uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow;
    uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;

    int32_t btnW = SPINNER_BTN_W;
    int32_t btnX = w->x + w->w - SPINNER_BORDER - btnW;

    // Sunken border around entire widget
    BevelStyleT bevel;
    bevel.highlight = colors->windowShadow;
    bevel.shadow    = colors->windowHighlight;
    bevel.face      = bg;
    bevel.width     = SPINNER_BORDER;
    drawBevel(disp, ops, w->x, w->y, w->w, w->h, &bevel);

    // Text area
    int32_t textX    = w->x + SPINNER_BORDER + SPINNER_PAD;
    int32_t textY    = w->y + (w->h - font->charHeight) / 2;
    int32_t textW    = btnX - (w->x + SPINNER_BORDER) - SPINNER_PAD * 2;
    int32_t maxChars = textW / font->charWidth;

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

    // Scroll to keep cursor visible
    if (d->cursorPos < d->scrollOff) {
        d->scrollOff = d->cursorPos;
    } else if (d->cursorPos > d->scrollOff + maxChars) {
        d->scrollOff = d->cursorPos - maxChars;
    }

    int32_t off = d->scrollOff;
    int32_t len = d->len - off;

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

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

    widgetTextEditPaintLine(disp, ops, font, colors, textX, textY, &d->buf[off], len, off, d->cursorPos, d->selStart, d->selEnd, fg, bg, w == sFocusedWidget, w->x + SPINNER_BORDER, btnX - SPINNER_PAD);

    // Up button (top half)
    int32_t btnTopH = w->h / 2;
    int32_t btnBotH = w->h - btnTopH;

    BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
    drawBevel(disp, ops, btnX, w->y, btnW + SPINNER_BORDER, btnTopH, &btnBevel);

    // Up arrow triangle
    {
        int32_t cx = btnX + btnW / 2;
        int32_t cy = w->y + btnTopH / 2;

        for (int32_t i = 0; i < 3; i++) {
            drawHLine(disp, ops, cx - i, cy - 1 + i, 1 + i * 2, fg);
        }
    }

    // Down button (bottom half)
    drawBevel(disp, ops, btnX, w->y + btnTopH, btnW + SPINNER_BORDER, btnBotH, &btnBevel);

    // Down arrow triangle
    {
        int32_t cx = btnX + btnW / 2;
        int32_t cy = w->y + btnTopH + btnBotH / 2;

        for (int32_t i = 0; i < 3; i++) {
            drawHLine(disp, ops, cx - i, cy + 1 - i, 1 + i * 2, fg);
        }
    }

    // Focus rect around entire widget
    if (w == sFocusedWidget) {
        drawFocusRect(disp, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
    }
}


static void widgetSpinnerSetText(WidgetT *w, const char *text) {
    SpinnerDataT *d = (SpinnerDataT *)w->data;

    if (d->useReal) {
        d->realValue = strtod(text, NULL);
    } else {
        d->value = (int32_t)strtol(text, NULL, 10);
    }

    spinnerClampAndFormat(d);
}
