// 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
// widgetCanvas.c  -- Drawable canvas widget (freehand draw, PNG save/load)
//
// The canvas widget provides a pixel buffer in the display's native pixel
// format that applications can draw into directly. It stores pixels in
// display format (not always RGB) to avoid per-pixel conversion on every
// repaint  -- the paint function just does a straight rectCopy blit from
// the canvas buffer to the display. This is critical on a 486 where
// per-pixel format conversion during repaint would be prohibitively slow.
//
// The tradeoff is that load/save operations must convert between RGB and
// the display format, but those are one-time costs at I/O time rather
// than per-frame costs.
//
// Drawing operations (dot, line, rect, circle) operate directly on the
// canvas buffer using canvasPutPixel for pixel-level writes. The dot
// primitive draws a filled circle using the pen size, and line uses
// Bresenham's algorithm placing dots along the path. This gives smooth
// freehand drawing with variable pen widths.
//
// Canvas coordinates are independent of widget position  -- (0,0) is the
// top-left of the canvas content, not the widget. Mouse events translate
// widget-space coordinates to canvas-space by subtracting the border offset.

#include "dvxWgtP.h"

static int32_t sTypeId = -1;

#define CANVAS_BORDER 2
#define CANVAS_SQRT_ITERATIONS 8   // integer-sqrt Newton iterations for span fill

typedef struct {
    uint8_t *pixelData;    // pixel buffer in display format
    int32_t  canvasW;
    int32_t  canvasH;
    int32_t  canvasPitch;
    int32_t  canvasBpp;    // cached bytes per pixel
    uint32_t penColor;
    int32_t  penSize;
    void   (*onMouse)(struct WidgetT *w, int32_t cx, int32_t cy, bool drag);
} CanvasDataT;


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

static void basClear(WidgetT *w, int32_t color);
static void basDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t color);
static void basDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height, int32_t color);
static void basFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius, int32_t color);
static void basFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height, int32_t color);
static uint32_t basGetPixel(const WidgetT *w, int32_t x, int32_t y);
static void basSetPenColor(WidgetT *w, int32_t color);
static void basSetPixel(WidgetT *w, int32_t x, int32_t y, int32_t color);
static int32_t canvasBufferBytes(int32_t w, int32_t h, int32_t bpp);
static void canvasDrawDot(CanvasDataT *cd, int32_t cx, int32_t cy);
static void canvasDrawLine(CanvasDataT *cd, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
static void canvasFillCircleSpans(CanvasDataT *cd, int32_t cx, int32_t cy, int32_t radius);
static inline uint32_t canvasGetPixel(const uint8_t *src, int32_t bpp);
static inline void canvasPutPixel(uint8_t *dst, uint32_t color, int32_t bpp);
static uint32_t rgbToPacked(WidgetT *w, uint32_t rgb);
WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h);
void wgtCanvasClear(WidgetT *w, uint32_t color);
void wgtCanvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height);
void wgtCanvasDrawText(WidgetT *w, int32_t x, int32_t y, const char *text);
void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius);
void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height);
uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y);
int32_t wgtCanvasLoad(WidgetT *w, const char *path);
void wgtCanvasResize(WidgetT *w, int32_t newW, int32_t newH);
int32_t wgtCanvasSave(WidgetT *w, const char *path);
void wgtCanvasSetMouseCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag));
void wgtCanvasSetPenColor(WidgetT *w, uint32_t color);
void wgtCanvasSetPenSize(WidgetT *w, int32_t size);
void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color);
void wgtRegister(void);
void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetCanvasDestroy(WidgetT *w);
static void widgetCanvasOnDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);


// BASIC-callable wrappers that convert 0x00RRGGBB colors to packed format.

static void basClear(WidgetT *w, int32_t color) {
    wgtCanvasClear(w, rgbToPacked(w, (uint32_t)color));
}


static void basDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t color) {
    wgtCanvasSetPenColor(w, rgbToPacked(w, (uint32_t)color));
    wgtCanvasDrawLine(w, x0, y0, x1, y1);
}


