// 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
// widgetSplitter.c  -- Splitter (draggable divider between two panes)
//
// A container that divides its area between exactly two child widgets
// with a draggable divider bar between them. The divider position
// (dividerPos) is stored as a pixel offset from the leading edge
// (left for vertical, top for horizontal).
//
// Architecture: the splitter is a special container (has its own
// layout function) that manually positions its two children and the
// divider bar. It does NOT use the generic box layout. Children are
// clipped to their respective panes during painting to prevent
// overflow into the other pane.
//
// The divider bar (SPLITTER_BAR_W = 5px) is drawn as a raised bevel
// with a "gripper" pattern -- small embossed 2x2 bumps arranged
// in a line centered on the bar. This provides visual feedback that
// the bar is draggable, following the Win3.1/Motif convention.
//
// Drag state: divider dragging stores the clicked splitter widget
// and the mouse offset within the bar in globals (sDragSplitter,
// sDragSplitStart). The event loop handles mouse-move during drag
// by computing the new divider position from mouse coordinates and
// calling widgetSplitterClampPos to enforce minimum pane sizes.
//
// Minimum pane sizes come from children's calcMinW/H, with a floor
// of SPLITTER_MIN_PANE (20px) to prevent panes from collapsing to
// nothing. The clamp also ensures the divider can't be dragged past
// where the second pane would violate its minimum.

#include "dvxWgtP.h"

#define SPLITTER_BAR_W       5
#define SPLITTER_MIN_PANE    20

static int32_t sTypeId = -1;

typedef struct {
    int32_t dividerPos;
    bool    vertical;
    int32_t dragStart;
} SplitterDataT;


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

static WidgetT  *spFirstChild(WidgetT *w);
static WidgetT  *spSecondChild(WidgetT *w);
void             wgtRegister(void);
WidgetT         *wgtSplitter(WidgetT *parent, bool vertical);
int32_t          wgtSplitterGetPos(const WidgetT *w);
void             wgtSplitterSetPos(WidgetT *w, int32_t pos);
void             widgetSplitterCalcMinSize(WidgetT *w, const BitmapFontT *font);
void             widgetSplitterClampPos(WidgetT *w, int32_t *pos);
int32_t          widgetSplitterGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy);
void             widgetSplitterLayout(WidgetT *w, const BitmapFontT *font);
static void      widgetSplitterOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mouseX, int32_t mouseY);
void             widgetSplitterOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
void             widgetSplitterPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);


// These helpers skip invisible children so the splitter works
// correctly even if one pane is hidden. The splitter only cares
// about the first two visible children; any additional children
// are ignored (though this shouldn't happen in practice).
static WidgetT *spFirstChild(WidgetT *w) {
    for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
        if (c->visible) {
            return c;
        }
    }

    return NULL;
}


static WidgetT *spSecondChild(WidgetT *w) {
    int32_t n = 0;

    for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
        if (c->visible) {
            n++;

            if (n == 2) {
                return c;
            }
        }
    }

    return NULL;
}


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

static const WidgetClassT sClassSplitter = {
    .version = WGT_CLASS_VERSION,
    .flags   = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
    .handlers = {
        [WGT_METHOD_PAINT]            = (void *)widgetSplitterPaint,
        [WGT_METHOD_CALC_MIN_SIZE]    = (void *)widgetSplitterCalcMinSize,
        [WGT_METHOD_LAYOUT]           = (void *)widgetSplitterLayout,
        [WGT_METHOD_ON_MOUSE]         = (void *)widgetSplitterOnMouse,
        [WGT_METHOD_GET_CURSOR_SHAPE] = (void *)widgetSplitterGetCursorShape,
        [WGT_METHOD_ON_DRAG_UPDATE]   = (void *)widgetSplitterOnDragUpdate,
    }
};


static const struct {
    WidgetT *(*create)(WidgetT *parent, bool vertical);
    void     (*setPos)(WidgetT *w, int32_t pos);
    int32_t  (*getPos)(const WidgetT *w);
} sApi = {
    .create = wgtSplitter,
    .setPos = wgtSplitterSetPos,
    .getPos = wgtSplitterGetPos
};

