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

// imgview.c  -- DVX Image Viewer
//
// Displays BMP, PNG, JPG, and GIF images.  The image is scaled to fit
// the window while preserving aspect ratio.  Resize the window to zoom.
// Open files via the File menu or by launching with Run in the Task Manager.

#include "dvxApp.h"
#include "dvxDlg.h"
#include "dvxWgt.h"
#include "dvxWm.h"
#include "shellApp.h"
#include "stb_image_wrap.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "dvxMem.h"

// ============================================================
// App descriptor
// ============================================================

AppDescriptorT appDescriptor = {
    .name          = "Image Viewer",
    .hasMainLoop   = false,
    .multiInstance = true,
    .stackSize     = SHELL_STACK_DEFAULT,
    .priority      = TS_PRIORITY_NORMAL
};

// ============================================================
// Constants
// ============================================================

#define IV_WIN_W    400
#define IV_WIN_H    320
#define CMD_OPEN    100
#define CMD_CLOSE   101

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

int32_t     appMain(DxeAppContextT *ctx);
static void loadAndDisplay(const char *path);
static void onMenu(WindowT *win, int32_t menuId);
static void onPaint(WindowT *win, RectT *dirty);
static void onResize(WindowT *win, int32_t contentW, int32_t contentH);
static void openFile(void);

// ============================================================
// Module state
// ============================================================

static DxeAppContextT *sCtx    = NULL;
static AppContextT    *sAc     = NULL;
static WindowT        *sWin    = NULL;

// Source image (RGB, from stb_image)
static uint8_t        *sImgRgb = NULL;
static int32_t         sImgW   = 0;
static int32_t         sImgH   = 0;

// Scaled image in native pixel format (for direct blit)
static uint8_t        *sScaled      = NULL;
static int32_t         sScaledW     = 0;
static int32_t         sScaledH     = 0;
static int32_t         sScaledPitch = 0;
static int32_t         sLastFitW    = 0;  // window size the image was last scaled for
static int32_t         sLastFitH    = 0;


// ============================================================
// buildScaled  -- scale source image to fit window
// ============================================================

static void buildScaled(int32_t fitW, int32_t fitH) {
    free(sScaled);
    sScaled  = NULL;
    sLastFitW = fitW;
    sLastFitH = fitH;

    if (!sImgRgb || fitW < 1 || fitH < 1) {
        return;
    }

    // Fit image into fitW x fitH preserving aspect ratio
    int32_t dstW = fitW;
    int32_t dstH = (sImgH * fitW) / sImgW;

    if (dstH > fitH) {
        dstH = fitH;
        dstW = (sImgW * fitH) / sImgH;
    }

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

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

    int32_t bpp       = sAc->display.format.bytesPerPixel;
    int32_t pitch     = dstW * bpp;
    int32_t bitsPerPx = sAc->display.format.bitsPerPixel;

    sScaled      = (uint8_t *)malloc(pitch * dstH);
    sScaledW     = dstW;
    sScaledH     = dstH;
    sScaledPitch = pitch;

    if (!sScaled) {
        return;
    }

    // Bilinear scale from sImgRgb to native pixel format
    int32_t srcStride = sImgW * 3;

    for (int32_t y = 0; y < dstH; y++) {
        if ((y & 31) == 0 && y > 0) {
            dvxUpdate(sAc);
        }

        int32_t  srcYfp = (int32_t)((int64_t)y * sImgH * 65536 / dstH);
        int32_t  sy0    = srcYfp >> 16;
        int32_t  sy1    = sy0 + 1;
        int32_t  fy     = (srcYfp >> 8) & 0xFF;
        int32_t  ify    = 256 - fy;
        uint8_t *dst    = sScaled + y * pitch;

        if (sy1 >= sImgH) {
            sy1 = sImgH - 1;
        }

        uint8_t *row0 = sImgRgb + sy0 * srcStride;
        uint8_t *row1 = sImgRgb + sy1 * srcStride;

        for (int32_t x = 0; x < dstW; x++) {
            int32_t srcXfp = (int32_t)((int64_t)x * sImgW * 65536 / dstW);
            int32_t sx0    = srcXfp >> 16;
            int32_t sx1    = sx0 + 1;
            int32_t fx     = (srcXfp >> 8) & 0xFF;
            int32_t ifx    = 256 - fx;

            if (sx1 >= sImgW) {
                sx1 = sImgW - 1;
            }

            uint8_t *p00 = row0 + sx0 * 3;
            uint8_t *p10 = row0 + sx1 * 3;
            uint8_t *p01 = row1 + sx0 * 3;
            uint8_t *p11 = row1 + sx1 * 3;

            int32_t r = (p00[0] * ifx * ify + p10[0] * fx * ify + p01[0] * ifx * fy + p11[0] * fx * fy) >> 16;
            int32_t g = (p00[1] * ifx * ify + p10[1] * fx * ify + p01[1] * ifx * fy + p11[1] * fx * fy) >> 16;
            int32_t b = (p00[2] * ifx * ify + p10[2] * fx * ify + p01[2] * ifx * fy + p11[2] * fx * fy) >> 16;

            uint32_t px = packColor(&sAc->display, (uint8_t)r, (uint8_t)g, (uint8_t)b);

            if (bitsPerPx == 8) {
                dst[x] = (uint8_t)px;
            } else if (bitsPerPx == 15 || bitsPerPx == 16) {
                ((uint16_t *)dst)[x] = (uint16_t)px;
            } else {
                ((uint32_t *)dst)[x] = px;
            }
        }
    }
}


// ============================================================
// loadAndDisplay
// ============================================================