static void basDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height, int32_t color) {
    wgtCanvasSetPenColor(w, rgbToPacked(w, (uint32_t)color));
    wgtCanvasDrawRect(w, x, y, width, height);
}


static void basFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius, int32_t color) {
    wgtCanvasSetPenColor(w, rgbToPacked(w, (uint32_t)color));
    wgtCanvasFillCircle(w, cx, cy, radius);
}


static void basFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height, int32_t color) {
    wgtCanvasSetPenColor(w, rgbToPacked(w, (uint32_t)color));
    wgtCanvasFillRect(w, x, y, width, height);
}


static uint32_t basGetPixel(const WidgetT *w, int32_t x, int32_t y) {
    uint32_t packed = wgtCanvasGetPixel(w, x, y);
    AppContextT *ctx = wgtGetContext((WidgetT *)w);
    if (!ctx) { return packed; }
    uint8_t r;
    uint8_t g;
    uint8_t b;
    unpackColor(&ctx->display, packed, &r, &g, &b);
    return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}


static void basSetPenColor(WidgetT *w, int32_t color) {
    wgtCanvasSetPenColor(w, rgbToPacked(w, (uint32_t)color));
}


static void basSetPixel(WidgetT *w, int32_t x, int32_t y, int32_t color) {
    wgtCanvasSetPixel(w, x, y, rgbToPacked(w, (uint32_t)color));
}


// Pixel buffer size for a w x h canvas at bpp bytes per pixel, or -1 when
// the size does not fit in int32_t: a wrapped multiply would allocate a tiny
// buffer that the row fill loops then overrun.
//
// pixels = (int64_t)w * h cannot overflow int64_t for int32_t w,h, but the
// subsequent * bpp can, so the range is checked by division BEFORE the final
// multiply. After a passing guard, pixels <= INT32_MAX / bpp, so both
// pixels * bpp and (since w <= w * h) the caller's pitch = w * bpp fit int32_t.
static int32_t canvasBufferBytes(int32_t w, int32_t h, int32_t bpp) {
    int64_t pixels = (int64_t)w * h;

    if (w < 0 || h < 0 || bpp <= 0 || pixels > (int64_t)INT32_MAX / bpp) {
        return -1;
    }

    return (int32_t)(pixels * bpp);
}


// Draw a filled circle of diameter penSize at (cx, cy) in canvas coords.

static void canvasDrawDot(CanvasDataT *cd, int32_t cx, int32_t cy) {
    int32_t  bpp   = cd->canvasBpp;
    int32_t  pitch = cd->canvasPitch;
    uint8_t *data  = cd->pixelData;
    int32_t  cw    = cd->canvasW;
    int32_t  ch    = cd->canvasH;
    uint32_t color = cd->penColor;
    int32_t  rad   = cd->penSize / 2;

    if (rad < 1) {
        // Single pixel
        if (cx >= 0 && cx < cw && cy >= 0 && cy < ch) {
            uint8_t *dst = data + cy * pitch + cx * bpp;

            canvasPutPixel(dst, color, bpp);
        }

        return;
    }

    // Filled circle via per-row horizontal spans (shared helper).
    canvasFillCircleSpans(cd, cx, cy, rad);
}


// Bresenham line from (x0,y0) to (x1,y1), placing dots along the path.
// Each point on the line gets a full pen dot (canvasDrawDot), which means
// lines with large pen sizes are smooth rather than aliased. Bresenham was
// chosen over DDA because it's pure integer arithmetic  -- no floating point
// needed, which matters on 486SX (no FPU).

static void canvasDrawLine(CanvasDataT *cd, int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
    int32_t dx  = x1 - x0;
    int32_t dy  = y1 - y0;
    int32_t sx  = (dx >= 0) ? 1 : -1;
    int32_t sy  = (dy >= 0) ? 1 : -1;

    if (dx < 0) { dx = -dx; }
    if (dy < 0) { dy = -dy; }

    int32_t err = dx - dy;

    for (;;) {
        canvasDrawDot(cd, x0, y0);

        if (x0 == x1 && y0 == y1) {
            break;
        }

        int32_t e2 = 2 * err;

        if (e2 > -dy) {
            err -= dy;
            x0  += sx;
        }

        if (e2 < dx) {
            err += dx;
            y0  += sy;
        }
    }
}


