// 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.

// ideMenuEditor.c -- DVX BASIC menu editor dialog
//
// VB3-style modal dialog for designing form menu bars. Works on
// a cloned copy of the menu item array; changes are only applied
// when the user clicks OK.

#include "ideMenuEditor.h"
#include "dvxDlg.h"
#include "dvxWm.h"
#include "box/box.h"
#include "button/button.h"
#include "checkbox/checkbox.h"
#include "label/label.h"
#include "listBox/listBox.h"
#include "textInput/textInpt.h"

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

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

#define MAX_MENU_LEVEL     (DSGN_MENU_STACK_DEPTH - 1)  // deepest level the preview bar can show
#define ARROW_STR          "-> "
#define MED_MSG_BUF        128  // error-message scratch buffer
#define MED_SEP_PREFIX     "mnuSep"  // auto-generated separator name prefix
#define MED_SEP_PREFIX_LEN ((int32_t)(sizeof(MED_SEP_PREFIX) - 1))
#define MED_NAME_PREFIX    "mnu"     // auto-generated item name prefix
#define MED_LABEL_BUF      (DSGN_MAX_TEXT + DSGN_MENU_STACK_DEPTH * (int32_t)sizeof(ARROW_STR))
#define MED_DLG_W          360
#define MED_DLG_H          420
#define MED_ROW_SPACING    4
#define MED_CHECK_SPACING  12
#define MED_OK_SPACING     8
#define MED_LABEL_W        60
#define MED_ARROW_BTN_W    32
#define MED_OK_BTN_W       60
#define MED_DLG_TITLE      "Menu Editor"

// ============================================================
// Dialog state
// ============================================================

typedef struct {
    // Fields are ordered by alignment to minimize struct padding.
    AppContextT  *ctx;
    DsgnFormT    *form;

    // Working copy of menu items
    DsgnMenuItemT *items;       // stb_ds array
    int32_t        selectedIdx;

    // Widgets
    WidgetT       *captionInput;
    WidgetT       *nameInput;
    WidgetT       *checkedCb;
    WidgetT       *radioCheckCb;
    WidgetT       *enabledCb;
    WidgetT       *popupCb;       // Top-level popup (hidden from menu bar)
    WidgetT       *listBox;

    bool           done;
    bool           accepted;
    bool           nameAutoGen;    // true = name was auto-generated, update on caption change
} MnuEdStateT;

static MnuEdStateT sMed;

// Dynamic per-refresh display buffers (stb_ds). The listBox stores the pointer
// array, so these must outlive each SetItems call. Freed on dialog teardown.
static char       **sLabelBufs = NULL;
static const char **sLabels    = NULL;

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

static void    applyFields(void);
static int32_t findSubtreeEnd(int32_t idx);
static void    loadFields(void);
static void    onCancel(WidgetT *w);
static void    onCaptionChange(WidgetT *w);
static void    onDelete(WidgetT *w);
static void    onIndent(WidgetT *w);
static void    onInsert(WidgetT *w);
static void    onListClick(WidgetT *w);
static void    onMoveDown(WidgetT *w);
static void    onMoveUp(WidgetT *w);
static void    onNameChange(WidgetT *w);
static void    onNext(WidgetT *w);
static void    onOk(WidgetT *w);
static void    onOutdent(WidgetT *w);
static void    rebuildList(void);


