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

// listHelp.c -- Shared list/dropdown helper functions
//
// Implements dropdown arrow drawing, item length scanning, keyboard
// navigation, and popup list painting used by ListBox, Dropdown,
// ComboBox, ListView, and TreeView widgets.

#include "listHelp.h"
#include "../texthelp/textHelp.h"
#include "stb_ds_wrap.h"

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

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

char  **widgetAdoptOwnedStrings(char **owned, const char **src, int32_t count);
void    widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color);
void    widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH);
int32_t widgetMaxItemLen(const char **items, int32_t count);
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize);
void    widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos);
bool    widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY, int32_t popW, int32_t popH, int32_t itemCount, int32_t visibleItems, int32_t *scrollPos);
int32_t widgetTypeAheadSearch(char ch, const char **items, int32_t itemCount, int32_t currentIdx);


// Rebuild an internally-owned stb_ds string array as a deep copy of an
// external source array. Shared by ListBox (ownedItems) and ListView
// (ownedCells) so the free-old/reset/strdup adoption sequence lives in one
// place. Returns the (possibly reallocated) owned array.
char **widgetAdoptOwnedStrings(char **owned, const char **src, int32_t count) {
    for (int32_t i = 0; i < (int32_t)arrlen(owned); i++) {
        free(owned[i]);
    }

    arrsetlen(owned, 0);

    for (int32_t i = 0; i < count; i++) {
        const char *s = src ? src[i] : NULL;

        arrput(owned, strdup(s ? s : ""));
    }

    return owned;
}


// Draws a small downward-pointing filled triangle (7, 5, 3, 1 pixels
// wide across 4 rows) centered at the given position. Used by both
// Dropdown and ComboBox for the drop button arrow glyph.
void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color) {
    for (int32_t i = 0; i < 4; i++) {
        drawHLine(d, ops, centerX - 3 + i, centerY + i, 7 - i * 2, color);
    }
}


// Calculates the screen rectangle for a dropdown/combobox popup list.
// Shared between Dropdown and ComboBox since they have identical
// popup positioning logic.
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) {
    int32_t visibleItems = itemCount;

    if (visibleItems > DROPDOWN_MAX_VISIBLE) {
        visibleItems = DROPDOWN_MAX_VISIBLE;
    }

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

    *popX = w->x;
    *popW = w->w;
    *popH = visibleItems * font->charHeight + 4;

    // Add scrollbar width when list exceeds visible area
    if (itemCount > DROPDOWN_MAX_VISIBLE) {
        *popW += POPUP_SCROLLBAR_W;
    }

    if (w->y + w->h + *popH <= contentH) {
        *popY = w->y + w->h;
    } else {
        *popY = w->y - *popH;

        if (*popY < 0) {
            *popY = 0;
        }
    }
}


// Scans an array of string items and returns the maximum strlen.
// Shared by ListBox, Dropdown, and ComboBox to cache the widest
// item length for calcMinSize without duplicating the loop.
int32_t widgetMaxItemLen(const char **items, int32_t count) {
    int32_t maxLen = 0;

    for (int32_t i = 0; i < count; i++) {
        int32_t slen = (int32_t)strlen(items[i]);

        if (slen > maxLen) {
            maxLen = slen;
        }
    }

    return maxLen;
}


// Shared keyboard navigation for list-like widgets (ListBox, Dropdown,
// ListView, etc.). Encapsulates the Up/Down/Home/End/PgUp/PgDn logic
// so each widget doesn't have to reimplement index clamping.
//
// Key values use the 0x100 flag to mark extended scan codes (arrow
// keys, Home, End, etc.) -- this is the DVX convention for passing
// scan codes through the same int32_t channel as ASCII values.
//
// Returns -1 for unrecognized keys so callers can check whether the
// key was consumed.
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) {
    // An empty list has no valid index; leave the selection unchanged so
    // callers treat it as 'no navigation' instead of selecting index 0.
    if (count <= 0) {
        return current;
    }

    if (key == KEY_DOWN) {
        // Down arrow
        if (current < count - 1) {
            return current + 1;
        }

        return current < 0 ? 0 : current;
    }

    if (key == KEY_UP) {
        // Up arrow
        if (current > 0) {
            return current - 1;
        }

        return current < 0 ? 0 : current;
    }

    if (key == KEY_HOME) {
        // Home
        return 0;
    }

    if (key == KEY_END) {
        // End
        return count - 1;
    }

    if (key == KEY_PGDN) {
        // Page Down
        int32_t n = current + pageSize;
        return n >= count ? count - 1 : n;
    }

    if (key == KEY_PGUP) {
        // Page Up
        int32_t n = current - pageSize;
        return n < 0 ? 0 : n;
    }

    return -1;
}


