// 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
// widgetRadio.c  -- RadioGroup and Radio button widgets
//
// Two-level architecture: RadioGroupE is an invisible container that
// holds the selection state (selectedIdx), while RadioE children are
// the visible buttons. This separation means the group tracks which
// index is selected while each radio button only knows its own index,
// keeping the per-button state minimal.
//
// Selection state is stored as an integer index on the parent group
// rather than a pointer, because indices survive widget reordering
// and are trivially serializable. The index is auto-assigned at
// construction time based on sibling order.
//
// Rendering: diamond-shaped indicator (Motif-style) using scanline
// horizontal lines rather than a circle algorithm. This avoids any
// need for anti-aliasing or trigonometry -- purely integer H/V lines.
// The selection dot inside uses a hardcoded width table for a 6-row
// filled diamond, sized to look crisp at the fixed 12px box size.
//
// Keyboard: Space/Enter select the current radio. Arrow keys move
// focus AND selection together (standard radio group behavior --
// focus and selection are coupled, unlike checkboxes).

#include "dvxWgtP.h"

#define CHECKBOX_BOX_SIZE    12
#define CHECKBOX_GAP         4

static int32_t sRadioGroupTypeId = -1;
static int32_t sRadioTypeId      = -1;

typedef struct {
    const char *text;
    int32_t     index;
    // `selected` is the source of truth for .frm-loaded OptionButtons,
    // whose parent is a plain layout container rather than a
    // RadioGroup.  When the parent IS a RadioGroup, the group's
    // selectedIdx still wins (the group view is canonical and the C
    // API callers like dvxdemo keep using it).
    bool        selected;
} RadioDataT;

typedef struct {
    int32_t selectedIdx;
} RadioGroupDataT;


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

static void  invalidateOldSelection(WidgetT *group, int32_t oldIdx);
WidgetT     *wgtRadio(WidgetT *parent, const char *text);
int32_t      wgtRadioGetIndex(const WidgetT *w);
WidgetT     *wgtRadioGroup(WidgetT *parent);
void         wgtRadioGroupSetSelected(WidgetT *w, int32_t idx);
void         wgtRegister(void);
void         widgetRadioAccelActivate(WidgetT *w, WidgetT *root);
void         widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font);
void         widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod);
void         widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void         widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);


// Find the currently selected radio in the group and invalidate it
static void invalidateOldSelection(WidgetT *group, int32_t oldIdx) {
    for (WidgetT *c = group->firstChild; c; c = c->nextSibling) {
        if (c->type == sRadioTypeId) {
            RadioDataT *rd = (RadioDataT *)c->data;

            if (rd->index == oldIdx) {
                wgtInvalidatePaint(c);
                return;
            }
        }
    }
}


WidgetT *wgtRadio(WidgetT *parent, const char *text) {
    WidgetT *w = widgetAllocWithText(parent, sRadioTypeId, sizeof(RadioDataT), text);

    // widgetAllocWithText permits a NULL parent; the index loop below
    // dereferences parent, so only run it when there is one (index stays 0,
    // the calloc default, otherwise).
    if (w && w->data && parent) {
        RadioDataT *d = (RadioDataT *)w->data;

        // Auto-assign index based on position in parent
        int32_t idx = 0;

        for (WidgetT *c = parent->firstChild; c != w; c = c->nextSibling) {
            if (c->type == sRadioTypeId) {
                idx++;
            }
        }

        d->index = idx;
    }

    return w;
}


int32_t wgtRadioGetIndex(const WidgetT *w) {
    VALIDATE_WIDGET(w, sRadioTypeId, -1);
    RadioDataT *d = (RadioDataT *)w->data;

    return d->index;
}


WidgetT *wgtRadioGroup(WidgetT *parent) {
    WidgetT *w = widgetAlloc(parent, sRadioGroupTypeId);

    if (w) {
        RadioGroupDataT *d = calloc(1, sizeof(RadioGroupDataT));
        w->data = d;
    }

    return w;
}