static const WgtPropDescT sProps[] = {
    { "Position", WGT_IFACE_INT, (void *)wgtSplitterGetPos, (void *)wgtSplitterSetPos, NULL }
};

static const WgtIfaceT sIface = {
    .basName      = "Splitter",
    .props        = sProps,
    .propCount    = 1,
    .methods      = NULL,
    .methodCount  = 0,
    .events       = NULL,
    .eventCount   = 0,
    .createSig    = WGT_CREATE_PARENT_BOOL,
    .createArgs   = { 1 },
    .isContainer  = true
};

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


WidgetT *wgtSplitter(WidgetT *parent, bool vertical) {
    WidgetT *w = widgetAlloc(parent, sTypeId);

    if (!w) {
        return NULL;
    }

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

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

    d->vertical   = vertical;
    d->dividerPos = 0;
    w->data       = d;
    w->weight     = WGT_WEIGHT_FILL;
    return w;
}


int32_t wgtSplitterGetPos(const WidgetT *w) {
    SplitterDataT *d = (SplitterDataT *)w->data;
    return d->dividerPos;
}


void wgtSplitterSetPos(WidgetT *w, int32_t pos) {
    SplitterDataT *d = (SplitterDataT *)w->data;
    d->dividerPos = pos;
    wgtInvalidate(w);
}


void widgetSplitterCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    // Recursively measure children
    for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
        widgetCalcMinSizeTree(c, font);
    }

    WidgetT *c1 = spFirstChild(w);
    WidgetT *c2 = spSecondChild(w);
    int32_t  m1w = c1 ? c1->calcMinW : 0;
    int32_t  m1h = c1 ? c1->calcMinH : 0;
    int32_t  m2w = c2 ? c2->calcMinW : 0;
    int32_t  m2h = c2 ? c2->calcMinH : 0;

    SplitterDataT *d = (SplitterDataT *)w->data;

    if (d->vertical) {
        w->calcMinW = m1w + m2w + SPLITTER_BAR_W;
        w->calcMinH = DVX_MAX(m1h, m2h);
    } else {
        w->calcMinW = DVX_MAX(m1w, m2w);
        w->calcMinH = m1h + m2h + SPLITTER_BAR_W;
    }
}


void widgetSplitterClampPos(WidgetT *w, int32_t *pos) {
    SplitterDataT *d = (SplitterDataT *)w->data;
    WidgetT       *c1   = spFirstChild(w);
    WidgetT       *c2   = spSecondChild(w);
    bool           vert = d->vertical;
    int32_t        totalSize = vert ? w->w : w->h;

    int32_t minFirst  = c1 ? (vert ? c1->calcMinW : c1->calcMinH) : SPLITTER_MIN_PANE;
    int32_t minSecond = c2 ? (vert ? c2->calcMinW : c2->calcMinH) : SPLITTER_MIN_PANE;

    if (minFirst < SPLITTER_MIN_PANE) {
        minFirst = SPLITTER_MIN_PANE;
    }

    if (minSecond < SPLITTER_MIN_PANE) {
        minSecond = SPLITTER_MIN_PANE;
    }

    int32_t maxPos = totalSize - SPLITTER_BAR_W - minSecond;

    if (maxPos < minFirst) {
        maxPos = minFirst;
    }

    *pos = clampInt(*pos, minFirst, maxPos);
}