// Fill a circle of the given radius centered at (cx, cy) using per-row
// horizontal spans. The half-span for each row is floor(sqrt(r^2 - dy^2)),
// computed with integer Newton's method so no FPU is required (486SX-safe).
// Shared by canvasDrawDot (pen dot) and wgtCanvasFillCircle.
static void canvasFillCircleSpans(CanvasDataT *cd, int32_t cx, int32_t cy, int32_t radius) {
    int32_t  bpp   = cd->canvasBpp;
    int32_t  pitch = cd->canvasPitch;
    uint8_t *data  = cd->pixelData;
    int32_t  cw    = cd->canvasW;
    int32_t  ch    = cd->canvasH;
    uint32_t color = cd->penColor;
    int32_t  r2    = radius * radius;

    for (int32_t dy = -radius; dy <= radius; dy++) {
        int32_t py = cy + dy;

        if (py < 0 || py >= ch) {
            continue;
        }

        int32_t dy2   = dy * dy;
        int32_t rem   = r2 - dy2;
        int32_t hspan = 0;

        if (rem > 0) {
            hspan = radius;

            for (int32_t i = 0; i < CANVAS_SQRT_ITERATIONS; i++) {
                hspan = (hspan + rem / hspan) / 2;
            }

            if (hspan * hspan > rem) {
                hspan--;
            }
        }

        int32_t x0 = cx - hspan;
        int32_t x1 = cx + hspan;

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

        if (x1 >= cw) {
            x1 = cw - 1;
        }

        if (x0 <= x1) {
            uint8_t *dst = data + py * pitch + x0 * bpp;

            for (int32_t px = x0; px <= x1; px++) {
                canvasPutPixel(dst, color, bpp);
                dst += bpp;
            }
        }
    }
}


// Read/write a single pixel at the given address, respecting the display's
// bytes-per-pixel depth (1=8-bit palette, 2=16-bit hicolor, 4=32-bit truecolor).
// These are inline because they're called per-pixel in tight loops (circle fill,
// line draw)  -- the function call overhead would dominate on a 486. The bpp
// branch is predictable since it doesn't change within a single draw operation.

static inline uint32_t canvasGetPixel(const uint8_t *src, int32_t bpp) {
    if (bpp == 1) {
        return *src;
    } else if (bpp == 2) {
        return *(const uint16_t *)src;
    } else {
        return *(const uint32_t *)src;
    }
}


static inline void canvasPutPixel(uint8_t *dst, uint32_t color, int32_t bpp) {
    if (bpp == 1) {
        *dst = (uint8_t)color;
    } else if (bpp == 2) {
        *(uint16_t *)dst = (uint16_t)color;
    } else {
        *(uint32_t *)dst = color;
    }
}


// Convert a 0x00RRGGBB color to the display's packed pixel format.
static uint32_t rgbToPacked(WidgetT *w, uint32_t rgb) {
    AppContextT *ctx = wgtGetContext(w);
    if (!ctx) { return rgb; }
    uint8_t r = (rgb >> 16) & 0xFF;
    uint8_t g = (rgb >> 8) & 0xFF;
    uint8_t b = rgb & 0xFF;
    return packColor(&ctx->display, r, g, b);
}


WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
    if (!parent || w <= 0 || h <= 0) {
        return NULL;
    }

    // Find the AppContextT to get display format
    AppContextT *ctx = wgtGetContext(parent);

    if (!ctx) {
        return NULL;
    }

    const DisplayT *d = &ctx->display;
    int32_t bpp       = d->format.bytesPerPixel;
    int32_t byteCount = canvasBufferBytes(w, h, bpp);

    if (byteCount < 0) {
        return NULL;
    }

    int32_t  pitch = w * bpp;
    uint8_t *data  = (uint8_t *)malloc(byteCount);

    if (!data) {
        return NULL;
    }

    // Fill with white using span fill for performance
    uint32_t white = packColor(d, 255, 255, 255);
    BlitOpsT canvasOps;
    drawInit(&canvasOps, d);

    for (int32_t y = 0; y < h; y++) {
        canvasOps.spanFill(data + y * pitch, white, w);
    }

    WidgetT *wgt = widgetAlloc(parent, sTypeId);

    if (!wgt) {
        free(data);
        return NULL;
    }

    wgt->contentOffX = CANVAS_BORDER;
    wgt->contentOffY = CANVAS_BORDER;

    CanvasDataT *cd = (CanvasDataT *)calloc(1, sizeof(CanvasDataT));

    if (!cd) {
        free(data);
        widgetAllocRollback(wgt);
        return NULL;
    }

    cd->pixelData    = data;
    cd->canvasW      = w;
    cd->canvasH      = h;
    cd->canvasPitch  = pitch;
    cd->canvasBpp    = bpp;
    cd->penColor     = packColor(d, 0, 0, 0);
    cd->penSize      = 1;
    wgt->data        = cd;
    return wgt;
}


void wgtCanvasClear(WidgetT *w, uint32_t color) {
    if (!w || w->type != sTypeId) {
        return;
    }

    CanvasDataT *cd = (CanvasDataT *)w->data;

    if (!cd->pixelData) {
        return;
    }

    // Find BlitOps for span fill
    AppContextT *ctx = wgtGetContext(w);
    if (!ctx) {
        return;
    }

    int32_t pitch = cd->canvasPitch;
    int32_t cw    = cd->canvasW;
    int32_t ch    = cd->canvasH;

    for (int32_t y = 0; y < ch; y++) {
        ctx->blitOps.spanFill(cd->pixelData + y * pitch, color, cw);
    }

    wgtInvalidatePaint(w);
}


// Draw a line using the current pen color and pen size.

void wgtCanvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
    if (!w || w->type != sTypeId) {
        return;
    }

    CanvasDataT *cd = (CanvasDataT *)w->data;

    if (!cd->pixelData) {
        return;
    }

    canvasDrawLine(cd, x0, y0, x1, y1);
    wgtInvalidatePaint(w);
}


// Draw a 1px outlined rectangle using the current pen color.

void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height) {
    if (!w || w->type != sTypeId) {
        return;
    }

    CanvasDataT *cd = (CanvasDataT *)w->data;

    if (!cd->pixelData) {
        return;
    }

    if (width <= 0 || height <= 0) {
        return;
    }

    int32_t  bpp   = cd->canvasBpp;
    int32_t  pitch = cd->canvasPitch;
    uint8_t *data  = cd->pixelData;
    int32_t  cw    = cd->canvasW;
    int32_t  ch    = cd->canvasH;
    uint32_t color = cd->penColor;

    // Top and bottom edges
    for (int32_t px = x; px < x + width; px++) {
        if (px < 0 || px >= cw) {
            continue;
        }

        if (y >= 0 && y < ch) {
            uint8_t *dst = data + y * pitch + px * bpp;

            canvasPutPixel(dst, color, bpp);
        }

        int32_t by = y + height - 1;

        if (by >= 0 && by < ch && by != y) {
            uint8_t *dst = data + by * pitch + px * bpp;

            canvasPutPixel(dst, color, bpp);
        }
    }

    // Left and right edges (excluding corners already drawn)
    for (int32_t py = y + 1; py < y + height - 1; py++) {
        if (py < 0 || py >= ch) {
            continue;
        }

        if (x >= 0 && x < cw) {
            uint8_t *dst = data + py * pitch + x * bpp;

            canvasPutPixel(dst, color, bpp);
        }

        int32_t rx = x + width - 1;

        if (rx >= 0 && rx < cw && rx != x) {
            uint8_t *dst = data + py * pitch + rx * bpp;

            canvasPutPixel(dst, color, bpp);
        }
    }

    wgtInvalidatePaint(w);
}