void wgtRadioGroupSetSelected(WidgetT *w, int32_t idx) {
    VALIDATE_WIDGET_VOID(w, sRadioGroupTypeId);
    RadioGroupDataT *d = (RadioGroupDataT *)w->data;

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


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


static const WidgetClassT sClassRadioGroup = {
    .version = WGT_CLASS_VERSION,
    .flags   = 0,
    .handlers = {
        [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetCalcMinSizeBox,
        [WGT_METHOD_LAYOUT]        = (void *)widgetLayoutBox,
    }
};

static const WidgetClassT sClassRadio = {
    .version = WGT_CLASS_VERSION,
    .flags   = WCLASS_FOCUSABLE | WCLASS_HAS_TEXT,
    .handlers = {
        [WGT_METHOD_PAINT]             = (void *)widgetRadioPaint,
        [WGT_METHOD_CALC_MIN_SIZE]     = (void *)widgetRadioCalcMinSize,
        [WGT_METHOD_ON_MOUSE]          = (void *)widgetRadioOnMouse,
        [WGT_METHOD_ON_KEY]            = (void *)widgetRadioOnKey,
        [WGT_METHOD_ON_ACCEL_ACTIVATE] = (void *)widgetRadioAccelActivate,
        [WGT_METHOD_GET_TEXT]          = (void *)widgetTextGet,
        [WGT_METHOD_SET_TEXT]          = (void *)widgetTextSet,
    }
};

// The `create` function MUST be the first field of this struct:
// createWidgetByIface dereferences the api pointer as a function
// pointer to call the widget's create function, so whatever field
// is first is what gets invoked when BASIC instantiates the widget.
static const struct {
    WidgetT *(*create)(WidgetT *parent, const char *text);
    WidgetT *(*group)(WidgetT *parent);
    void     (*groupSetSelected)(WidgetT *group, int32_t index);
    int32_t  (*getIndex)(const WidgetT *w);
} sApi = {
    .create           = wgtRadio,
    .group            = wgtRadioGroup,
    .groupSetSelected = wgtRadioGroupSetSelected,
    .getIndex         = wgtRadioGetIndex
};

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

static const WgtMethodDescT sMethods[] = {
    { "SetSelected", WGT_SIG_INT, (void *)wgtRadioGroupSetSelected }
};

static const WgtIfaceT sIface = {
    .basName      = "OptionButton",
    .props        = sProps,
    .propCount    = 1,
    .methods      = sMethods,
    .methodCount  = 1,
    .events       = NULL,
    .eventCount   = 0,
    .createSig    = WGT_CREATE_PARENT_TEXT,
    .defaultEvent = "Click",
    .namePrefix   = "Option"
};

void wgtRegister(void) {
    sRadioGroupTypeId = wgtRegisterClass(&sClassRadioGroup);
    sRadioTypeId      = wgtRegisterClass(&sClassRadio);
    wgtRegisterApi("radio", &sApi);
    wgtRegisterIface("radio", &sIface);
}


void widgetRadioAccelActivate(WidgetT *w, WidgetT *root) {
    (void)root;
    widgetRadioOnMouse(w, NULL, 0, 0);
}


// Shares CHECKBOX_BOX_SIZE/GAP constants with the checkbox widget
// so radio buttons and checkboxes align when placed in the same
// column, and the indicator + label layout is visually consistent.
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    RadioDataT *d = (RadioDataT *)w->data;
    // +1 on the width: widgetRadioPaint draws the focus rect from
    // labelX-1 with width labelW+2, whose rightmost column lands at
    // w->x + (BOX+GAP+labelW).  Without the +1 that column is one
    // pixel past the widget's claimed right edge, so the rectFill
    // in the next paint doesn't clear it and the focus dot survives.
    w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
                   textWidthAccel(font, d->text) + 1;
    w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight);
}


// Arrow keys move focus AND selection simultaneously -- this is the
// standard radio group behavior where navigating with arrows also
// commits the selection. This differs from checkboxes where arrows
// just move focus and Space toggles. The coupling is deliberate:
// radio groups represent a single mutually-exclusive choice, so
// "looking at" an option means "choosing" it.
//
// Key codes use DOS BIOS scancode convention: high byte 0x01 flag
// ORed with the scancode. 0x50=Down, 0x4D=Right, 0x48=Up, 0x4B=Left.
// Selects `target` within `parent` -- parent may be a RadioGroup (C
// API flow, updates group selectedIdx) or a plain layout container
// (.frm-loaded OptionButton flow, updates each RadioDataT.selected).
static void selectRadio(WidgetT *parent, WidgetT *target) {
    if (!parent || !target) {
        return;
    }

    RadioDataT *td = (RadioDataT *)target->data;

    if (parent->type == sRadioGroupTypeId) {
        RadioGroupDataT *gd = (RadioGroupDataT *)parent->data;
        invalidateOldSelection(parent, gd->selectedIdx);
        gd->selectedIdx = td ? td->index : 0;
    } else {
        for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
            if (c->type == sRadioTypeId && c != target) {
                RadioDataT *sd = (RadioDataT *)c->data;

                if (sd && sd->selected) {
                    sd->selected = false;
                    wgtInvalidatePaint(c);
                }
            }
        }

        if (td) {
            td->selected = true;
        }
    }

    if (parent->type == sRadioGroupTypeId && parent->onChange) {
        parent->onChange(parent);
    } else if (parent->type != sRadioGroupTypeId && target->onChange) {
        target->onChange(target);
    }
}


