// 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
// widgetLayout.c  -- Layout engine (measure + arrange)
//
// Implements a two-pass layout algorithm inspired by CSS flexbox:
//   Pass 1 (Measure): bottom-up calculation of each widget's minimum
//     size. Leaf widgets report their intrinsic size (text width, etc.)
//     and containers sum their children's minimums plus padding/spacing.
//   Pass 2 (Arrange): top-down assignment of actual positions and sizes.
//     The root gets the full available area, then each container divides
//     its space among children based on their minimums and weight values.
//
// Why two passes instead of one:
//   Containers can't assign sizes until they know all children's minimums
//   (to calculate extra space for weight distribution). And children can't
//   know their minimums until their subtrees are measured. So measure must
//   be bottom-up before arrange can be top-down.
//
// Why flexbox-like rather than constraint-based:
//   Flexbox maps naturally to the VBox/HBox container model  -- it's simple
//   to understand, implement, and debug. Constraint-based layout (like
//   Cassowary) would add significant complexity for little benefit in a
//   DOS GUI where most layouts are linear stacks of widgets.
//
// Size hints use a tagged integer encoding (see dvxWidget.h):
//   High 2 bits encode the unit type (pixels, characters, percent),
//   low 30 bits encode the numeric value. This packs three distinct
//   unit types into a single int32_t without needing a separate struct.
//   The wgtResolveSize() function decodes the tag and converts to pixels.

#include "dvxWgtP.h"


// Resolves the layout metrics of a box container: padding and gap come
// from the widget's tagged sizes (falling back to DEFAULT_PADDING /
// DEFAULT_SPACING when unset), then a class-supplied getLayoutMetrics
// may override them and add extraTop / borderW (Frame, TabPage).
// Shared by the measure pass, the arrange pass, and
// widgetFrameBorderWidth so the three never disagree.

void widgetBoxMetrics(const WidgetT *w, const BitmapFontT *font, int32_t *pad, int32_t *gap, int32_t *extraTop, int32_t *borderW) {
    int32_t charW = font ? font->charWidth : 0;

    *pad      = wgtResolveSize(w->padding, 0, charW);
    *gap      = wgtResolveSize(w->spacing, 0, charW);
    *extraTop = 0;
    *borderW  = 0;

    if (*pad == 0) {
        *pad = DEFAULT_PADDING;
    }

    if (*gap == 0) {
        *gap = DEFAULT_SPACING;
    }

    if (wclsHas(w, WGT_METHOD_GET_LAYOUT_METRICS)) {
        wclsGetLayoutMetrics(w, font, pad, gap, extraTop, borderW);
    }
}


// Measure pass for box containers (VBox, HBox, RadioGroup, StatusBar,
// Toolbar, Frame, TabPage). Recursively measures all visible children,
// then computes this container's minimum size as:
//   main axis:  sum of children minimums + gaps + padding
//   cross axis: max of children minimums + padding
//
// "Main axis" is vertical for VBox, horizontal for HBox. The horiz
// flag flips the calculation so VBox and HBox share the same code.

void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
    bool    horiz         = widgetIsHorizContainer(w->type);
    int32_t mainSize      = 0;
    int32_t crossSize     = 0;
    int32_t count         = 0;
    int32_t pad;
    int32_t gap;
    int32_t frameExtraTop;
    int32_t metricBorderW;

    widgetBoxMetrics(w, font, &pad, &gap, &frameExtraTop, &metricBorderW);

    for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
        if (!c->visible) {
            continue;
        }

        widgetCalcMinSizeTree(c, font);

        if (horiz) {
            mainSize += c->calcMinW;
            crossSize = DVX_MAX(crossSize, c->calcMinH);
        } else {
            mainSize += c->calcMinH;
            crossSize = DVX_MAX(crossSize, c->calcMinW);
        }

        count++;
    }

    // Add spacing between children
    if (count > 1) {
        mainSize += gap * (count - 1);
    }

    // Add padding
    mainSize  += pad * 2;
    crossSize += pad * 2;

    if (horiz) {
        w->calcMinW = mainSize;
        w->calcMinH = crossSize + frameExtraTop;
    } else {
        w->calcMinW = crossSize;
        w->calcMinH = mainSize + frameExtraTop;
    }

    // Border (Frame and similar containers with getLayoutMetrics; 0 otherwise)
    w->calcMinW += metricBorderW * 2;
    w->calcMinH += metricBorderW * 2;
}