static void loadAndDisplay(const char *path) {
    // Free previous image
    stbi_image_free(sImgRgb);
    sImgRgb = NULL;

    dvxSetBusy(sAc, true);

    int imgW;
    int imgH;
    int channels;
    sImgRgb = stbi_load(path, &imgW, &imgH, &channels, 3);
    sImgW = imgW;
    sImgH = imgH;

    if (!sImgRgb) {
        dvxSetBusy(sAc, false);
        dvxMessageBox(sAc, "Error", "Could not load image.", MB_OK | MB_ICONERROR);
        return;
    }

    // Update title bar
    const char *fname = strrchr(path, '/');
    const char *bslash = strrchr(path, '\\');

    if (bslash > fname) {
        fname = bslash;
    }

    fname = fname ? fname + 1 : path;

    char title[280];
    snprintf(title, sizeof(title), "%s - Image Viewer", fname);
    dvxSetTitle(sAc, sWin, title);

    // Scale and repaint
    buildScaled(sWin->contentW, sWin->contentH);
    dvxSetBusy(sAc, false);

    dvxInvalidateWindow(sAc, sWin);
}


// ============================================================
// onMenu
// ============================================================

static void onMenu(WindowT *win, int32_t menuId) {
    (void)win;

    switch (menuId) {
        case CMD_OPEN:
            openFile();
            break;

        case CMD_CLOSE:
            dvxDestroyWindow(sAc, sWin);
            sWin = NULL;
            break;
    }
}


// ============================================================
// onPaint
// ============================================================

static void onPaint(WindowT *win, RectT *dirty) {
    (void)dirty;

    // If the resize drag has ended and the window size changed since
    // the last scale, rebuild now.  During drag, just show the old
    // scaled image (or dark background) to avoid expensive per-frame scaling.
    if (sImgRgb && sAc->stack.resizeWindow < 0) {
        if (sLastFitW != win->contentW || sLastFitH != win->contentH) {
            dvxSetBusy(sAc, true);
            buildScaled(win->contentW, win->contentH);
            dvxSetBusy(sAc, false);
        }
    }

    DisplayT cd        = sAc->display;
    cd.backBuf         = win->contentBuf;
    cd.width           = win->contentW;
    cd.height          = win->contentH;
    cd.pitch           = win->contentPitch;
    cd.clipX           = 0;
    cd.clipY           = 0;
    cd.clipW           = win->contentW;
    cd.clipH           = win->contentH;

    // Fill background
    uint32_t bg = packColor(&sAc->display, 32, 32, 32);
    rectFill(&cd, &sAc->blitOps, 0, 0, win->contentW, win->contentH, bg);

    // Blit scaled image centered, clipped to content bounds
    if (sScaled) {
        int32_t offX = (win->contentW - sScaledW) / 2;
        int32_t offY = (win->contentH - sScaledH) / 2;
        int32_t bpp  = sAc->display.format.bytesPerPixel;

        // Compute visible region (clip source and dest)
        int32_t srcX = 0;
        int32_t srcY = 0;
        int32_t blitW = sScaledW;
        int32_t blitH = sScaledH;

        if (offX < 0) {
            srcX  = -offX;
            blitW += offX;
            offX  = 0;
        }

        if (offY < 0) {
            srcY  = -offY;
            blitH += offY;
            offY  = 0;
        }

        if (offX + blitW > win->contentW) {
            blitW = win->contentW - offX;
        }

        if (offY + blitH > win->contentH) {
            blitH = win->contentH - offY;
        }

        for (int32_t y = 0; y < blitH; y++) {
            uint8_t *src = sScaled + (srcY + y) * sScaledPitch + srcX * bpp;
            uint8_t *dst = win->contentBuf + (offY + y) * win->contentPitch + offX * bpp;
            memcpy(dst, src, blitW * bpp);
        }
    }
}


// ============================================================
// onResize
// ============================================================

static void onResize(WindowT *win, int32_t contentW, int32_t contentH) {
    (void)win;
    (void)contentW;
    (void)contentH;
    // Don't rescale here  -- onPaint handles it after the drag ends.
    // This avoids expensive bilinear scaling on every frame of a drag.
}


// ============================================================
// openFile
// ============================================================

static void openFile(void) {
    FileFilterT filters[] = {
        { "Images (*.bmp;*.jpg;*.png;*.gif)" },
        { "All Files (*.*)" }
    };
    char path[DVX_MAX_PATH];

    if (dvxFileDialog(sAc, "Open Image", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
        loadAndDisplay(path);
    }
}


// ============================================================
// appMain
// ============================================================

int32_t appMain(DxeAppContextT *ctx) {
    sCtx = ctx;
    sAc  = ctx->shellCtx;

    int32_t winX = (sAc->display.width - IV_WIN_W) / 2;
    int32_t winY = (sAc->display.height - IV_WIN_H) / 2;

    sWin = dvxCreateWindow(sAc, "Image Viewer", winX, winY, IV_WIN_W, IV_WIN_H, true);

    if (!sWin) {
        return -1;
    }

    sWin->onPaint  = onPaint;
    sWin->onResize = onResize;
    sWin->onMenu   = onMenu;

    MenuBarT *menuBar = wmAddMenuBar(sWin);
    MenuT    *fileMenu = wmAddMenu(menuBar, "&File");
    wmAddMenuItem(fileMenu, "&Open...\tCtrl+O", CMD_OPEN);
    wmAddMenuSeparator(fileMenu);
    wmAddMenuItem(fileMenu, "&Close", CMD_CLOSE);

    AccelTableT *accel = dvxCreateAccelTable();
    dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN);
    sWin->accelTable = accel;

    // Initial paint (dark background)
    RectT fullRect = {0, 0, sWin->contentW, sWin->contentH};
    onPaint(sWin, &fullRect);
    sWin->iconNeedsRefresh = true;

    return 0;
}