void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
    (void)mod;

    if (!w->parent) {
        return;
    }

    if (key == ' ' || key == 0x0D) {
        selectRadio(w->parent, w);
        wgtInvalidatePaint(w);
    } else if (key == KEY_DOWN || key == KEY_RIGHT) {
        WidgetT *next = NULL;

        for (WidgetT *s = w->nextSibling; s; s = s->nextSibling) {
            if (s->type == sRadioTypeId && s->visible && s->enabled) {
                next = s;
                break;
            }
        }

        if (next) {
            wgtInvalidatePaint(w);  // erase focus rect on widget we are leaving
            sFocusedWidget = next;
            selectRadio(w->parent, next);
            wgtInvalidatePaint(next);
        }
    } else if (key == KEY_UP || key == KEY_LEFT) {
        WidgetT *prev = NULL;

        for (WidgetT *s = w->parent->firstChild; s && s != w; s = s->nextSibling) {
            if (s->type == sRadioTypeId && s->visible && s->enabled) {
                prev = s;
            }
        }

        if (prev) {
            wgtInvalidatePaint(w);  // erase focus rect on widget we are leaving
            sFocusedWidget = prev;
            selectRadio(w->parent, prev);
            wgtInvalidatePaint(prev);
        }
    }
}


void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
    (void)root;
    (void)vx;
    (void)vy;
    sFocusedWidget = w;

    // selectRadio handles both parent types (RadioGroup vs plain layout
    // container) and the onChange dispatch; this was a verbatim copy of it.
    selectRadio(w->parent, w);

    // Always repaint the clicked radio: its selection dot or focus rect
    // may need to appear.  The mouse dispatcher invalidates the previous
    // focus owner, but nothing invalidates the newly-focused widget
    // unless we do it here.
    wgtInvalidatePaint(w);
}


// The diamond radio indicator is rendered in three passes:
// 1. Interior fill -- scanline-based diamond fill for the background
// 2. Border -- upper-left edges in shadow, lower-right in highlight
//    (sunken appearance, same lighting model as bevels)
// 3. Selection dot -- smaller filled diamond from a hardcoded 6-row
//    width table. The static const avoids recomputation per paint.
//
// This approach avoids floating point entirely. Each scanline's
// left/right extent is computed with simple integer distance from
// the midpoint, making the diamond perfectly symmetric at any size.
void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    RadioDataT *rd = (RadioDataT *)w->data;
    uint32_t fg    = w->fgColor ? w->fgColor : colors->contentFg;
    uint32_t bg    = w->bgColor ? w->bgColor : colors->contentBg;
    int32_t boxY   = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2;

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

    // Draw diamond-shaped radio box
    int32_t bx  = w->x;
    int32_t mid = CHECKBOX_BOX_SIZE / 2;
    uint32_t hi = colors->windowShadow;
    uint32_t sh = colors->windowHighlight;

    // Fill interior
    for (int32_t i = 0; i < CHECKBOX_BOX_SIZE; i++) {
        int32_t dist  = i < mid ? i : CHECKBOX_BOX_SIZE - 1 - i;
        int32_t left  = mid - dist;
        int32_t right = mid + dist;

        if (right > left) {
            drawHLine(d, ops, bx + left + 1, boxY + i, right - left - 1, bg);
        }
    }

    // Diamond border  -- upper-left edges get highlight, lower-right get shadow
    for (int32_t i = 0; i < mid; i++) {
        int32_t left  = mid - i;
        int32_t right = mid + i;
        drawHLine(d, ops, bx + left, boxY + i, 1, hi);
        drawHLine(d, ops, bx + right, boxY + i, 1, sh);
    }

    for (int32_t i = mid; i < CHECKBOX_BOX_SIZE; i++) {
        int32_t left  = mid - (CHECKBOX_BOX_SIZE - 1 - i);
        int32_t right = mid + (CHECKBOX_BOX_SIZE - 1 - i);
        drawHLine(d, ops, bx + left, boxY + i, 1, hi);
        drawHLine(d, ops, bx + right, boxY + i, 1, sh);
    }

    // Draw filled diamond if selected.  Two sources of truth: the
    // RadioGroup's selectedIdx (when parent is a RadioGroup, via C
    // API or wgtRadioGroupSetSelected), or this widget's own
    // rd->selected bit (set by mouse/key handlers when the radio is
    // just a child of a layout container, as happens for
    // .frm-loaded OptionButtons).
    bool isSelected = false;

    if (w->parent && w->parent->type == sRadioGroupTypeId) {
        RadioGroupDataT *gd = (RadioGroupDataT *)w->parent->data;
        isSelected = (gd->selectedIdx == rd->index);
    } else {
        isSelected = rd->selected;
    }

    if (isSelected) {
        uint32_t dotFg = w->enabled ? fg : colors->windowShadow;

        static const int32_t dotW[] = {2, 4, 6, 6, 4, 2};

        for (int32_t i = 0; i < 6; i++) {
            int32_t dw = dotW[i];
            drawHLine(d, ops, bx + mid - dw / 2, boxY + mid - 3 + i, dw, dotFg);
        }
    }

    int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
    int32_t labelY = w->y + (w->h - font->charHeight) / 2;
    int32_t labelW = textWidthAccel(font, rd->text);

    drawWidgetTextAccel(d, ops, font, labelX, labelY, rd->text, fg, bg, false, w->enabled, colors);

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