int32_t widgetSplitterGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy) {
    SplitterDataT *d = (SplitterDataT *)w->data;

    // During active drag, always show resize cursor
    if (sDragWidget == w) {
        return d->vertical ? CURSOR_RESIZE_H : CURSOR_RESIZE_V;
    }

    int32_t pos = d->dividerPos;

    if (d->vertical) {
        int32_t barX = w->x + pos;

        if (vx >= barX && vx < barX + SPLITTER_BAR_W) {
            return CURSOR_RESIZE_H;
        }
    } else {
        int32_t barY = w->y + pos;

        if (vy >= barY && vy < barY + SPLITTER_BAR_W) {
            return CURSOR_RESIZE_V;
        }
    }

    // Not on our divider -- check children (handles nested splitters
    // and other widgets with cursor shapes like ListView column borders)
    for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
        WidgetT *child = widgetHitTest(c, vx, vy);

        if (child) {
            int32_t shape = wclsGetCursorShape(child, vx, vy);

            if (shape > 0) {
                return shape;
            }
        }
    }

    return 0;
}


// Layout assigns each child its full pane area, then recurses into
// child layouts. The first child gets [0, dividerPos) and the second
// gets [dividerPos + SPLITTER_BAR_W, end). The divider position is
// clamped before use to respect minimum pane sizes, even if the
// user hasn't dragged yet (dividerPos=0 from construction gets
// clamped to minFirst).
void widgetSplitterLayout(WidgetT *w, const BitmapFontT *font) {
    SplitterDataT *d   = (SplitterDataT *)w->data;
    WidgetT       *c1  = spFirstChild(w);
    WidgetT       *c2  = spSecondChild(w);
    int32_t        pos = d->dividerPos;

    widgetSplitterClampPos(w, &pos);
    d->dividerPos = pos;

    if (d->vertical) {
        // Left pane
        if (c1) {
            c1->x = w->x;
            c1->y = w->y;
            c1->w = pos;
            c1->h = w->h;
            widgetLayoutChildren(c1, font);
        }

        // Right pane
        if (c2) {
            c2->x = w->x + pos + SPLITTER_BAR_W;
            c2->y = w->y;
            c2->w = w->w - pos - SPLITTER_BAR_W;
            c2->h = w->h;

            if (c2->w < 0) {
                c2->w = 0;
            }

            widgetLayoutChildren(c2, font);
        }
    } else {
        // Top pane
        if (c1) {
            c1->x = w->x;
            c1->y = w->y;
            c1->w = w->w;
            c1->h = pos;
            widgetLayoutChildren(c1, font);
        }

        // Bottom pane
        if (c2) {
            c2->x = w->x;
            c2->y = w->y + pos + SPLITTER_BAR_W;
            c2->w = w->w;
            c2->h = w->h - pos - SPLITTER_BAR_W;

            if (c2->h < 0) {
                c2->h = 0;
            }

            widgetLayoutChildren(c2, font);
        }
    }
}


static void widgetSplitterOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mouseX, int32_t mouseY) {
    (void)root;
    SplitterDataT *d = (SplitterDataT *)w->data;
    int32_t pos;

    if (d->vertical) {
        pos = mouseX - w->x - d->dragStart;
    } else {
        pos = mouseY - w->y - d->dragStart;
    }

    widgetSplitterClampPos(w, &pos);

    if (pos != d->dividerPos) {
        d->dividerPos = pos;

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

        wgtInvalidate(w);
    }
}


// Mouse handling first checks if the click is on the divider bar.
// If so, it starts a drag. If not, it recursively hit-tests into
// children and forwards the event. This manual hit-test forwarding
// is needed because the splitter has WCLASS_NO_HIT_RECURSE (the
// generic hit-test would find the splitter but not recurse into its
// clipped children).
void widgetSplitterOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
    SplitterDataT *d   = (SplitterDataT *)hit->data;
    int32_t        pos = d->dividerPos;

    // Check if click is on the divider bar
    bool onDivider;

    if (d->vertical) {
        int32_t barX = hit->x + pos;
        onDivider = (vx >= barX && vx < barX + SPLITTER_BAR_W);
    } else {
        int32_t barY = hit->y + pos;
        onDivider = (vy >= barY && vy < barY + SPLITTER_BAR_W);
    }

    if (onDivider) {
        // Start dragging
        sDragWidget = hit;

        if (d->vertical) {
            d->dragStart = vx - hit->x - pos;
        } else {
            d->dragStart = vy - hit->y - pos;
        }

        return;
    }

    // Forward click to child widgets
    WidgetT *child = NULL;

    for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
        WidgetT *ch = widgetHitTest(c, vx, vy);

        if (ch) {
            child = ch;
        }
    }

    if (child && child->enabled && wclsHas(child, WGT_METHOD_ON_MOUSE)) {
        wclsOnMouse(child, root, vx, vy);
    }

    wgtInvalidatePaint(hit);
}