void wgtCanvasDrawText(WidgetT *w, int32_t x, int32_t y, const char *text) {
    if (!w || w->type != sTypeId || !text) {
        return;
    }

    CanvasDataT *cd = (CanvasDataT *)w->data;

    if (!cd->pixelData) {
        return;
    }

    AppContextT *ctx = wgtGetContext(w);

    if (!ctx) {
        return;
    }

    const BitmapFontT *font = &ctx->font;
    int32_t cw  = font->charWidth;
    int32_t ch  = font->charHeight;
    int32_t bpp = cd->canvasBpp;

    for (int32_t ci = 0; text[ci]; ci++) {
        int32_t cx = x + ci * cw;

        if (cx + cw <= 0 || cx >= cd->canvasW || y + ch <= 0 || y >= cd->canvasH) {
            continue;
        }

        int32_t idx = (uint8_t)text[ci] - font->firstChar;

        if (idx < 0 || idx >= font->numChars) {
            continue;
        }

        const uint8_t *glyph = font->glyphData + idx * ch;

        for (int32_t row = 0; row < ch; row++) {
            int32_t py = y + row;

            if (py < 0 || py >= cd->canvasH) {
                continue;
            }

            uint8_t bits = glyph[row];

            if (bits == 0) {
                continue;
            }

            for (int32_t col = 0; col < cw; col++) {
                int32_t px = cx + col;

                if (px < 0 || px >= cd->canvasW) {
                    continue;
                }

                if (bits & (0x80 >> col)) {
                    if (bpp == 2) {
                        *(uint16_t *)(cd->pixelData + py * cd->canvasPitch + px * 2) = (uint16_t)cd->penColor;
                    } else if (bpp == 4) {
                        *(uint32_t *)(cd->pixelData + py * cd->canvasPitch + px * 4) = cd->penColor;
                    } else {
                        cd->pixelData[py * cd->canvasPitch + px] = (uint8_t)cd->penColor;
                    }
                }
            }
        }
    }

    wgtInvalidatePaint(w);
}


// Draw a filled circle using the current pen color.

void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius) {
    if (!w || w->type != sTypeId) {
        return;
    }

    CanvasDataT *cd = (CanvasDataT *)w->data;

    if (!cd->pixelData) {
        return;
    }

    if (radius <= 0) {
        return;
    }

    canvasFillCircleSpans(cd, cx, cy, radius);

    wgtInvalidatePaint(w);
}


// Draw a filled rectangle using the current pen color.

void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height) {
    if (!w || w->type != sTypeId) {
        return;
    }

    CanvasDataT *cd = (CanvasDataT *)w->data;

    if (!cd->pixelData) {
        return;
    }

    if (width <= 0 || height <= 0) {
        return;
    }

    int32_t  bpp   = cd->canvasBpp;
    int32_t  pitch = cd->canvasPitch;
    uint8_t *data  = cd->pixelData;
    int32_t  cw    = cd->canvasW;
    int32_t  ch    = cd->canvasH;
    uint32_t color = cd->penColor;

    // Clip to canvas bounds
    int32_t x0 = x < 0 ? 0 : x;
    int32_t y0 = y < 0 ? 0 : y;
    int32_t x1 = x + width > cw ? cw : x + width;
    int32_t y1 = y + height > ch ? ch : y + height;
    int32_t fillW = x1 - x0;

    if (fillW <= 0) {
        return;
    }

    // Find BlitOps for span fill
    AppContextT *ctx = wgtGetContext(w);

    // Use the optimized spanFill (which uses rep stosl on x86) when available,
    // falling back to per-pixel writes if the AppContextT can't be reached.
    // The spanFill path is ~4x faster for 32-bit modes because it writes
    // 4 bytes per iteration instead of going through the bpp switch.
    if (ctx) {
        for (int32_t py = y0; py < y1; py++) {
            ctx->blitOps.spanFill(data + py * pitch + x0 * bpp, color, fillW);
        }
    } else {
        for (int32_t py = y0; py < y1; py++) {
            for (int32_t px = x0; px < x1; px++) {
                canvasPutPixel(data + py * pitch + px * bpp, color, bpp);
            }
        }
    }

    wgtInvalidatePaint(w);
}


uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y) {
    if (!w || w->type != sTypeId) {
        return 0;
    }

    const CanvasDataT *cd = (const CanvasDataT *)w->data;

    if (!cd->pixelData) {
        return 0;
    }

    if (x < 0 || x >= cd->canvasW || y < 0 || y >= cd->canvasH) {
        return 0;
    }

    int32_t bpp = cd->canvasBpp;
    const uint8_t *src = cd->pixelData + y * cd->canvasPitch + x * bpp;

    return canvasGetPixel(src, bpp);
}