static void applyFields(void) {
    int32_t count = (int32_t)arrlen(sMed.items);

    if (sMed.selectedIdx < 0 || sMed.selectedIdx >= count) {
        return;
    }

    DsgnMenuItemT *mi = &sMed.items[sMed.selectedIdx];
    const char *cap = wgtGetText(sMed.captionInput);
    const char *nam = wgtGetText(sMed.nameInput);

    if (cap) {
        snprintf(mi->caption, DSGN_MAX_TEXT, "%s", cap);
    }

    if (nam) {
        snprintf(mi->name, DSGN_MAX_NAME, "%s", nam);
    }

    // Auto-generate name from caption if name is empty or was auto-generated
    if ((mi->name[0] == '\0' || sMed.nameAutoGen) && mi->caption[0] != '\0') {
        char autoName[DSGN_MAX_NAME];

        if (mi->caption[0] == '-') {
            // Separator: generate mnuSep1, mnuSep2, etc.
            int32_t sepNum = 1;
            int32_t itemCount = (int32_t)arrlen(sMed.items);

            for (int32_t i = 0; i < itemCount; i++) {
                if (i != sMed.selectedIdx && strncasecmp(sMed.items[i].name, MED_SEP_PREFIX, MED_SEP_PREFIX_LEN) == 0) {
                    int32_t n = atoi(sMed.items[i].name + MED_SEP_PREFIX_LEN);

                    if (n >= sepNum) {
                        sepNum = n + 1;
                    }
                }
            }

            snprintf(autoName, DSGN_MAX_NAME, MED_SEP_PREFIX "%d", (int)sepNum);
        } else {
            // Normal item: strip & and non-alphanumeric, prefix "mnu"
            int32_t p = snprintf(autoName, DSGN_MAX_NAME, "%s", MED_NAME_PREFIX);

            for (const char *c = mi->caption; *c && p < DSGN_MAX_NAME - 1; c++) {
                if (*c == '&') {
                    continue;
                }

                if ((*c >= 'A' && *c <= 'Z') || (*c >= 'a' && *c <= 'z') || (*c >= '0' && *c <= '9')) {
                    autoName[p++] = *c;
                }
            }

            autoName[p] = '\0';
        }

        snprintf(mi->name, DSGN_MAX_NAME, "%s", autoName);
        wgtSetText(sMed.nameInput, mi->name);
        sMed.nameAutoGen = true;
    }

    mi->checked    = wgtCheckboxIsChecked(sMed.checkedCb);
    mi->radioCheck = wgtCheckboxIsChecked(sMed.radioCheckCb);
    mi->enabled    = wgtCheckboxIsChecked(sMed.enabledCb);
    // Popup checkbox only meaningful on top-level items; for nested
    // items `visible` stays true (the field is ignored there).
    if (mi->level == 0) {
        mi->visible = !wgtCheckboxIsChecked(sMed.popupCb);
    } else {
        mi->visible = true;
    }
}


static int32_t findSubtreeEnd(int32_t idx) {
    int32_t count = (int32_t)arrlen(sMed.items);
    int32_t level = sMed.items[idx].level;
    int32_t end   = idx + 1;

    while (end < count && sMed.items[end].level > level) {
        end++;
    }

    return end;
}


static void loadFields(void) {
    int32_t count = (int32_t)arrlen(sMed.items);

    // Reset auto-gen flag -- if the loaded item has an empty name, it's
    // eligible for auto-generation; if it has a name, the user set it.
    sMed.nameAutoGen = false;

    if (sMed.selectedIdx < 0 || sMed.selectedIdx >= count) {
        wgtSetText(sMed.captionInput, "");
        wgtSetText(sMed.nameInput, "");
        wgtCheckboxSetChecked(sMed.checkedCb, false);
        wgtCheckboxSetChecked(sMed.radioCheckCb, false);
        wgtCheckboxSetChecked(sMed.enabledCb, true);
        wgtCheckboxSetChecked(sMed.popupCb, false);
        wgtSetEnabled(sMed.popupCb, false);
        sMed.nameAutoGen = true; // new blank item -- auto-gen eligible
        return;
    }

    DsgnMenuItemT *mi = &sMed.items[sMed.selectedIdx];
    sMed.nameAutoGen = (mi->name[0] == '\0');
    wgtSetText(sMed.captionInput, mi->caption);
    wgtSetText(sMed.nameInput, mi->name);
    wgtCheckboxSetChecked(sMed.checkedCb, mi->checked);
    wgtCheckboxSetChecked(sMed.radioCheckCb, mi->radioCheck);
    wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled);

    // Popup (Visible=False) only applies to top-level menus.
    wgtCheckboxSetChecked(sMed.popupCb, mi->level == 0 && !mi->visible);
    wgtSetEnabled(sMed.popupCb, mi->level == 0);
}


bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
    memset(&sMed, 0, sizeof(sMed));
    sMed.ctx          = ctx;
    sMed.form         = form;
    sMed.selectedIdx  = 0;

    // Clone menu items as working copy
    int32_t count = (int32_t)arrlen(form->menuItems);

    for (int32_t i = 0; i < count; i++) {
        arrput(sMed.items, form->menuItems[i]);
    }

    // If empty, start with one blank item so the user can type immediately
    if (arrlen(sMed.items) == 0) {
        DsgnMenuItemT mi;
        dsgnMenuItemInit(&mi);
        arrput(sMed.items, mi);
    }

    // Create modal dialog
    WindowT *win = dvxCreateWindowCentered(ctx, MED_DLG_TITLE, MED_DLG_W, MED_DLG_H, false);

    if (!win) {
        arrfree(sMed.items);
        return false;
    }

    win->maxW = win->w;
    win->maxH = win->h;

    WidgetT *root = wgtInitWindow(ctx, win);
    root->spacing = wgtPixels(MED_ROW_SPACING);

    // Caption row.  Text inputs hold maxLen characters, so pass one less
    // than the field size to leave room for the terminator.
    WidgetT *capRow = wgtHBox(root);
    capRow->spacing = wgtPixels(MED_ROW_SPACING);
    WidgetT *capLbl = wgtLabel(capRow, "Caption:");
    capLbl->minW    = wgtPixels(MED_LABEL_W);
    sMed.captionInput = wgtTextInput(capRow, DSGN_MAX_TEXT - 1);
    sMed.captionInput->weight   = WGT_WEIGHT_FILL;
    sMed.captionInput->onChange = onCaptionChange;

    // Name row
    WidgetT *namRow = wgtHBox(root);
    namRow->spacing = wgtPixels(MED_ROW_SPACING);
    WidgetT *namLbl = wgtLabel(namRow, "Name:");
    namLbl->minW    = wgtPixels(MED_LABEL_W);
    sMed.nameInput = wgtTextInput(namRow, DSGN_MAX_NAME - 1);
    sMed.nameInput->weight   = WGT_WEIGHT_FILL;
    sMed.nameInput->onChange = onNameChange;

    // Check row
    WidgetT *chkRow = wgtHBox(root);
    chkRow->spacing = wgtPixels(MED_CHECK_SPACING);
    sMed.checkedCb    = wgtCheckbox(chkRow, "Checked");
    sMed.radioCheckCb = wgtCheckbox(chkRow, "RadioCheck");
    sMed.enabledCb    = wgtCheckbox(chkRow, "Enabled");
    sMed.popupCb      = wgtCheckbox(chkRow, "Popup");
    wgtCheckboxSetChecked(sMed.enabledCb, true);

    // Listbox
    sMed.listBox = wgtListBox(root);
    sMed.listBox->weight   = WGT_WEIGHT_FILL;
    sMed.listBox->onChange = onListClick;

    // Arrow buttons
    WidgetT *arrowRow = wgtHBox(root);
    arrowRow->spacing = wgtPixels(MED_ROW_SPACING);

    WidgetT *btnOut = wgtButton(arrowRow, "<-");
    btnOut->onClick = onOutdent;
    btnOut->minW    = wgtPixels(MED_ARROW_BTN_W);

    WidgetT *btnIn = wgtButton(arrowRow, "->");
    btnIn->onClick = onIndent;
    btnIn->minW    = wgtPixels(MED_ARROW_BTN_W);

    WidgetT *btnUp = wgtButton(arrowRow, "Up");
    btnUp->onClick = onMoveUp;
    btnUp->minW    = wgtPixels(MED_ARROW_BTN_W);

    WidgetT *btnDn = wgtButton(arrowRow, "Dn");
    btnDn->onClick = onMoveDown;
    btnDn->minW    = wgtPixels(MED_ARROW_BTN_W);

    // Action buttons
    WidgetT *actRow = wgtHBox(root);
    actRow->spacing = wgtPixels(MED_ROW_SPACING);

    WidgetT *btnNext = wgtButton(actRow, "&Next");
    btnNext->onClick = onNext;

    WidgetT *btnIns = wgtButton(actRow, "&Insert");
    btnIns->onClick = onInsert;

    WidgetT *btnDel = wgtButton(actRow, "&Delete");
    btnDel->onClick = onDelete;

    // OK / Cancel
    WidgetT *okRow = wgtHBox(root);
    okRow->spacing = wgtPixels(MED_OK_SPACING);

    WidgetT *btnOk = wgtButton(okRow, "OK");
    btnOk->onClick = onOk;
    btnOk->minW    = wgtPixels(MED_OK_BTN_W);

    WidgetT *btnCancel = wgtButton(okRow, "Cancel");
    btnCancel->onClick = onCancel;
    btnCancel->minW    = wgtPixels(MED_OK_BTN_W);

    // Populate
    rebuildList();
    loadFields();
    wgtSetFocused(sMed.captionInput);

    dvxFitWindow(ctx, win);

    // Modal loop
    WindowT *prevModal = ctx->modalWindow;
    ctx->modalWindow   = win;

    while (!sMed.done && ctx->running) {
        dvxUpdate(ctx);
    }

    ctx->modalWindow = prevModal;
    dvxDestroyWindow(ctx, win);

    // Cleanup working copy
    arrfree(sMed.items);
    sMed.items = NULL;

    // Cleanup display buffers
    for (int32_t i = 0; i < (int32_t)arrlen(sLabelBufs); i++) {
        free(sLabelBufs[i]);
    }

    arrfree(sLabelBufs);
    arrfree(sLabels);
    sLabelBufs = NULL;
    sLabels    = NULL;

    return sMed.accepted;
}