// Paint clips each child to its pane area, preventing overflow across
// the divider. The clip rect is saved/restored around each child's
// paint. The divider bar is painted last (on top) so it's always
// visible even if a child overflows.
//
// The gripper bumps use the classic embossed-dot technique: a
// highlight pixel at (x,y) and a shadow pixel at (x+1,y+1) create
// a tiny raised bump. 11 bumps spaced 3px apart center vertically
// (or horizontally) on the bar. This is purely decorative but
// provides the expected visual affordance for "draggable".
void widgetSplitterPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    SplitterDataT *d   = (SplitterDataT *)w->data;
    int32_t        pos = d->dividerPos;

    // Paint first child with clip rect
    WidgetT *c1 = spFirstChild(w);
    WidgetT *c2 = spSecondChild(w);

    int32_t oldClipX = disp->clipX;
    int32_t oldClipY = disp->clipY;
    int32_t oldClipW = disp->clipW;
    int32_t oldClipH = disp->clipH;

    if (c1) {
        if (d->vertical) {
            setClipRect(disp, w->x, w->y, pos, w->h);
        } else {
            setClipRect(disp, w->x, w->y, w->w, pos);
        }

        widgetPaintOne(c1, disp, ops, font, colors);
        setClipRect(disp, oldClipX, oldClipY, oldClipW, oldClipH);
    }

    if (c2) {
        if (d->vertical) {
            setClipRect(disp, w->x + pos + SPLITTER_BAR_W, w->y, w->w - pos - SPLITTER_BAR_W, w->h);
        } else {
            setClipRect(disp, w->x, w->y + pos + SPLITTER_BAR_W, w->w, w->h - pos - SPLITTER_BAR_W);
        }

        widgetPaintOne(c2, disp, ops, font, colors);
        setClipRect(disp, oldClipX, oldClipY, oldClipW, oldClipH);
    }

    // Draw divider bar  -- raised bevel
    BevelStyleT bevel = BEVEL_RAISED(colors, 1);

    if (d->vertical) {
        int32_t barX = w->x + pos;
        drawBevel(disp, ops, barX, w->y, SPLITTER_BAR_W, w->h, &bevel);

        // Gripper  -- row of embossed 2x2 bumps centered vertically
        int32_t gx = barX + 1;
        int32_t midY = w->y + w->h / 2;
        int32_t count = 5;

        for (int32_t i = -count; i <= count; i++) {
            int32_t dy = midY + i * 3;
            drawHLine(disp, ops, gx, dy, 2, colors->windowHighlight);
            drawHLine(disp, ops, gx + 1, dy + 1, 2, colors->windowShadow);
        }
    } else {
        int32_t barY = w->y + pos;
        drawBevel(disp, ops, w->x, barY, w->w, SPLITTER_BAR_W, &bevel);

        // Gripper  -- row of embossed 2x2 bumps centered horizontally
        int32_t gy = barY + 1;
        int32_t midX = w->x + w->w / 2;
        int32_t count = 5;

        for (int32_t i = -count; i <= count; i++) {
            int32_t dx = midX + i * 3;
            drawHLine(disp, ops, dx, gy, 1, colors->windowHighlight);
            drawHLine(disp, ops, dx + 1, gy, 1, colors->windowShadow);
            drawHLine(disp, ops, dx, gy + 1, 1, colors->windowHighlight);
            drawHLine(disp, ops, dx + 1, gy + 1, 1, colors->windowShadow);
        }
    }
}
