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

// clock.c  -- Clock DXE application (main-loop with tsYield)
//
// This is a main-loop DXE app (hasMainLoop = true), in contrast to callback-
// only apps like notepad or progman. The difference is fundamental:
//
//   Callback-only: appMain creates UI, returns. Shell calls callbacks.
//   Main-loop:     appMain runs forever in its own task, calling tsYield()
//                  to cooperatively yield to the shell and other tasks.
//
// A main-loop app is needed when the app has ongoing work that can't be
// expressed purely as event callbacks  -- here, polling the system clock
// every second and repainting. The shell allocates a dedicated task stack
// and schedules this task alongside the main event loop.
//
// The onPaint/onClose callbacks still execute in task 0 during dvxUpdate(),
// not in this task. The main loop only handles the clock-tick polling; the
// actual rendering and window management happen through the normal callback
// path. tsYield() is what makes this cooperative  -- without it, this task
// would starve the shell and all other apps.

#include "dvxApp.h"
#include "dvxWgt.h"
#include "dvxDraw.h"
#include "dvxVideo.h"
#include "shellApp.h"
#include "taskSwch.h"

#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "dvxMem.h"

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

#define CLOCK_WIN_W        200
#define CLOCK_WIN_H        100
#define CLOCK_MARGIN       40
#define CLOCK_DATE_GAP     8

typedef struct {
    bool    quit;
    char    timeStr[32];
    char    dateStr[32];
    time_t  lastUpdate;
} ClockStateT;

static DxeAppContextT *sCtx = NULL;
static WindowT        *sWin = NULL;
static ClockStateT     sState;

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

int32_t     appMain(DxeAppContextT *ctx);
void        appShutdown(void);
static void onClose(WindowT *win);
static void onPaint(WindowT *win, RectT *dirty);
static void updateTime(void);

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

// hasMainLoop = true tells the shell to create a dedicated task for appMain.
// TS_PRIORITY_LOW because clock updates are cosmetic and should never
// preempt interactive apps or the shell's event processing.
AppDescriptorT appDescriptor = {
    .name          = "Clock",
    .hasMainLoop   = true,
    .multiInstance = true,
    .stackSize     = SHELL_STACK_DEFAULT,
    .priority      = TS_PRIORITY_LOW
};


// The shell calls appShutdown (if exported) when force-killing an app via
// Task Manager or during shell shutdown. For main-loop apps, this is the
// signal to break out of the main loop. Without this, shellForceKillApp
// would have to terminate the task forcibly, potentially leaking resources.
void appShutdown(void) {
    sState.quit = true;
}


// Setting quit = true tells the main loop (running in a separate task) to
// exit. The main loop then destroys the window and returns from appMain,
// which causes the shell to reap this task and unload the DXE.
static void onClose(WindowT *win) {
    (void)win;
    sState.quit = true;
}


// onPaint demonstrates the raw paint callback approach (no widget tree).
// Instead of using wgtInitWindow + widgets, this app renders directly into
// the window's content buffer. This is the lower-level alternative to the
// widget system  -- useful for custom rendering like this centered clock display.
//
// We create a temporary DisplayT struct pointing at the window's content buffer
// so the drawing primitives (rectFill, drawText) can operate on the window
// content directly rather than the backbuffer.
static void onPaint(WindowT *win, RectT *dirty) {
    (void)dirty;

    AppContextT        *ac     = sCtx->shellCtx;
    const BlitOpsT     *ops    = dvxGetBlitOps(ac);
    const BitmapFontT  *font   = dvxGetFont(ac);
    const ColorSchemeT *colors = dvxGetColors(ac);

    // Shallow copy of the display, redirected to the window's content buffer.
    // This lets us reuse the standard drawing API without modifying the
    // global display state.
    DisplayT cd    = *dvxGetDisplay(ac);
    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;

    // Background
    rectFill(&cd, ops, 0, 0, win->contentW, win->contentH, colors->contentBg);

    // Time string (large, centered)
    int32_t timeW = textWidth(font, sState.timeStr);
    int32_t timeX = (win->contentW - timeW) / 2;
    int32_t timeY = win->contentH / 3;
    drawText(&cd, ops, font, timeX, timeY, sState.timeStr, colors->contentFg, colors->contentBg, true);

    // Date string (centered below)
    int32_t dateW = textWidth(font, sState.dateStr);
    int32_t dateX = (win->contentW - dateW) / 2;
    int32_t dateY = timeY + font->charHeight + CLOCK_DATE_GAP;
    drawText(&cd, ops, font, dateX, dateY, sState.dateStr, colors->contentFg, colors->contentBg, true);
}


static void updateTime(void) {
    time_t now = time(NULL);
    struct tm *tm = localtime(&now);

    if (!tm) {
        snprintf(sState.timeStr, sizeof(sState.timeStr), "--:--:--");
        snprintf(sState.dateStr, sizeof(sState.dateStr), "----------");
        sState.lastUpdate = now;
        return;
    }

    int32_t     hour12 = tm->tm_hour % 12;
    const char *ampm   = (tm->tm_hour < 12) ? "AM" : "PM";

    if (hour12 == 0) {
        hour12 = 12;
    }

    snprintf(sState.timeStr, sizeof(sState.timeStr), "%d:%02d:%02d %s", (int)hour12, tm->tm_min, tm->tm_sec, ampm);
    snprintf(sState.dateStr, sizeof(sState.dateStr), "%04d-%02d-%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
    sState.lastUpdate = now;
}


// This runs in its own task. The shell creates the task before calling
// appMain, and reaps it when appMain returns.
int32_t appMain(DxeAppContextT *ctx) {
    sCtx = ctx;
    AppContextT *ac = ctx->shellCtx;

    memset(&sState, 0, sizeof(sState));
    updateTime();

    // Position in the upper-right corner, out of the way of other windows
    int32_t winW = CLOCK_WIN_W;
    int32_t winH = CLOCK_WIN_H;
    int32_t winX = ac->display.width - winW - CLOCK_MARGIN;
    int32_t winY = CLOCK_MARGIN;

    // resizable=false: clock has a fixed size, no resize handle
    sWin = dvxCreateWindow(ac, "Clock", winX, winY, winW, winH, false);

    if (!sWin) {
        return -1;
    }

    sWin->onClose = onClose;
    sWin->onPaint = onPaint;

    // Main loop: check if the second has changed, repaint if so, then yield.
    // First paint happens automatically on the next dvxUpdate frame.
    // tsYield() transfers control back to the shell's task scheduler.
    // On a 486, time() resolution is 1 second, so we yield many times per
    // second between actual updates  -- this keeps CPU usage near zero.
    // dvxInvalidateWindow marks the window dirty and calls onPaint to
    // update the content buffer before the compositor flushes it.
    while (!sState.quit) {
        time_t now = time(NULL);

        if (now != sState.lastUpdate) {
            updateTime();
            dvxInvalidateWindow(ac, sWin);
        }

        tsYield();
    }

    // Cleanup
    if (sWin) {
        dvxDestroyWindow(ac, sWin);
        sWin = NULL;
    }

    return 0;
}
