// 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
// widgetSlider.c  -- Slider (trackbar) widget
//
// A continuous-value selector with a draggable thumb on a groove track.
// Supports both horizontal and vertical orientation. The value maps
// linearly to thumb position using integer arithmetic only.
//
// Rendering: thin sunken groove for the track, raised 2px-bevel thumb,
// and a center tick line on the thumb for grip feedback. The groove
// uses reversed highlight/shadow (same as progress bar trough) for
// the recessed look. The thumb uses the standard raised bevel.
//
// Interaction model: clicking on the track jumps the thumb to that
// position (instant seek), while clicking on the thumb starts a drag
// via the global sDragSlider/sDragOffset state. The drag offset stores
// where within the thumb the user grabbed, so the thumb doesn't snap
// to the cursor center during drag -- this feels much more natural.
//
// Keyboard: arrow keys increment/decrement by a computed step that
// scales with range (step = range/100 for ranges > 100, else 1).
// Home/End jump to min/max. This gives ~100 keyboard steps regardless
// of the actual value range.

#include "dvxWgtP.h"

#define SLIDER_TRACK_H       4
#define SLIDER_THUMB_W       11
// Tick-line inset from the widget's edge (each side).  Leaves a margin
// between the thumb's center-tick and where the track ends.
#define SLIDER_TICK_INSET    3

static int32_t sTypeId = -1;

typedef struct {
    int32_t value;
    int32_t minValue;
    int32_t maxValue;
    bool    vertical;
    int32_t dragOffset;
} SliderDataT;


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

void         wgtRegister(void);
WidgetT     *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal);
int32_t      wgtSliderGetValue(const WidgetT *w);
void         wgtSliderSetValue(WidgetT *w, int32_t value);
void         widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font);
static void  widgetSliderOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mouseX, int32_t mouseY);
void         widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod);
void         widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
void         widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);


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


static const WidgetClassT sClassSlider = {
    .version = WGT_CLASS_VERSION,
    .flags   = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE,
    .handlers = {
        [WGT_METHOD_PAINT]          = (void *)widgetSliderPaint,
        [WGT_METHOD_CALC_MIN_SIZE]  = (void *)widgetSliderCalcMinSize,
        [WGT_METHOD_ON_MOUSE]       = (void *)widgetSliderOnMouse,
        [WGT_METHOD_ON_KEY]         = (void *)widgetSliderOnKey,
        [WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetSliderOnDragUpdate,
    }
};

static const struct {
    WidgetT *(*create)(WidgetT *parent, int32_t minVal, int32_t maxVal);
    void     (*setValue)(WidgetT *w, int32_t value);
    int32_t  (*getValue)(const WidgetT *w);
} sApi = {
    .create   = wgtSlider,
    .setValue  = wgtSliderSetValue,
    .getValue = wgtSliderGetValue
};

static const WgtPropDescT sProps[] = {
    { "Value", WGT_IFACE_INT, (void *)wgtSliderGetValue, (void *)wgtSliderSetValue, NULL }
};

static const WgtIfaceT sIface = {
    .basName      = "HScrollBar",
    .props        = sProps,
    .propCount    = 1,
    .methods      = NULL,
    .methodCount  = 0,
    .events       = NULL,
    .eventCount   = 0,
    .createSig    = WGT_CREATE_PARENT_INT_INT,
    .createArgs   = { 0, 100 },
    .defaultEvent = "Change",
    .namePrefix   = "HScroll"
};

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


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

    if (!w) {
        return NULL;
    }

    SliderDataT *d = (SliderDataT *)calloc(1, sizeof(SliderDataT));

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

    d->value    = minVal;
    d->minValue = minVal;
    d->maxValue = maxVal;
    w->data     = d;
    w->weight   = WGT_WEIGHT_FILL;
    return w;
}


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

    return d->value;
}


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

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

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

    d->value = value;
    wgtInvalidatePaint(w);
}