static void onCancel(WidgetT *w) {
    (void)w;
    sMed.accepted = false;
    sMed.done     = true;
}


static void onCaptionChange(WidgetT *w) {
    (void)w;
    applyFields();
    rebuildList();
}


static void onDelete(WidgetT *w) {
    (void)w;
    int32_t count = (int32_t)arrlen(sMed.items);

    if (sMed.selectedIdx < 0 || sMed.selectedIdx >= count) {
        return;
    }

    applyFields();

    // Delete selected item and its children
    int32_t subEnd = findSubtreeEnd(sMed.selectedIdx);

    for (int32_t i = subEnd - 1; i >= sMed.selectedIdx; i--) {
        arrdel(sMed.items, i);
    }

    count = (int32_t)arrlen(sMed.items);

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

    rebuildList();
    loadFields();
    wgtSetFocused(sMed.captionInput);
}


static void onIndent(WidgetT *w) {
    (void)w;
    int32_t count = (int32_t)arrlen(sMed.items);
    int32_t idx   = sMed.selectedIdx;

    if (idx <= 0 || idx >= count) {
        return;
    }

    applyFields();

    // Can only indent if previous item is at same or higher level
    int32_t prevLevel = sMed.items[idx - 1].level;

    if (sMed.items[idx].level > prevLevel) {
        return; // already deeper than prev
    }

    if (sMed.items[idx].level >= MAX_MENU_LEVEL) {
        return;
    }

    // Indent this item and all its children
    int32_t subEnd = findSubtreeEnd(idx);

    for (int32_t i = idx; i < subEnd; i++) {
        sMed.items[i].level++;
    }

    rebuildList();
}


static void onInsert(WidgetT *w) {
    (void)w;
    int32_t count = (int32_t)arrlen(sMed.items);

    applyFields();

    DsgnMenuItemT mi;
    dsgnMenuItemInit(&mi);

    // Insert after the current item's subtree, at the same level
    int32_t insertAt;

    if (sMed.selectedIdx >= 0 && sMed.selectedIdx < count) {
        mi.level = sMed.items[sMed.selectedIdx].level;
        insertAt = findSubtreeEnd(sMed.selectedIdx);
    } else {
        insertAt = count;
    }

    arrins(sMed.items, insertAt, mi);
    sMed.selectedIdx = insertAt;
    rebuildList();
    loadFields();
    wgtSetFocused(sMed.captionInput);
}


static void onListClick(WidgetT *w) {
    int32_t prev = sMed.selectedIdx;
    sMed.selectedIdx = wgtListBoxGetSelected(w);

    if (prev != sMed.selectedIdx && prev >= 0 && prev < (int32_t)arrlen(sMed.items)) {
        // Save fields from previously selected item
        int32_t saveSel = sMed.selectedIdx;
        sMed.selectedIdx = prev;
        applyFields();
        sMed.selectedIdx = saveSel;
    }

    loadFields();
}


