// 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
// widgetTimer.c  -- Invisible timer widget
//
// Fires onChange callbacks at a configurable interval.  Supports both
// one-shot and repeating modes.  The timer is invisible  -- it takes no
// space in layout and produces no visual output.
//
// Active timers are tracked via a module-level linked list (using the
// stb_ds dynamic array) so wgtUpdateTimers can check only timers, not
// walk the entire widget tree each frame.

#include "dvxWgtP.h"

static int32_t sTypeId = -1;

#include <time.h>

#include "stb_ds_wrap.h"

typedef struct {
    int32_t intervalMs;
    bool    repeat;
    bool    running;
    clock_t lastFire;
} TimerDataT;


// Active timer list
static WidgetT **sActiveTimers = NULL;

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

static void timerAddToActiveList(WidgetT *w);
static void timerRemoveFromActiveList(WidgetT *w);
void        widgetTimerCalcMinSize(WidgetT *w, const BitmapFontT *font);
static void widgetTimerDestroy(WidgetT *w);
void        wgtTimerStart(WidgetT *w);
void        wgtTimerStop(WidgetT *w);


static void timerAddToActiveList(WidgetT *w) {
    // Check if already present
    for (int32_t i = 0; i < arrlen(sActiveTimers); i++) {
        if (sActiveTimers[i] == w) {
            return;
        }
    }

    arrput(sActiveTimers, w);
}


static void timerRemoveFromActiveList(WidgetT *w) {
    for (int32_t i = 0; i < arrlen(sActiveTimers); i++) {
        if (sActiveTimers[i] == w) {
            arrdel(sActiveTimers, i);
            return;
        }
    }
}


void widgetTimerCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    (void)font;
    w->calcMinW = 0;
    w->calcMinH = 0;
}


static void widgetTimerDestroy(WidgetT *w) {
    timerRemoveFromActiveList(w);
    free(w->data);
    w->data = NULL;
}


WidgetT *wgtTimer(WidgetT *parent, int32_t intervalMs, bool repeat) {
    WidgetT *w = widgetAlloc(parent, sTypeId);

    if (!w) {
        return NULL;
    }

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

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

    w->data        = d;
    w->visible     = false;
    d->intervalMs  = intervalMs;
    d->repeat      = repeat;
    // Match VB Timer default (Enabled=True on create).  Callers can
    // Stop() if they want it dormant.
    d->running  = true;
    d->lastFire = clock();
    timerAddToActiveList(w);
    return w;
}


int32_t wgtTimerGetInterval(const WidgetT *w) {
    if (!w || w->type != sTypeId) {
        return 0;
    }

    TimerDataT *d = (TimerDataT *)w->data;
    return d->intervalMs;
}


bool wgtTimerIsRunning(const WidgetT *w) {
    if (!w || w->type != sTypeId) {
        return false;
    }

    TimerDataT *d = (TimerDataT *)w->data;
    return d->running;
}


void wgtTimerSetEnabled(WidgetT *w, bool enabled) {
    if (enabled) {
        wgtTimerStart(w);
    } else {
        wgtTimerStop(w);
    }
}


void wgtTimerSetInterval(WidgetT *w, int32_t intervalMs) {
    if (!w || w->type != sTypeId) {
        return;
    }

    TimerDataT *d = (TimerDataT *)w->data;
    d->intervalMs = intervalMs;
}


void wgtTimerStart(WidgetT *w) {
    if (!w || w->type != sTypeId) {
        return;
    }

    TimerDataT *d = (TimerDataT *)w->data;
    d->running  = true;
    d->lastFire = clock();
    timerAddToActiveList(w);
}


void wgtTimerStop(WidgetT *w) {
    if (!w || w->type != sTypeId) {
        return;
    }

    TimerDataT *d = (TimerDataT *)w->data;
    d->running = false;
    timerRemoveFromActiveList(w);
}


