#pragma once
#include <cstdint>

enum class Gesture { NONE, CLICK, LONG, LONG_RELEASE, DOUBLE_CLICK };
// Independent of GPIO/task scheduling: one long event, no trailing click.
//
// Single/double-click disambiguation (only when allow_double is true):
// the first short release is not reported immediately. A pending CLICK is armed
// with an absolute deadline of now + DOUBLE_GAP_MS. A second confirmed short
// press+release of the SAME button before that deadline reports exactly one
// DOUBLE_CLICK and never the first CLICK. Otherwise the pending CLICK is
// reported once the deadline passes and the line is idle. A confirmed second
// press that reaches LONG_MS wins: the pending single/double is discarded, LONG
// is reported, and LONG_RELEASE keeps its existing meaning.
//
// A pending CLICK is bound to the epoch that armed it. If the epoch moves (UI,
// approval generation, modal ownership, target/session navigation) the pending
// click is discarded and is never replayed against the new UI.
//
// allow_double false (modals/approval/overlay) keeps the original immediate
// CLICK latency: no waiting, no DOUBLE_CLICK.
class ButtonGesture {
public:
    static constexpr uint64_t LONG_MS = 600;
    static constexpr uint64_t DEBOUNCE_MS = 30;
    static constexpr uint64_t DOUBLE_GAP_MS = 300;
    uint64_t epoch = 0;
    // Read-only diagnostics: the debounced state, not the raw GPIO level.
    bool down() const { return down_; }
    // Read-only diagnostics: a CLICK is armed waiting for a possible second tap.
    bool pending_click() const { return pending_; }
    Gesture update(bool raw, uint64_t now, uint64_t current_epoch, bool allow_long,
                   bool allow_double = false) {
        if (pending_ && current_epoch != pending_epoch_)
            pending_ = second_ = false, deadline_ = 0;   // Stale UI ownership.
        if (!allow_double && pending_)
            pending_ = second_ = false, deadline_ = 0;   // Modal took ownership.
        if (raw != raw_) { raw_ = raw; changed_ = now; if (raw) epoch = current_epoch; }
        // Pending CLICK expires only once its gap has elapsed and the line is
        // idle. A raw-down during the gap is a possible second press, not an
        // expiry: its 30 ms debounce is longer than the remaining gap. Once the
        // second press is confirmed the pending pair is owned by it.
        if (pending_ && !second_ && !raw_ && now >= deadline_) {
            pending_ = false; deadline_ = 0;
            return epoch == current_epoch ? Gesture::CLICK : Gesture::NONE;
        }
        if (raw_ != down_ && now - changed_ >= DEBOUNCE_MS) {
            down_ = raw_;
            if (down_) {
                start_ = now; long_ = false;
                // Second confirmed press of a pending pair, still inside the gap.
                if (pending_ && epoch == current_epoch) second_ = true;
            } else {
                if (long_) {
                    second_ = pending_ = false; deadline_ = 0;
                    return Gesture::LONG_RELEASE;
                }
                if (second_) {
                    second_ = pending_ = false; deadline_ = 0;
                    return epoch == current_epoch ? Gesture::DOUBLE_CLICK : Gesture::NONE;
                }
                pending_ = false; deadline_ = 0;
                if (epoch != current_epoch) return Gesture::NONE;
                if (!allow_double) return Gesture::CLICK;
                pending_ = true; pending_epoch_ = current_epoch; deadline_ = now + DOUBLE_GAP_MS;
                return Gesture::NONE;
            }
        }
        if (down_ && raw_ && !long_ && epoch == current_epoch && allow_long && now-start_ >= LONG_MS) {
            long_ = true;
            // LONG wins over a pending single/double pair.
            pending_ = second_ = false; deadline_ = 0;
            return Gesture::LONG;
        }
        return Gesture::NONE;
    }
    // Invalidate a pending CLICK because the owning UI changed (epoch, modal,
    // approval, overlay). Never replayed against the new UI.
    void invalidate_pending() { pending_ = second_ = false; deadline_ = 0; }
private:
    bool raw_ = false, down_ = false, long_ = false, second_ = false, pending_ = false;
    uint64_t changed_ = 0, start_ = 0, deadline_ = 0, pending_epoch_ = 0;
};