static void onMoveDown(WidgetT *w) {
    (void)w;
    int32_t count = (int32_t)arrlen(sMed.items);
    int32_t idx   = sMed.selectedIdx;

    if (idx < 0 || idx >= count) {
        return;
    }

    applyFields();

    int32_t subEnd = findSubtreeEnd(idx);

    if (subEnd >= count) {
        return; // already at bottom
    }

    // The item/subtree after us
    int32_t nextEnd = findSubtreeEnd(subEnd);

    // Rotate: move the next subtree before our subtree
    // Simple approach: extract our subtree, delete it, insert after next subtree
    int32_t subSize = subEnd - idx;
    DsgnMenuItemT *tmp = (DsgnMenuItemT *)malloc(subSize * sizeof(DsgnMenuItemT));

    if (!tmp) {
        return;
    }

    memcpy(tmp, &sMed.items[idx], subSize * sizeof(DsgnMenuItemT));

    // Delete our subtree
    for (int32_t i = subEnd - 1; i >= idx; i--) {
        arrdel(sMed.items, i);
    }

    // Insert after what was the next subtree (now shifted)
    int32_t insertAt = nextEnd - subSize;

    for (int32_t i = 0; i < subSize; i++) {
        arrins(sMed.items, insertAt + i, tmp[i]);
    }

    free(tmp);
    sMed.selectedIdx = insertAt;
    rebuildList();
}


static void onMoveUp(WidgetT *w) {
    (void)w;
    int32_t idx = sMed.selectedIdx;

    if (idx <= 0) {
        return;
    }

    applyFields();

    // Find the start of the previous subtree at the same or lower level
    int32_t prevIdx = idx - 1;

    while (prevIdx > 0 && sMed.items[prevIdx].level > sMed.items[idx].level) {
        prevIdx--;
    }

    // No previous sibling at the same level (e.g. selected item is the first
    // child of its parent) -- Move Up would insert the subtree before its
    // parent and corrupt nesting, so do nothing.
    if (sMed.items[prevIdx].level != sMed.items[idx].level) {
        return;
    }

    // Move our subtree before the previous item's subtree
    int32_t subEnd  = findSubtreeEnd(idx);
    int32_t subSize = subEnd - idx;
    DsgnMenuItemT *tmp = (DsgnMenuItemT *)malloc(subSize * sizeof(DsgnMenuItemT));

    if (!tmp) {
        return;
    }

    memcpy(tmp, &sMed.items[idx], subSize * sizeof(DsgnMenuItemT));

    // Delete our subtree
    for (int32_t i = subEnd - 1; i >= idx; i--) {
        arrdel(sMed.items, i);
    }

    // Insert at prevIdx
    for (int32_t i = 0; i < subSize; i++) {
        arrins(sMed.items, prevIdx + i, tmp[i]);
    }

    free(tmp);
    sMed.selectedIdx = prevIdx;
    rebuildList();
}


static void onNameChange(WidgetT *w) {
    (void)w;
    sMed.nameAutoGen = false; // user took over
    applyFields();
    rebuildList();
}


static void onNext(WidgetT *w) {
    (void)w;
    int32_t count = (int32_t)arrlen(sMed.items);

    applyFields();

    if (sMed.selectedIdx < count - 1) {
        sMed.selectedIdx++;
    } else {
        // Append new item
        DsgnMenuItemT mi;
        dsgnMenuItemInit(&mi);

        if (count > 0) {
            mi.level = sMed.items[count - 1].level;
        }

        arrput(sMed.items, mi);
        sMed.selectedIdx = (int32_t)arrlen(sMed.items) - 1;
    }

    rebuildList();
    loadFields();
    wgtSetFocused(sMed.captionInput);
}


