/*
    Copyright (c) 2026 Christian Schoenebeck

    This file is part of "gigedit" and released under the terms of the
    GNU General Public License version 2.
*/

#ifndef GIGEDIT_DEBOUNCE_H
#define GIGEDIT_DEBOUNCE_H

#include <functional>
#include <chrono>
#include "compat.h"

/** @brief Debouncer with dynamically increased delay.
 *
 * Used to reduce the amount of calls on a heavy function, e.g. to keep an UI
 * responsive. Unlike a standard debouncer, this class is dynamically increasing
 * the delay of the function being called to allow minimum latency if possible.
 * It is configurable whether to call function immediately on very first
 * invocation, e.g. to ease debugging like getting the full call stack on a
 * crash of the function.
 */
class DynDebouncer {
    typedef std::chrono::steady_clock Clock;
    typedef std::chrono::time_point<Clock> TimeStamp;
public:
    //TODO: default values commented for now as it errors with GCC even with C++14
    struct Opt {
        // actual function to be invoked by this debouncer
        std::function<void()> fn;
        // if false: directly invoke fn() on very first event without delay
        bool delayFirst /* = true */;
        // initial delay waiting for invoke() to settle before calling fn()
        double startDelaySeconds /* = 0.01 */;
        // max. duration after last invocation when fn() must be called
        double maxDelaySeconds /* = 1.0 */;
        // increase the dynamic delay by this amount of stages (min. 1)
        int delayIncreaseStages /* = 5 */;
    };

    DynDebouncer(const Opt& o) {
        this->o = o;
        resetDelay();
    }

    virtual ~DynDebouncer() {
        signalTimeout.disconnect();
    }

    void invoke() {
        #if DEBUG_DYN_DEBOUNCER
        printf("DynDebouncer: invoke()\n");
        #endif
        const TimeStamp now = Clock::now();
        const std::chrono::duration<double> diff = now - latestInvocation;
        const bool green = diff.count() > delay;

        latestInvocation = now;

        if (!green || o.delayFirst) { // debounce ...
            // abort already queued timeout signal
            signalTimeout.disconnect();
            // schedule new timeout signal
            #if DEBUG_DYN_DEBOUNCER
            printf("DynDebouncer: queue timeout (delay=%f)\n", delay);
            #endif
            signalTimeout = Glib::signal_timeout().connect(
                [this]() -> bool {
                    return onTimeout();
                },
                int(delay * 1000.0)
            );
            increaseDelay();
            return;
        }

        // direct invocation (without any delay)
        #if DEBUG_DYN_DEBOUNCER
        printf("DynDebouncer: fn() immediately\n");
        #endif
        o.fn();
    }

protected:
    bool onTimeout() {
        signalTimeout.disconnect();
        resetDelay();
        #if DEBUG_DYN_DEBOUNCER
        printf("DynDebouncer: fn() (delayed)\n");
        #endif
        o.fn();
        return false; // don't fire signal again
    }

    void resetDelay() {
        #if DEBUG_DYN_DEBOUNCER
        printf("DynDebouncer: reset delay\n");
        #endif
        delay = o.startDelaySeconds;
        stage = 0;
    }

    void increaseDelay() {
        if (stage >= o.delayIncreaseStages) return;
        stage++;
        const double increasePerStage =
            (o.maxDelaySeconds - o.startDelaySeconds) / o.delayIncreaseStages;
        delay = o.startDelaySeconds + stage * increasePerStage;
        #if DEBUG_DYN_DEBOUNCER
        printf("DynDebouncer: increase delay=%f stage=%d\n", delay, stage);
        #endif
    }

private:
    Opt o;
    sigc::connection signalTimeout;
    TimeStamp latestInvocation;
    double delay;
    int stage;
};

#endif // GIGEDIT_DEBOUNCE_H