// Min size: 5 thumb-widths along the main axis gives enough room for
// the thumb to travel meaningfully. Cross-axis is thumb width + 4px
// margin so the groove has visual breathing room around the thumb.
void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    (void)font;
    SliderDataT *d = (SliderDataT *)w->data;

    if (d->vertical) {
        w->calcMinW = SLIDER_THUMB_W + 4;
        w->calcMinH = SLIDER_THUMB_W * 5;
    } else {
        w->calcMinW = SLIDER_THUMB_W * 5;
        w->calcMinH = SLIDER_THUMB_W + 4;
    }
}


// Called by the event loop during slider thumb drag. Computes the
// new value from mouse position and the stored drag offset.
static void widgetSliderOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mouseX, int32_t mouseY) {
    (void)root;
    SliderDataT *d = (SliderDataT *)w->data;
    int32_t range = d->maxValue - d->minValue;

    if (range <= 0) {
        return;
    }

    // Guard the divisor: a slider sized at or below the thumb width has
    // no travel and would divide by zero (or negative) here.
    int32_t thumbRange = (d->vertical ? w->h : w->w) - SLIDER_THUMB_W;

    if (thumbRange <= 0) {
        return;
    }

    int32_t newVal;

    if (d->vertical) {
        int32_t relY = mouseY - w->y - d->dragOffset;
        newVal = d->minValue + (relY * range) / thumbRange;
    } else {
        int32_t relX = mouseX - w->x - d->dragOffset;
        newVal = d->minValue + (relX * range) / thumbRange;
    }

    newVal = clampInt(newVal, d->minValue, d->maxValue);

    if (newVal != d->value) {
        d->value = newVal;

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


void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod) {
    (void)mod;
    SliderDataT *d = (SliderDataT *)w->data;

    int32_t range    = d->maxValue - d->minValue;
    int32_t pageStep = range / 10;

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

    // Arrow keys: step by 1.  Page Up/Down: step by 10% of range.
    // Home/End: jump to min/max.
    if (d->vertical) {
        if (key == KEY_UP) {
            d->value -= 1;
        } else if (key == KEY_DOWN) {
            d->value += 1;
        } else if (key == KEY_PGUP) {
            d->value -= pageStep;
        } else if (key == KEY_PGDN) {
            d->value += pageStep;
        } else if (key == KEY_HOME) {
            d->value = d->minValue;
        } else if (key == KEY_END) {
            d->value = d->maxValue;
        } else {
            return;
        }
    } else {
        if (key == KEY_LEFT || key == KEY_UP) {
            d->value -= 1;
        } else if (key == KEY_RIGHT || key == KEY_DOWN) {
            d->value += 1;
        } else if (key == KEY_PGUP) {
            d->value -= pageStep;
        } else if (key == KEY_PGDN) {
            d->value += pageStep;
        } else if (key == KEY_HOME) {
            d->value = d->minValue;
        } else if (key == KEY_END) {
            d->value = d->maxValue;
        } else {
            return;
        }
    }

    d->value = clampInt(d->value, d->minValue, d->maxValue);

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

    wgtInvalidatePaint(w);
}


// Mouse click distinguishes between thumb hit (start drag) and track
// hit (jump to position). The thumb hit area is the full SLIDER_THUMB_W
// pixel range at the current thumb position. The jump-to-position
// calculation centers the thumb at the click point by subtracting
// half the thumb width before converting pixel position to value.
//
// Drag state is stored in globals (sDragSlider, sDragOffset) rather
// than per-widget because only one slider can be dragged at a time.
// The event loop checks sDragSlider on mouse-move to continue the drag.
void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
    (void)root;
    sFocusedWidget = hit;
    SliderDataT *d = (SliderDataT *)hit->data;
    int32_t range = d->maxValue - d->minValue;

    if (range <= 0) {
        return;
    }

    int32_t thumbRange;
    int32_t thumbPos;
    int32_t mousePos;

    if (d->vertical) {
        thumbRange = hit->h - SLIDER_THUMB_W;

        if (thumbRange <= 0) {
            return;  // no travel; nothing to position
        }

        thumbPos   = ((d->value - d->minValue) * thumbRange) / range;
        mousePos   = vy - hit->y;

        if (mousePos >= thumbPos && mousePos < thumbPos + SLIDER_THUMB_W) {
            // Click on thumb  -- start drag
            sDragWidget = hit;
            d->dragOffset = mousePos - thumbPos;
        } else {
            // Click on track  -- jump to position
            int32_t newVal = d->minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange;

            if (newVal < d->minValue) { newVal = d->minValue; }
            if (newVal > d->maxValue) { newVal = d->maxValue; }

            d->value = newVal;
            wgtInvalidatePaint(hit);

            if (hit->onChange) {
                hit->onChange(hit);
            }
        }
    } else {
        thumbRange = hit->w - SLIDER_THUMB_W;

        if (thumbRange <= 0) {
            return;  // no travel; nothing to position
        }

        thumbPos   = ((d->value - d->minValue) * thumbRange) / range;
        mousePos   = vx - hit->x;

        if (mousePos >= thumbPos && mousePos < thumbPos + SLIDER_THUMB_W) {
            // Click on thumb  -- start drag
            sDragWidget = hit;
            d->dragOffset = mousePos - thumbPos;
        } else {
            // Click on track  -- jump to position
            int32_t newVal = d->minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange;

            if (newVal < d->minValue) { newVal = d->minValue; }
            if (newVal > d->maxValue) { newVal = d->maxValue; }

            d->value = newVal;
            wgtInvalidatePaint(hit);

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


void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    (void)font;
    SliderDataT *sd = (SliderDataT *)w->data;
    uint32_t fg      = w->fgColor ? w->fgColor : colors->contentFg;
    uint32_t bg      = w->bgColor ? w->bgColor : colors->contentBg;
    uint32_t tickFg  = w->enabled ? fg : colors->windowShadow;
    uint32_t thumbFg = w->enabled ? colors->buttonFace : colors->scrollbarTrough;

    // Clear full widget area so old thumb position is erased
    rectFill(d, ops, w->x, w->y, w->w, w->h, bg);

    int32_t range = sd->maxValue - sd->minValue;

    if (range <= 0) {
        range = 1;
    }

    if (sd->vertical) {
        // Track groove
        int32_t trackX = w->x + (w->w - SLIDER_TRACK_H) / 2;
        BevelStyleT groove;
        groove.highlight = colors->windowShadow;
        groove.shadow    = colors->windowHighlight;
        groove.face      = colors->scrollbarTrough;
        groove.width     = 1;
        drawBevel(d, ops, trackX, w->y, SLIDER_TRACK_H, w->h, &groove);

        // Thumb
        int32_t thumbRange = w->h - SLIDER_THUMB_W;
        int32_t thumbY     = w->y + ((sd->value - sd->minValue) * thumbRange) / range;

        BevelStyleT thumb;
        thumb.highlight = colors->windowHighlight;
        thumb.shadow    = colors->windowShadow;
        thumb.face      = thumbFg;
        thumb.width     = 2;
        drawBevel(d, ops, w->x, thumbY, w->w, SLIDER_THUMB_W, &thumb);

        // Center tick on thumb
        drawHLine(d, ops, w->x + SLIDER_TICK_INSET, thumbY + SLIDER_THUMB_W / 2, w->w - 2 * SLIDER_TICK_INSET, tickFg);
    } else {
        // Track groove
        int32_t trackY = w->y + (w->h - SLIDER_TRACK_H) / 2;
        BevelStyleT groove;
        groove.highlight = colors->windowShadow;
        groove.shadow    = colors->windowHighlight;
        groove.face      = colors->scrollbarTrough;
        groove.width     = 1;
        drawBevel(d, ops, w->x, trackY, w->w, SLIDER_TRACK_H, &groove);

        // Thumb
        int32_t thumbRange = w->w - SLIDER_THUMB_W;
        int32_t thumbX     = w->x + ((sd->value - sd->minValue) * thumbRange) / range;

        BevelStyleT thumb;
        thumb.highlight = colors->windowHighlight;
        thumb.shadow    = colors->windowShadow;
        thumb.face      = thumbFg;
        thumb.width     = 2;
        drawBevel(d, ops, thumbX, w->y, SLIDER_THUMB_W, w->h, &thumb);

        // Center tick on thumb
        drawVLine(d, ops, thumbX + SLIDER_THUMB_W / 2, w->y + SLIDER_TICK_INSET, w->h - 2 * SLIDER_TICK_INSET, tickFg);
    }

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