int32_t wgtCanvasLoad(WidgetT *w, const char *path) {
    if (!w || w->type != sTypeId || !path) {
        return -1;
    }

    AppContextT *ctx = wgtGetContext(w);

    if (!ctx) {
        return -1;
    }

    int32_t  imgW;
    int32_t  imgH;
    int32_t  pitch;
    uint8_t *data = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch);

    if (!data) {
        return -1;
    }

    CanvasDataT *cd = (CanvasDataT *)w->data;

    free(cd->pixelData);
    cd->pixelData    = data;
    cd->canvasW      = imgW;
    cd->canvasH      = imgH;
    cd->canvasPitch  = pitch;
    cd->canvasBpp    = ctx->display.format.bytesPerPixel;

    // Load changes canvas dimensions (canvasW/canvasH drive the min-size
    // calc), so a full relayout is needed, not just a repaint.
    wgtInvalidate(w);
    return 0;
}


void wgtCanvasResize(WidgetT *w, int32_t newW, int32_t newH) {
    if (!w || w->type != sTypeId || newW <= 0 || newH <= 0) {
        return;
    }

    CanvasDataT *cd = (CanvasDataT *)w->data;

    if (!cd || (cd->canvasW == newW && cd->canvasH == newH)) {
        return;
    }

    AppContextT *ctx = wgtGetContext(w);

    if (!ctx) {
        return;
    }

    const DisplayT *d = &ctx->display;
    int32_t bpp       = d->format.bytesPerPixel;
    int32_t byteCount = canvasBufferBytes(newW, newH, bpp);

    if (byteCount < 0) {
        return;
    }

    int32_t  pitch = newW * bpp;
    uint8_t *data  = (uint8_t *)malloc(byteCount);

    if (!data) {
        return;
    }

    // Fill with white
    uint32_t white = packColor(d, 255, 255, 255);
    BlitOpsT canvasOps;
    drawInit(&canvasOps, d);

    for (int32_t y = 0; y < newH; y++) {
        canvasOps.spanFill(data + y * pitch, white, newW);
    }

    free(cd->pixelData);
    cd->pixelData   = data;
    cd->canvasW     = newW;
    cd->canvasH     = newH;
    cd->canvasPitch = pitch;
    cd->canvasBpp   = bpp;

    wgtInvalidatePaint(w);
}


int32_t wgtCanvasSave(WidgetT *w, const char *path) {
    if (!w || w->type != sTypeId || !path) {
        return -1;
    }

    CanvasDataT *cd = (CanvasDataT *)w->data;

    if (!cd->pixelData) {
        return -1;
    }

    AppContextT *ctx = wgtGetContext(w);

    if (!ctx) {
        return -1;
    }

    return dvxSaveImage(ctx, cd->pixelData, cd->canvasW, cd->canvasH, cd->canvasPitch, path);
}


void wgtCanvasSetMouseCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag)) {
    if (w && w->type == sTypeId) {
        CanvasDataT *cd = (CanvasDataT *)w->data;

        cd->onMouse = cb;
    }
}


void wgtCanvasSetPenColor(WidgetT *w, uint32_t color) {
    if (w && w->type == sTypeId) {
        CanvasDataT *cd = (CanvasDataT *)w->data;

        cd->penColor = color;
    }
}


void wgtCanvasSetPenSize(WidgetT *w, int32_t size) {
    if (w && w->type == sTypeId && size > 0) {
        CanvasDataT *cd = (CanvasDataT *)w->data;

        cd->penSize = size;
    }
}


