#include "stats_buffer.h"

#include <util/generic/xrange.h>
#include <util/system/guard.h>
#include <util/generic/ymath.h>

TStatsBuffer::TStatsBuffer(TInstant start, size_t valuesPerInterval, TDuration interval, TDuration maxStatsAge, TDuration maxWriteAge)
    : MaxStatsAge(maxStatsAge)
    , MaxWriteAge(maxWriteAge)
    , Interval(interval)
    , ValuesPerInterval(valuesPerInterval)
{
    Y_VERIFY(Interval.GetValue() % TInstant::Seconds(1).GetValue() == 0);
    Y_VERIFY(MaxStatsAge.GetValue() % Interval.GetValue() == 0);
    Y_VERIFY(MaxWriteAge.GetValue() % Interval.GetValue() == 0);

    AddInterval(IntervalUpperBoundary(start));
}

bool TStatsBuffer::VisitReadableStats(TInstant currentTime, TInstant readTime, TReadVisitor visitor) const {
    // TODO: Be more explicit about what data we un-const.
    // TODO: Maybe also don't use currentTime (don't Advance() on reads).
    auto this2 = const_cast<TStatsBuffer*>(this); // remove const
    return this2->VisitStats(currentTime, readTime, visitor, [this, &readTime]() {
        return LastIntervalEnd_ - readTime > MaxWriteAge;
    });
}

bool TStatsBuffer::VisitWritableStats(TInstant currentTime, TInstant writeTime, TWriteVisitor visitor) {
    return VisitStats(currentTime, writeTime, visitor, [this, &writeTime]() {
        return LastIntervalEnd_ - writeTime <= MaxWriteAge;
    });
}

bool TStatsBuffer::VisitStats(TInstant currentTime, TInstant time, TWriteVisitor visitor, std::function<bool()> checkTimeRestrictions) {
    TGuard<TMutex> guard(Lock_);
    Advance(currentTime);

    if (time > currentTime || !checkTimeRestrictions())
        return false;

    // counting from the end, starting at 1
    size_t offset = CeilDiv((LastIntervalEnd_ - time).GetValue(), Interval.GetValue());
    if (offset > StatsIntervals_.size())
        return false;

    visitor(StatsIntervals_[StatsIntervals_.size() - offset]);
    return true;
}

void TStatsBuffer::Advance(TInstant currentTime) {
    while (LastIntervalEnd_ <= currentTime) {
        AddInterval();
    }
}

void TStatsBuffer::AddInterval(std::optional<TInstant> intervalEnd) {
    StatsIntervals_.emplace_back(ValuesPerInterval, 0);

    // the case where we were doing initialization
    if (intervalEnd) {
        Y_ASSERT(StatsIntervals_.size() == 1);
        LastIntervalEnd_ = intervalEnd.value();
        return;
    }
    LastIntervalEnd_ += Interval;
    // offsets are counted from the end, starting at 1
    size_t firstReadableOffset =
        (MaxWriteAge.GetValue() / Interval.GetValue()) + 1;
    size_t cleanupOffset =
        (MaxStatsAge.GetValue() / Interval.GetValue()) + 1;

    if (StatsIntervals_.size() > firstReadableOffset) {
        BakeInterval(StatsIntervals_.size() - firstReadableOffset);
    }
    if (StatsIntervals_.size() > cleanupOffset) {
        StatsIntervals_.pop_front();
    }
}

void TStatsBuffer::BakeInterval(size_t pos) {
    Y_VERIFY(pos >= 1);

    // TODO: Disabled as we are currently using IGAUGE instead of RATE
    // (rate does not support timestamps)
    /* auto old = StatsIntervals_[pos - 1].cbegin();
    for (auto& toBake : StatsIntervals_[pos]) {
        toBake += *old++;
    } */
}

TInstant TStatsBuffer::IntervalUpperBoundary(TInstant instant) {
    return TInstant::FromValue(Interval.GetValue() * (instant.GetValue() / Interval.GetValue() + 1));
}
