// 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
// widgetImage.c  -- Image widget (displays bitmap, responds to clicks)
//
// Displays a bitmap image, optionally responding to clicks. The image data
// must be in the display's native pixel format (pre-converted). Two creation
// paths are provided:
//   - wgtImage: from raw pixel data already in display format (takes ownership)
//   - wgtImageFromFile: loads from file via stb_image and converts to display
//     format during load
//
// The widget supports a simple press effect (1px offset on click) and fires
// onClick immediately on mouse-down. Unlike Button which has press/release
// tracking, Image fires instantly  -- this is intentional for image-based
// click targets where visual press feedback is less important than
// responsiveness.
//
// No border or bevel is drawn  -- the image fills its widget bounds with
// centering if the widget is larger than the image.

#include "dvxWgtP.h"

static int32_t sTypeId = -1;

typedef struct {
    // Fields are ordered by alignment to minimize struct padding.
    uint8_t *pixelData;
    uint8_t *grayData;
    int32_t  imgW;
    int32_t  imgH;
    int32_t  imgPitch;
    uint32_t keyColor;
    bool     hasTransparency;
    bool     stretch;       // true = scale to fit widget bounds
    char     picturePath[DVX_MAX_PATH];
} ImageDataT;


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

WidgetT *wgtImage(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch);
WidgetT *wgtImageFromFile(WidgetT *parent, const char *path);
static int32_t wgtImageGetHeight(const WidgetT *w);
static const char *wgtImageGetPicture(const WidgetT *w);
static bool wgtImageGetStretch(const WidgetT *w);
static int32_t wgtImageGetWidth(const WidgetT *w);
static void wgtImageLoadFile(WidgetT *w, const char *path);
void wgtImageSetData(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, int32_t pitch);
static void wgtImageSetStretch(WidgetT *w, bool stretch);
void wgtImageSetTransparent(WidgetT *w, bool hasTransparency, uint32_t keyColor);
void wgtRegister(void);
void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetImageDestroy(WidgetT *w);
void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetImagePaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);


// Ownership contract: the widget takes ownership of pixelData
// unconditionally.  On success it is freed by widgetImageDestroy; on
// failure it is freed here before returning NULL.  Callers must never
// free pixelData themselves, even when this returns NULL.
WidgetT *wgtImage(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch) {
    WidgetT *wgt = widgetAlloc(parent, sTypeId);

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

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

    if (!d) {
        free(pixelData);
        widgetAllocRollback(wgt);
        return NULL;
    }

    d->pixelData = pixelData;
    d->imgW      = w;
    d->imgH      = h;
    d->imgPitch  = pitch;
    wgt->data    = d;
    return wgt;
}


WidgetT *wgtImageFromFile(WidgetT *parent, const char *path) {
    if (!parent || !path) {
        return NULL;
    }

    AppContextT *ctx = wgtGetContext(parent);

    if (!ctx) {
        return NULL;
    }

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

    if (!buf) {
        return NULL;
    }

    return wgtImage(parent, buf, imgW, imgH, pitch);
}


static int32_t wgtImageGetHeight(const WidgetT *w) {
    VALIDATE_WIDGET(w, sTypeId, 0);
    ImageDataT *d = (ImageDataT *)w->data;
    return d->imgH;
}


static const char *wgtImageGetPicture(const WidgetT *w) {
    VALIDATE_WIDGET(w, sTypeId, "");
    ImageDataT *d = (ImageDataT *)w->data;
    return d->picturePath;
}


static bool wgtImageGetStretch(const WidgetT *w) {
    VALIDATE_WIDGET(w, sTypeId, false);
    ImageDataT *d = (ImageDataT *)w->data;
    return d->stretch;
}


static int32_t wgtImageGetWidth(const WidgetT *w) {
    VALIDATE_WIDGET(w, sTypeId, 0);
    ImageDataT *d = (ImageDataT *)w->data;
    return d->imgW;
}