void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color) {
    if (!w || w->type != sTypeId) {
        return;
    }

    CanvasDataT *cd = (CanvasDataT *)w->data;

    if (!cd->pixelData) {
        return;
    }

    if (x < 0 || x >= cd->canvasW || y < 0 || y >= cd->canvasH) {
        return;
    }

    int32_t  bpp = cd->canvasBpp;
    uint8_t *dst = cd->pixelData + y * cd->canvasPitch + x * bpp;

    canvasPutPixel(dst, color, bpp);

    // Guard the invalidate so a per-pixel plotting burst (BASIC SetPixel loop)
    // walks the ancestor chain only once per frame instead of once per pixel.
    // paintDirty is cleared together with childDirty during the paint pass, so
    // it is a safe proxy for "already invalidated this frame".
    if (!w->paintDirty) {
        wgtInvalidatePaint(w);
    }
}


// The canvas requests exactly its pixel dimensions plus the sunken bevel
// border. The font parameter is unused since the canvas has no text content.
// The canvas is not designed to scale  -- it reports its exact size as the
// minimum, and the layout engine should respect that.
void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    (void)font;
    CanvasDataT *cd = (CanvasDataT *)w->data;

    w->calcMinW = cd->canvasW + CANVAS_BORDER * 2;
    w->calcMinH = cd->canvasH + CANVAS_BORDER * 2;
}


void widgetCanvasDestroy(WidgetT *w) {
    CanvasDataT *cd = (CanvasDataT *)w->data;

    free(cd->pixelData);
    free(cd);
}


static void widgetCanvasOnDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
    (void)root;
    CanvasDataT *cd = (CanvasDataT *)w->data;

    if (!cd->onMouse) {
        return;
    }

    int32_t cx = vx - w->x - CANVAS_BORDER;
    int32_t cy = vy - w->y - CANVAS_BORDER;

    cd->onMouse(w, cx, cy, true);
    wgtInvalidatePaint(w);
}


// Mouse handler: translates widget-space coordinates to canvas-space (subtracting
// border offset) and invokes the application's mouse callback. The sDrawingCanvas
// global tracks whether this is a drag (mouse was already down on this canvas)
// vs a new click, so the callback can distinguish between starting a new stroke
// and continuing one.
void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
    (void)root;
    CanvasDataT *cd = (CanvasDataT *)hit->data;

    if (!cd->onMouse) {
        return;
    }

    // Convert widget coords to canvas coords
    int32_t cx = vx - hit->x - CANVAS_BORDER;
    int32_t cy = vy - hit->y - CANVAS_BORDER;

    sDragWidget = hit;

    cd->onMouse(hit, cx, cy, false);
    wgtInvalidatePaint(hit);
}


// Paint: draws a sunken bevel border then blits the canvas buffer. Because
// the canvas stores pixels in the display's native format, rectCopy is a
// straight memcpy per scanline  -- no per-pixel conversion needed. This makes
// repaint essentially free relative to the display bandwidth.
void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    (void)font;
    CanvasDataT *cd = (CanvasDataT *)w->data;

    if (!cd->pixelData) {
        return;
    }

    // Bevel wraps tightly around the bitmap, not the full widget.
    // If the layout stretched the widget beyond the bitmap, fill
    // the excess area with content background.
    int32_t bevelW = cd->canvasW + CANVAS_BORDER * 2;
    int32_t bevelH = cd->canvasH + CANVAS_BORDER * 2;

    if (w->w > bevelW || w->h > bevelH) {
        rectFill(d, ops, w->x, w->y, w->w, w->h, colors->contentBg);
    }

    BevelStyleT sunken;
    sunken.highlight = colors->windowShadow;
    sunken.shadow    = colors->windowHighlight;
    sunken.face      = BEVEL_NO_FILL;
    sunken.width     = CANVAS_BORDER;
    drawBevel(d, ops, w->x, w->y, bevelW, bevelH, &sunken);

    // Blit the canvas data inside the border
    rectCopy(d, ops, w->x + CANVAS_BORDER, w->y + CANVAS_BORDER,
             cd->pixelData, cd->canvasPitch,
             0, 0, cd->canvasW, cd->canvasH);
}


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