// Top-level measure dispatcher. Routes to the appropriate measure
// function based on widget type:
//   - Box containers use the generic widgetCalcMinSizeBox()
//   - Widgets with custom layout (TabControl, TreeView, ScrollPane,
//     Splitter) provide their own calcMinSize via the vtable
//   - Widgets without calcMinSize get 0x0 (they rely on size hints)
//
// After the type-specific measure, explicit minW/minH size hints
// are applied as a floor. This allows "this widget should be at
// least N pixels/chars wide" without changing the widget's natural
// size calculation. Hints only increase the minimum, never shrink it.

void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
    if (wclsHas(w, WGT_METHOD_CALC_MIN_SIZE)) {
        wclsCalcMinSize(w, font);
    } else {
        w->calcMinW = 0;
        w->calcMinH = 0;
    }

    // Size hints act as a floor: if the app specified wgtPixels(200) as
    // minW, the widget will be at least 200px wide even if its intrinsic
    // content is smaller. This is resolved at measure time (not arrange)
    // so parent containers see the correct minimum when distributing space.
    if (w->minW) {
        int32_t hintW = wgtResolveSize(w->minW, 0, font->charWidth);

        if (hintW > w->calcMinW) {
            w->calcMinW = hintW;
        }
    }

    if (w->minH) {
        int32_t hintH = wgtResolveSize(w->minH, 0, font->charWidth);

        if (hintH > w->calcMinH) {
            w->calcMinH = hintH;
        }
    }
}


// Arrange pass for box containers. This is the core of the flexbox-
// like layout algorithm, working in two sub-passes:
//
// Sub-pass 1: Sum all children's minimum sizes and total weight.
//   "Extra space" = available main-axis size - total minimum sizes.
//
// Sub-pass 2: Assign each child's position and size.
//   - Each child gets at least its minimum size.
//   - Extra space is distributed proportionally by weight (like CSS
//     flex-grow). A child with weight=100 gets twice as much extra
//     as one with weight=50.
//   - If total weight is 0 (all children are fixed-size), extra space
//     is distributed according to the container's alignment: start
//     (default), center, or end.
//   - maxW/maxH constraints cap a child's final size.
//   - Cross-axis size is the full available cross dimension (minus
//     any maxW/maxH constraint on that axis).
//
// After positioning each child, widgetLayoutChildren() recurses to
// lay out the child's own subtree.