// Shared popup list painting for Dropdown and ComboBox.
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos) {
    bool hasScrollbar    = (itemCount > DROPDOWN_MAX_VISIBLE);
    int32_t visibleItems = popH / font->charHeight;
    int32_t listW        = hasScrollbar ? popW - POPUP_SCROLLBAR_W : popW;

    // Draw popup border (covers item area only, not scrollbar)
    BevelStyleT bevel;
    bevel.highlight = colors->windowHighlight;
    bevel.shadow    = colors->windowShadow;
    bevel.face      = colors->contentBg;
    bevel.width     = 2;
    drawBevel(d, ops, popX, popY, listW, popH, &bevel);

    // Draw items
    int32_t textX = popX + TEXT_INPUT_PAD;
    int32_t textY = popY + 2;
    int32_t textW = listW - TEXT_INPUT_PAD * 2 - 4;

    for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) {
        int32_t  idx = scrollPos + i;
        int32_t  iy  = textY + i * font->charHeight;
        uint32_t ifg = colors->contentFg;
        uint32_t ibg = colors->contentBg;

        if (idx == hoverIdx) {
            ifg = colors->menuHighlightFg;
            ibg = colors->menuHighlightBg;
            rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
        }

        drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false);
    }

    // Draw scrollbar
    if (hasScrollbar) {
        wmDrawVScrollbarAt(d, ops, colors, popX + listW, popY, popH, scrollPos, visibleItems, itemCount);
    }
}


bool widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY, int32_t popW, int32_t popH, int32_t itemCount, int32_t visibleItems, int32_t *scrollPos) {
    if (itemCount <= DROPDOWN_MAX_VISIBLE) {
        return false;
    }

    int32_t listW = popW - POPUP_SCROLLBAR_W;
    int32_t sbX   = popX + listW;

    if (x < sbX || x >= sbX + POPUP_SCROLLBAR_W) {
        return false;
    }

    int32_t maxScroll = itemCount - visibleItems;
    int32_t relY      = y - popY;

    if (relY < POPUP_SCROLLBAR_W) {
        // Up arrow
        if (*scrollPos > 0) {
            (*scrollPos)--;
        }
    } else if (relY >= popH - POPUP_SCROLLBAR_W) {
        // Down arrow
        if (*scrollPos < maxScroll) {
            (*scrollPos)++;
        }
    } else {
        // Trough — page up/down based on which side of thumb
        int32_t trackLen  = popH - POPUP_SCROLLBAR_W * 2;
        int32_t thumbSize = (int32_t)(((int64_t)visibleItems * trackLen) / itemCount);

        if (thumbSize < POPUP_SCROLLBAR_W) {
            thumbSize = POPUP_SCROLLBAR_W;
        }

        int32_t thumbPos = 0;

        if (maxScroll > 0) {
            thumbPos = (int32_t)(((int64_t)(*scrollPos) * (trackLen - thumbSize)) / maxScroll);
        }

        int32_t clickInTrack = relY - POPUP_SCROLLBAR_W;

        if (clickInTrack < thumbPos) {
            // Page up
            *scrollPos -= visibleItems;

            if (*scrollPos < 0) {
                *scrollPos = 0;
            }
        } else if (clickInTrack >= thumbPos + thumbSize) {
            // Page down
            *scrollPos += visibleItems;

            if (*scrollPos > maxScroll) {
                *scrollPos = maxScroll;
            }
        }
    }

    return true;
}


int32_t widgetTypeAheadSearch(char ch, const char **items, int32_t itemCount, int32_t currentIdx) {
    if (itemCount <= 0 || !items) {
        return -1;
    }

    char upper = (ch >= 'a' && ch <= 'z') ? ch - 32 : ch;

    // Search forward from currentIdx+1, wrapping around
    for (int32_t i = 1; i <= itemCount; i++) {
        int32_t idx = (currentIdx + i) % itemCount;
        char first  = items[idx][0];

        if (first >= 'a' && first <= 'z') {
            first -= 32;
        }

        if (first == upper) {
            return idx;
        }
    }

    return -1;
}