static void onOk(WidgetT *w) {
    (void)w;
    applyFields();

    // Strip blank items (no caption and no name)
    for (int32_t i = (int32_t)arrlen(sMed.items) - 1; i >= 0; i--) {
        if (sMed.items[i].caption[0] == '\0' && sMed.items[i].name[0] == '\0') {
            arrdel(sMed.items, i);
        }
    }

    // Validate: every name is an identifier, unique among the menu items,
    // and not the name of the form or one of its controls.  A name with
    // spaces would be written as "Begin Menu File Menu" and mis-parsed.
    int32_t     count  = (int32_t)arrlen(sMed.items);
    int32_t     badIdx = -1;
    const char *reason = NULL;
    char        msg[MED_MSG_BUF];

    for (int32_t i = 0; i < count && badIdx < 0; i++) {
        const char *name = sMed.items[i].name;

        if (name[0] == '\0') {
            badIdx = i;
            reason = "All menu items must have a Name.";
        } else if (!basIsValidIdent(name)) {
            badIdx = i;
            snprintf(msg, sizeof(msg), "Menu name must be an identifier (letters, digits, underscores): %s", name);
            reason = msg;
        } else if (dsgnNameInUse(sMed.form, name, NULL, false)) {
            badIdx = i;
            snprintf(msg, sizeof(msg), "Menu name is already used by the form or a control: %s", name);
            reason = msg;
        }

        for (int32_t j = i + 1; j < count && badIdx < 0; j++) {
            if (strcasecmp(name, sMed.items[j].name) == 0) {
                badIdx = j;
                snprintf(msg, sizeof(msg), "Duplicate menu name: %s", name);
                reason = msg;
            }
        }
    }

    if (badIdx >= 0) {
        dvxErrorBox(sMed.ctx, MED_DLG_TITLE, reason);
        sMed.selectedIdx = badIdx;
        rebuildList();
        loadFields();
        wgtSetFocused(sMed.captionInput);
        return;
    }

    // Copy working items back to form
    arrfree(sMed.form->menuItems);
    sMed.form->menuItems = NULL;

    for (int32_t i = 0; i < count; i++) {
        arrput(sMed.form->menuItems, sMed.items[i]);
    }

    sMed.accepted = true;
    sMed.done     = true;
}


static void onOutdent(WidgetT *w) {
    (void)w;
    int32_t count = (int32_t)arrlen(sMed.items);
    int32_t idx   = sMed.selectedIdx;

    if (idx < 0 || idx >= count) {
        return;
    }

    if (sMed.items[idx].level <= 0) {
        return;
    }

    applyFields();

    int32_t subEnd = findSubtreeEnd(idx);

    for (int32_t i = idx; i < subEnd; i++) {
        sMed.items[i].level--;
    }

    rebuildList();
}


static void rebuildList(void) {
    int32_t count = (int32_t)arrlen(sMed.items);

    // The listBox stores pointers, not copies -- so we must keep these buffers
    // alive until the next rebuild or dialog destroy. Free prior pass first.
    for (int32_t i = 0; i < (int32_t)arrlen(sLabelBufs); i++) {
        free(sLabelBufs[i]);
    }

    arrsetlen(sLabelBufs, 0);
    arrsetlen(sLabels,    0);

    for (int32_t i = 0; i < count; i++) {
        char    buf[MED_LABEL_BUF];
        int32_t pos = 0;

        buf[0] = '\0';

        for (int32_t lv = 0; lv < sMed.items[i].level; lv++) {
            pos += snprintf(buf + pos, sizeof(buf) - pos, "%s", ARROW_STR);
        }

        const char *cap = sMed.items[i].caption;

        if (cap[0] == '-') {
            snprintf(buf + pos, sizeof(buf) - pos, "--------");
        } else if (cap[0]) {
            snprintf(buf + pos, sizeof(buf) - pos, "%s", cap);
        } else {
            snprintf(buf + pos, sizeof(buf) - pos, "(%s)", sMed.items[i].name[0] ? sMed.items[i].name : "untitled");
        }

        arrput(sLabelBufs, strdup(buf));
    }

    for (int32_t i = 0; i < count; i++) {
        arrput(sLabels, (const char *)sLabelBufs[i]);
    }

    wgtListBoxSetItems(sMed.listBox, sLabels, count);
    wgtListBoxSetSelected(sMed.listBox, sMed.selectedIdx);
}