void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
    bool    horiz = widgetIsHorizContainer(w->type);
    int32_t pad;
    int32_t gap;
    int32_t frameExtraTop;
    int32_t fb;

    widgetBoxMetrics(w, font, &pad, &gap, &frameExtraTop, &fb);

    int32_t innerX = w->x + pad + fb;
    int32_t innerY = w->y + pad + fb + frameExtraTop;
    int32_t innerW = w->w - pad * 2 - fb * 2;
    int32_t innerH = w->h - pad * 2 - fb * 2 - frameExtraTop;

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

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

    int32_t count = widgetCountVisibleChildren(w);

    if (count == 0) {
        return;
    }

    int32_t totalGap   = gap * (count - 1);
    int32_t availMain  = horiz ? (innerW - totalGap) : (innerH - totalGap);
    int32_t availCross = horiz ? innerH : innerW;

    // Sub-pass 1: accumulate children's minimum sizes and weights.
    // totalMin is the space needed if every child gets exactly its minimum.
    // totalWeight determines how to split any leftover space.
    int32_t totalMin    = 0;
    int32_t totalWeight = 0;

    for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
        if (!c->visible) {
            continue;
        }

        int32_t cmin = horiz ? c->calcMinW : c->calcMinH;
        totalMin += cmin;
        totalWeight += c->weight;
    }

    int32_t extraSpace = availMain - totalMin;

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

    // Alignment only applies when no children have weight (all fixed-size).
    // When weights exist, the extra space is consumed by weighted children
    // and alignment has no effect.
    int32_t alignOffset = 0;

    if (totalWeight == 0 && extraSpace > 0) {
        if (w->align == AlignCenterE) {
            alignOffset = extraSpace / 2;
        } else if (w->align == AlignEndE) {
            alignOffset = extraSpace;
        }
    }

    // Second pass: assign positions and sizes.  weightSeen/extraGiven track
    // the running weight and pixel totals so the last weighted child gets
    // whatever integer division left over instead of leaving it unused.
    int32_t pos        = (horiz ? innerX : innerY) + alignOffset;
    int32_t weightSeen = 0;
    int32_t extraGiven = 0;

    for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
        if (!c->visible) {
            continue;
        }

        int32_t cmin     = horiz ? c->calcMinW : c->calcMinH;
        int32_t mainSize = cmin;

        // Distribute extra space by weight
        if (totalWeight > 0 && c->weight > 0 && extraSpace > 0) {
            int32_t share;

            weightSeen += c->weight;

            if (weightSeen >= totalWeight) {
                share = extraSpace - extraGiven;
            } else {
                share = (extraSpace * c->weight) / totalWeight;
            }

            extraGiven += share;
            mainSize   += share;
        }

        // Apply max size constraint
        if (horiz && c->maxW) {
            int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth);

            if (mainSize > maxPx) {
                mainSize = maxPx;
            }
        } else if (!horiz && c->maxH) {
            int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth);

            if (mainSize > maxPx) {
                mainSize = maxPx;
            }
        }

        // Assign geometry
        int32_t crossSize = availCross;

        // Apply preferred size as cross-axis cap (e.g. Width in a VBox).
        // When set, the widget won't stretch beyond this on the cross axis.
        if (horiz && c->prefH) {
            int32_t prefPx = wgtResolveSize(c->prefH, innerH, font->charWidth);

            if (crossSize > prefPx) {
                crossSize = prefPx;
            }
        } else if (!horiz && c->prefW) {
            int32_t prefPx = wgtResolveSize(c->prefW, innerW, font->charWidth);

            if (crossSize > prefPx) {
                crossSize = prefPx;
            }
        }

        // Apply max size on cross axis
        if (horiz && c->maxH) {
            int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth);

            if (crossSize > maxPx) {
                crossSize = maxPx;
            }
        } else if (!horiz && c->maxW) {
            int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth);

            if (crossSize > maxPx) {
                crossSize = maxPx;
            }
        }

        // Center on cross axis when child is constrained smaller
        int32_t crossOff = (crossSize < availCross) ? (availCross - crossSize) / 2 : 0;

        if (horiz) {
            c->x = pos;
            c->y = innerY + crossOff;
            c->w = mainSize;
            c->h = crossSize;
        } else {
            c->x = innerX + crossOff;
            c->y = pos;
            c->w = crossSize;
            c->h = mainSize;
        }

        pos += mainSize + gap;

        // Recurse into child containers
        widgetLayoutChildren(c, font);
    }
}


// Top-level layout dispatcher. Mirrors the measure dispatcher
// in widgetCalcMinSizeTree(): box containers use the generic
// algorithm, widgets with custom layout (TabControl, TreeView,
// ScrollPane, Splitter) use their vtable entry, and leaf widgets
// do nothing (they have no children to lay out).

void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font) {
    if (wclsHas(w, WGT_METHOD_LAYOUT)) {
        wclsLayout(w, font);
    }
}


// Public entry point: runs both passes on the entire widget tree.
// The root widget is positioned at (0,0) and given the full available
// area, then the arrange pass distributes space to its children.
//
// The window paths (widgetManageScrollbars, widgetOnPaint) do not use
// this: they measure once and arrange at the scrolled root geometry
// themselves.  This entry point serves callers that need a complete
// layout on demand (e.g. dvxFitWindow).

void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font) {
    if (!root) {
        return;
    }

    // Measure pass
    widgetCalcMinSizeTree(root, font);

    // Layout pass
    root->x = 0;
    root->y = 0;
    root->w = availW;
    root->h = availH;

    widgetLayoutChildren(root, font);
}


// Decodes a tagged size value into an actual pixel count.
//
// The tagged integer format uses the high 2 bits as a type tag:
//   00 = pixels (value is used directly)
//   01 = characters (value * charWidth, for text-relative sizing)
//   10 = percent (value% of parentSize, for responsive layouts)
//
// This encoding allows a single int32_t field to represent any of
// three unit types without needing a separate struct or enum.
// The tradeoff is that pixel values are limited to 30 bits (~1 billion),
// which is far more than any supported display resolution.
//
// A value of 0 means "auto" (use intrinsic/calculated size)  -- the
// caller checks for 0 before calling this function.

int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth) {
    if (taggedSize == 0) {
        return 0;
    }

    uint32_t sizeType = (uint32_t)taggedSize & WGT_SIZE_TYPE_MASK;
    int32_t  value    = taggedSize & WGT_SIZE_VAL_MASK;

    switch (sizeType) {
        case WGT_SIZE_PIXELS:
            return value;

        case WGT_SIZE_CHARS:
            return value * charWidth;

        case WGT_SIZE_PERCENT:
            return (parentSize * value) / 100;

        default:
            return value;
    }
}
