#pragma once

#include "metrics/MetricSample.hpp"

#include <chrono>
#include <cstddef>
#include <stdexcept>
#include <vector>

namespace tw::metrics {

/**
 * Ring of samples, one bucket per `Interval` of elapsed time.
 *
 * Values pushed during the same interval fold into one bucket, and intervals
 * that pass without a value become empty buckets, so the distance between two
 * buckets always matches the time between them. Once `capacity` buckets are
 * held the oldest one is dropped.
 */
template<typename Interval, typename Clock = std::chrono::steady_clock>
class MetricSeries {
public:
    using TimePoint = typename Clock::time_point;

private:
    std::vector<MetricSample> m_buckets;

    /** Index of the newest bucket, in `Interval` units since the clock epoch. */
    int64_t m_newest = 0;

    /** Buckets holding data, counted back from the newest. */
    size_t m_count = 0;

    static int64_t bucket_of(TimePoint time) {
        return (int64_t)std::chrono::floor<Interval>(time).time_since_epoch().count();
    }

    size_t slot_of(int64_t index) const {
        int64_t size = (int64_t)m_buckets.size();
        int64_t slot = index % size;

        return (size_t)(slot < 0 ? slot + size : slot);
    }

    MetricSample& bucket_at(int64_t index) {
        return m_buckets[slot_of(index)];
    }

    const MetricSample& bucket_at(int64_t index) const {
        return m_buckets[slot_of(index)];
    }

    /**
     * Moves the newest bucket up to `index`, emptying every bucket the gap
     * covers. A gap wider than the ring empties all of it.
     */
    void advance_to(int64_t index) {
        int64_t steps    = index - m_newest;
        int64_t capacity = (int64_t)m_buckets.size();
        int64_t to_clear = std::min(steps, capacity);

        for(int64_t i = 0; i < to_clear; i++) {
            bucket_at(index - i).reset();
        }

        m_newest = index;
        m_count  = steps >= capacity
            ? m_buckets.size()
            : std::min(m_count + (size_t)steps, m_buckets.size());
    }

public:
    explicit MetricSeries(size_t capacity) :
        m_buckets(capacity)
    {
        if(capacity == 0) {
            throw std::invalid_argument("`capacity` must hold at least one bucket");
        }
    }

    size_t capacity() const {
        return m_buckets.size();
    }

    size_t size() const {
        return m_count;
    }

    bool is_empty() const {
        return m_count == 0;
    }

    /** Index of the newest bucket, in `Interval` units since the clock epoch. */
    int64_t newest_index() const {
        return m_newest;
    }

    void push(double value) {
        push(value, Clock::now());
    }

    /**
     * Adds `value` to the bucket `at` falls into. A value older than every
     * bucket still held is dropped.
     */
    void push(double value, TimePoint at) {
        int64_t index = bucket_of(at);

        if(m_count == 0) {
            m_newest = index;
            m_count  = 1;

            bucket_at(index).reset();
            bucket_at(index).add(value);
            return;
        }

        if(index > m_newest) {
            advance_to(index);
        } else if(m_newest - index >= (int64_t)m_count) {
            return;
        }

        bucket_at(index).add(value);
    }

    /** Newest first: age 0 is the bucket currently being filled. */
    const MetricSample& at_age(size_t age) const {
        return bucket_at(m_newest - (int64_t)age);
    }

    /** Aggregate of the newest `buckets` buckets. */
    MetricSample window(size_t buckets) const {
        MetricSample result;

        size_t count = std::min(buckets, m_count);
        for(size_t age = 0; age < count; age++) {
            result.merge(at_age(age));
        }

        return result;
    }

    /** Aggregate of everything still held. */
    MetricSample window() const {
        return window(m_count);
    }

    /**
     * Writes the newest `buckets` buckets into `xs` and `ys` oldest first, as
     * two contiguous arrays. `xs` holds the age of each bucket in `Interval`
     * units, so the bucket being filled sits at 0 and older ones run negative.
     * Both vectors are resized to the number of points written.
     *
     * `skip_newest` leaves that many of the newest buckets out. The bucket
     * being filled only holds the part of its interval that has elapsed, so
     * reading it next to whole ones makes the newest point dip and recover;
     * skipping it keeps every point covering the same span of time. Ages stay
     * true, so a skipped bucket leaves a gap rather than shifting the rest.
     */
    size_t linearize(std::vector<double>& xs,
                     std::vector<double>& ys,
                     MetricField          field,
                     size_t               buckets,
                     size_t               skip_newest = 0) const {
        size_t available = m_count > skip_newest ? m_count - skip_newest : 0;
        size_t count     = std::min(buckets, available);

        xs.resize(count);
        ys.resize(count);

        for(size_t i = 0; i < count; i++) {
            size_t age = skip_newest + count - 1 - i;

            xs[i] = -(double)age;
            ys[i] = value_of(at_age(age), field);
        }

        return count;
    }

    size_t linearize(std::vector<double>& xs,
                     std::vector<double>& ys,
                     MetricField          field) const {
        return linearize(xs, ys, field, m_count);
    }

    void clear() {
        m_count = 0;
    }
};

}