static void wgtImageLoadFile(WidgetT *w, const char *path) {
    VALIDATE_WIDGET_VOID(w, sTypeId);

    if (!path) {
        return;
    }

    AppContextT *ctx = wgtGetContext(w);

    if (!ctx) {
        return;
    }

    dvxSetBusy(ctx, true);

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

    dvxSetBusy(ctx, false);

    if (!buf) {
        return;
    }

    ImageDataT *d = (ImageDataT *)w->data;
    snprintf(d->picturePath, sizeof(d->picturePath), "%s", path);

    wgtImageSetData(w, buf, imgW, imgH, pitch);
}


void wgtImageSetData(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, int32_t pitch) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    ImageDataT *d = (ImageDataT *)w->data;

    free(d->pixelData);
    free(d->grayData);
    d->pixelData = pixelData;
    d->grayData  = NULL;
    d->imgW      = imgW;
    d->imgH      = imgH;
    d->imgPitch  = pitch;
    wgtInvalidate(w);
}


static void wgtImageSetStretch(WidgetT *w, bool stretch) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    ImageDataT *d = (ImageDataT *)w->data;
    d->stretch = stretch;
    wgtInvalidatePaint(w);
}


void wgtImageSetTransparent(WidgetT *w, bool hasTransparency, uint32_t keyColor) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    ImageDataT *d     = (ImageDataT *)w->data;
    d->hasTransparency = hasTransparency;
    d->keyColor        = keyColor;
    wgtInvalidatePaint(w);
}


void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    (void)font;
    ImageDataT *d = (ImageDataT *)w->data;

    if (d->stretch) {
        // Stretch mode: don't force the widget to the image size.
        // Let the layout engine determine the size; the paint method
        // will scale the image to fit.
        w->calcMinW = 0;
        w->calcMinH = 0;
    } else {
        w->calcMinW = d->imgW;
        w->calcMinH = d->imgH;
    }
}


void widgetImageDestroy(WidgetT *w) {
    ImageDataT *d = (ImageDataT *)w->data;

    if (d) {
        free(d->pixelData);
        free(d->grayData);
        free(d);
    }
}


// Mouse click: invalidates and fires onClick.  Image clicks are instant
// actions (e.g. clicking a logo or icon area) with no press/release tracking.
void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
    (void)root;
    (void)vx;
    (void)vy;
    wgtInvalidatePaint(w);

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


void widgetImagePaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    (void)font;
    (void)colors;
    ImageDataT *d = (ImageDataT *)w->data;

    if (!d->pixelData || d->imgW <= 0 || d->imgH <= 0) {
        return;
    }

    int32_t imgW = d->imgW;
    int32_t imgH = d->imgH;
    int32_t wgtW = w->w;
    int32_t wgtH = w->h;

    // Scale to fit (if stretch enabled) or use native size
    int32_t fitW;
    int32_t fitH;

    if (d->stretch && (imgW > wgtW || imgH > wgtH)) {
        fitW = wgtW;
        fitH = (imgH * wgtW) / imgW;

        if (fitH > wgtH) {
            fitH = wgtH;
            fitW = (imgW * wgtH) / imgH;
        }

        if (fitW <= 0) { fitW = 1; }
        if (fitH <= 0) { fitH = 1; }
    } else {
        fitW = imgW;
        fitH = imgH;
    }

    // Center in widget bounds
    int32_t dx = w->x + (wgtW - fitW) / 2;
    int32_t dy = w->y + (wgtH - fitH) / 2;

    // If image fits at 1:1, blit directly (no scaling needed)
    if (fitW == imgW && fitH == imgH) {
        uint8_t *src = w->enabled ? d->pixelData : d->grayData;

        if (!src) {
            src = d->pixelData;
        }

        if (d->hasTransparency && w->enabled) {
            rectCopyTransparent(disp, ops, dx, dy,
                                src, d->imgPitch,
                                0, 0, imgW, imgH, d->keyColor);
        } else {
            rectCopy(disp, ops, dx, dy,
                     src, d->imgPitch,
                     0, 0, imgW, imgH);
        }

        return;
    }

    // Nearest-neighbor scale: sample source pixels directly into the
    // display backbuffer. This avoids allocating a scaled copy.
    int32_t bpp = disp->format.bitsPerPixel;

    for (int32_t y = 0; y < fitH; y++) {
        int32_t screenY = dy + y;

        if (screenY < disp->clipY || screenY >= disp->clipY + disp->clipH) {
            continue;
        }

        int32_t srcY = (y * imgH) / fitH;

        if (srcY >= imgH) {
            srcY = imgH - 1;
        }

        uint8_t *srcRow = d->pixelData + srcY * d->imgPitch;

        for (int32_t x = 0; x < fitW; x++) {
            int32_t screenX = dx + x;

            if (screenX < disp->clipX || screenX >= disp->clipX + disp->clipW) {
                continue;
            }

            int32_t srcX = (x * imgW) / fitW;

            if (srcX >= imgW) {
                srcX = imgW - 1;
            }

            // Copy one pixel from source to display
            if (bpp == 16 || bpp == 15) {
                uint16_t px = ((uint16_t *)srcRow)[srcX];
                ((uint16_t *)(disp->backBuf + screenY * disp->pitch))[screenX] = px;
            } else if (bpp == 32) {
                uint32_t px = ((uint32_t *)srcRow)[srcX];
                ((uint32_t *)(disp->backBuf + screenY * disp->pitch))[screenX] = px;
            } else if (bpp == 8) {
                uint8_t px = srcRow[srcX];
                (disp->backBuf + screenY * disp->pitch)[screenX] = px;
            }
        }
    }
}


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

static const WidgetClassT sClassImage = {
    .version = WGT_CLASS_VERSION,
    .flags   = 0,
    .handlers = {
        [WGT_METHOD_PAINT]         = (void *)widgetImagePaint,
        [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetImageCalcMinSize,
        [WGT_METHOD_ON_MOUSE]      = (void *)widgetImageOnMouse,
        [WGT_METHOD_DESTROY]       = (void *)widgetImageDestroy,
        [WGT_METHOD_SET_TEXT]      = (void *)wgtImageLoadFile,
    }
};

static const struct {
    WidgetT *(*create)(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch);
    WidgetT *(*fromFile)(WidgetT *parent, const char *path);
    void     (*setData)(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, int32_t pitch);
    void     (*loadFile)(WidgetT *w, const char *path);
    void     (*setTransparent)(WidgetT *w, bool hasTransparency, uint32_t keyColor);
} sApi = {
    .create   = wgtImage,
    .fromFile = wgtImageFromFile,
    .setData  = wgtImageSetData,
    .loadFile       = wgtImageLoadFile,
    .setTransparent = wgtImageSetTransparent
};

static const WgtPropDescT sProps[] = {
    { "ImageHeight", WGT_IFACE_INT,    (void *)wgtImageGetHeight, NULL, NULL },
    { "ImageWidth",  WGT_IFACE_INT,    (void *)wgtImageGetWidth,  NULL, NULL },
    { "Picture",     WGT_IFACE_STRING, (void *)wgtImageGetPicture, (void *)wgtImageLoadFile, NULL },
    { "Stretch",     WGT_IFACE_BOOL,   (void *)wgtImageGetStretch, (void *)wgtImageSetStretch, NULL }
};

static const WgtIfaceT sIface = {
    .basName      = "Image",
    .props        = sProps,
    .propCount    = 4,
    .methods      = NULL,
    .methodCount  = 0,
    .events       = NULL,
    .eventCount   = 0,
    .createSig    = WGT_CREATE_PARENT_DATA,
    .defaultEvent = "Click"
};

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