#include <Timeout.h>
#include <sys/time.h>
#include <queue>

using namespace std::chrono_literals;

static auto earlier = [](std::weak_ptr<Timeout> a, std::weak_ptr<Timeout> b) {
    auto as = a.lock();
    auto bs = b.lock();
    if (!as) return true;
    if (!bs) return false;
    return as->absoluteTimeout > bs->absoluteTimeout;
};

static std::weak_ptr<Timeout> currentTimeout;
static std::priority_queue<
        std::weak_ptr<Timeout>,
        std::vector<std::weak_ptr<Timeout>>,
        decltype(earlier)
    > timeouts(earlier);


static struct itimerval toITV(std::chrono::microseconds time) {
    return {{0, 0},
            {std::chrono::duration_cast<std::chrono::seconds>(time).count(),
             static_cast<suseconds_t>((time % 1s).count())}};
}

Timeout::Timeout(std::chrono::microseconds time, std::function<void(void)> handler) :
    _handler(handler), absoluteTimeout(Clock::now()+time)
{}

void Timeout::sigAlarm(siginfo_t*) {
    auto cur = currentTimeout.lock();
    if (cur) {
        cur->_handler();
    }

    programNext();
}

void Timeout::programNext() {
    std::shared_ptr<Timeout> next = nullptr;
    while (next == nullptr) {
        if (timeouts.empty()) {
            currentTimeout.reset();
            return;
        }
        next = timeouts.top().lock();
        timeouts.pop();
    }
    currentTimeout = next;
    struct itimerval t = toITV(
        std::chrono::duration_cast<std::chrono::microseconds>(next->absoluteTimeout-Clock::now()));
    //std::cerr << "Set timeout to: " << t.it_value.tv_sec << "s and " << t.it_value.tv_usec << "us" << std::endl;
    setitimer(ITIMER_REAL, &t, nullptr);
}

std::shared_ptr<Timeout> Timeout::programTimeout(std::chrono::microseconds time,
                                        std::function<void(void)> handler) {
    std::shared_ptr<Timeout> ret(new Timeout(time,handler));
    auto cur = currentTimeout.lock();

    if (!cur) {
        //cur expired or is not set
        timeouts.push(ret);
        programNext();
    } else {
        if (cur->absoluteTimeout > ret->absoluteTimeout) { //New one earlier?
            timeouts.push(cur); //We switch timeouts ...
            struct itimerval t = toITV(time);
            setitimer(ITIMER_REAL, &t, nullptr);
        } else {
            timeouts.push(ret);
        }
    }

    return ret;
}