static const WidgetClassT sClassCanvas = {
    .version = WGT_CLASS_VERSION,
    .flags   = 0,
    .handlers = {
        [WGT_METHOD_PAINT]          = (void *)widgetCanvasPaint,
        [WGT_METHOD_CALC_MIN_SIZE]  = (void *)widgetCanvasCalcMinSize,
        [WGT_METHOD_ON_MOUSE]       = (void *)widgetCanvasOnMouse,
        [WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetCanvasOnDragUpdate,
        [WGT_METHOD_DESTROY]        = (void *)widgetCanvasDestroy,
    }
};

static const struct {
    WidgetT *(*create)(WidgetT *parent, int32_t w, int32_t h);
    void     (*clear)(WidgetT *w, uint32_t color);
    void     (*setPenColor)(WidgetT *w, uint32_t color);
    void     (*setPenSize)(WidgetT *w, int32_t size);
    void     (*setMouseCallback)(WidgetT *w, void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag));
    int32_t  (*save)(WidgetT *w, const char *path);
    int32_t  (*load)(WidgetT *w, const char *path);
    void     (*drawLine)(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
    void     (*drawRect)(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height);
    void     (*fillRect)(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height);
    void     (*fillCircle)(WidgetT *w, int32_t cx, int32_t cy, int32_t radius);
    void     (*setPixel)(WidgetT *w, int32_t x, int32_t y, uint32_t color);
    uint32_t (*getPixel)(const WidgetT *w, int32_t x, int32_t y);
    void     (*drawText)(WidgetT *w, int32_t x, int32_t y, const char *text);
    void     (*resize)(WidgetT *w, int32_t newW, int32_t newH);
} sApi = {
    .create           = wgtCanvas,
    .clear            = wgtCanvasClear,
    .setPenColor      = wgtCanvasSetPenColor,
    .setPenSize       = wgtCanvasSetPenSize,
    .setMouseCallback = wgtCanvasSetMouseCallback,
    .save             = wgtCanvasSave,
    .load             = wgtCanvasLoad,
    .drawLine         = wgtCanvasDrawLine,
    .drawRect         = wgtCanvasDrawRect,
    .fillRect         = wgtCanvasFillRect,
    .fillCircle       = wgtCanvasFillCircle,
    .setPixel         = wgtCanvasSetPixel,
    .getPixel         = wgtCanvasGetPixel,
    .drawText         = wgtCanvasDrawText,
    .resize           = wgtCanvasResize
};

static const WgtMethodDescT sMethods[] = {
    { "Clear",       WGT_SIG_INT,             (void *)basClear },
    { "DrawLine",    WGT_SIG_INT5,            (void *)basDrawLine },
    { "DrawRect",    WGT_SIG_INT5,            (void *)basDrawRect },
    { "DrawText",    WGT_SIG_INT_INT_STR,     (void *)wgtCanvasDrawText },
    { "FillCircle",  WGT_SIG_INT4,            (void *)basFillCircle },
    { "FillRect",    WGT_SIG_INT5,            (void *)basFillRect },
    { "GetPixel",    WGT_SIG_RET_INT_INT_INT, (void *)basGetPixel },
    { "Load",        WGT_SIG_STR,             (void *)wgtCanvasLoad },
    { "Resize",      WGT_SIG_INT_INT,         (void *)wgtCanvasResize },
    { "Save",        WGT_SIG_STR,             (void *)wgtCanvasSave },
    { "SetPenColor", WGT_SIG_INT,             (void *)basSetPenColor },
    { "SetPenSize",  WGT_SIG_INT,             (void *)wgtCanvasSetPenSize },
    { "SetPixel",    WGT_SIG_INT3,            (void *)basSetPixel },
};

static const WgtIfaceT sIface = {
    // VB-compatible name is "PictureBox" -- matches what the rest of
    // the docs (ideguide, canvas.bhs) and sample .frm files (iconed)
    // already use.  Prior "Canvas" basName caused silent frm-load
    // drops whenever an app asked for a PictureBox.
    .basName      = "PictureBox",
    .props        = NULL,
    .propCount    = 0,
    .methods      = sMethods,
    .methodCount  = 13,
    .events       = NULL,
    .eventCount   = 0,
    .createSig    = WGT_CREATE_PARENT_INT_INT,
    .createArgs   = { 64, 64 },
    .defaultEvent = "Click",
    .namePrefix   = "Picture"
};

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