void wgtUpdateTimers(void) {
    clock_t now = clock();

    // Iterate backwards so arrdel doesn't skip entries
    for (int32_t i = arrlen(sActiveTimers) - 1; i >= 0; i--) {
        WidgetT *w    = sActiveTimers[i];
        TimerDataT *d = (TimerDataT *)w->data;

        // A timer on a deferred-destroyed window must not fire: the
        // widget tree is still allocated, but its backing app state
        // (e.g. the BASIC control in userData) may already be freed.
        if (w->window && w->window->destroyPending) {
            continue;
        }

        if (!d->running) {
            arrdel(sActiveTimers, i);
            continue;
        }

        clock_t elapsed  = now - d->lastFire;
        clock_t interval = (clock_t)d->intervalMs * CLOCKS_PER_SEC / 1000;

        if (elapsed >= interval) {
            d->lastFire = now;

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

                // The callback may have destroyed this timer (or its
                // whole form), freeing w and d.  Every destroy path
                // removes the widget from sActiveTimers, so re-locate
                // it: missing means freed -- stop touching it.  The
                // list may also have shifted under us; refresh i.
                int32_t idx = -1;

                for (int32_t j = (int32_t)arrlen(sActiveTimers) - 1; j >= 0; j--) {
                    if (sActiveTimers[j] == w) {
                        idx = j;
                        break;
                    }
                }

                if (idx < 0) {
                    if (i > (int32_t)arrlen(sActiveTimers)) {
                        i = (int32_t)arrlen(sActiveTimers);
                    }

                    continue;
                }

                i = idx;

                // The relocation matched by pointer only: the callback may
                // have destroyed this timer and a newly created timer may
                // have reused the same heap address.  Re-fetch the data
                // pointer so the repeat/running checks below act on the
                // live TimerDataT, not the freed pre-callback one.
                d = (TimerDataT *)w->data;
            }

            if (!d->repeat) {
                d->running = false;
                arrdel(sActiveTimers, i);
            }
        }
    }
}


static const WidgetClassT sClassTimer = {
    .version = WGT_CLASS_VERSION,
    .flags   = 0,
    .handlers = {
        [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetTimerCalcMinSize,
        [WGT_METHOD_DESTROY]       = (void *)widgetTimerDestroy,
    }
};


static const struct {
    WidgetT *(*create)(WidgetT *parent, int32_t intervalMs, bool repeat);
    void     (*start)(WidgetT *w);
    void     (*stop)(WidgetT *w);
    void     (*setInterval)(WidgetT *w, int32_t intervalMs);
    bool     (*isRunning)(const WidgetT *w);
    void     (*updateTimers)(void);
} sApi = {
    .create            = wgtTimer,
    .start             = wgtTimerStart,
    .stop              = wgtTimerStop,
    .setInterval       = wgtTimerSetInterval,
    .isRunning         = wgtTimerIsRunning,
    .updateTimers      = wgtUpdateTimers
};

static const WgtPropDescT sProps[] = {
    { "Enabled",  WGT_IFACE_BOOL, (void *)wgtTimerIsRunning,    (void *)wgtTimerSetEnabled,  NULL },
    { "Interval", WGT_IFACE_INT,  (void *)wgtTimerGetInterval,   (void *)wgtTimerSetInterval, NULL }
};

static const WgtMethodDescT sMethods[] = {
    { "Start", WGT_SIG_VOID, (void *)wgtTimerStart },
    { "Stop",  WGT_SIG_VOID, (void *)wgtTimerStop  }
};

static const WgtEventDescT sEvents[] = {
    { "Timer" }
};

static const WgtIfaceT sIface = {
    .basName      = "Timer",
    .props        = sProps,
    .propCount    = 2,
    .methods      = sMethods,
    .methodCount  = 2,
    .events       = sEvents,
    .eventCount   = 1,
    .createSig    = WGT_CREATE_PARENT_INT_BOOL,
    .createArgs   = { 1000, 1 },
    .defaultEvent = "Timer"
};

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