#pragma once

#include "metrics/MetricSeries.hpp"

#include <chrono>
#include <cstdint>

namespace tw::dbg {

/**
 * Per-second history of what the client sends, receives and waits for.
 *
 * Traffic arrives as running totals, one reading per tick: sample() keeps the
 * change since the previous reading, so a bucket sums to the traffic of that
 * second and its extremes are the quietest and busiest tick within it.
 * Durations are recorded as they are measured.
 */
class NetworkMetrics {
public:
    using Interval = std::chrono::seconds;
    using Series   = metrics::MetricSeries<Interval>;

    /** How many seconds of history are kept. */
    static constexpr size_t DEFAULT_HISTORY = 300;

    /** Running totals as of one tick. */
    struct Totals {
        uint64_t bytes_sent        = 0;
        uint64_t bytes_received    = 0;
        uint64_t messages_sent     = 0;
        uint64_t messages_received = 0;
    };

private:
    Series m_bytes_out;
    Series m_bytes_in;
    Series m_messages_out;
    Series m_messages_in;
    Series m_response_ms;
    Series m_update_ms;

    Series m_rollbacks;
    Series m_correction_distance;
    Series m_ack_lag_frames;
    Series m_replayed_frames;

    Totals m_previous;
    bool   m_has_previous = false;

    static uint64_t delta(uint64_t current, uint64_t previous) {
        return current > previous ? current - previous : 0;
    }

    static double to_millis(std::chrono::nanoseconds elapsed) {
        return std::chrono::duration<double, std::milli>(elapsed).count();
    }

public:
    explicit NetworkMetrics(size_t history_in_seconds = DEFAULT_HISTORY) :
        m_bytes_out(history_in_seconds),
        m_bytes_in(history_in_seconds),
        m_messages_out(history_in_seconds),
        m_messages_in(history_in_seconds),
        m_response_ms(history_in_seconds),
        m_update_ms(history_in_seconds),
        m_rollbacks(history_in_seconds),
        m_correction_distance(history_in_seconds),
        m_ack_lag_frames(history_in_seconds),
        m_replayed_frames(history_in_seconds)
    { }

    /**
     * Records how much `totals` grew since the previous call. The first call
     * only remembers where the counters started.
     */
    void sample(const Totals& totals) {
        if(m_has_previous) {
            m_bytes_out.push((double)delta(totals.bytes_sent, m_previous.bytes_sent));
            m_bytes_in.push((double)delta(totals.bytes_received, m_previous.bytes_received));
            m_messages_out.push((double)delta(totals.messages_sent, m_previous.messages_sent));
            m_messages_in.push((double)delta(totals.messages_received, m_previous.messages_received));
        }

        m_previous     = totals;
        m_has_previous = true;
    }

    /** Time between sending an input and seeing the answer to it. */
    void record_response_time(std::chrono::nanoseconds elapsed) {
        m_response_ms.push(to_millis(elapsed));
    }

    /** Time one tick spent moving messages in and out, handlers included. */
    void record_update_time(std::chrono::nanoseconds elapsed) {
        m_update_ms.push(to_millis(elapsed));
    }

    const Series& bytes_out() const {
        return m_bytes_out;
    }

    const Series& bytes_in() const {
        return m_bytes_in;
    }

    const Series& messages_out() const {
        return m_messages_out;
    }

    const Series& messages_in() const {
        return m_messages_in;
    }

    const Series& response_ms() const {
        return m_response_ms;
    }

    const Series& update_ms() const {
        return m_update_ms;
    }

    /** Records a rollback event (one sample per rollback). */
    void record_rollback() {
        m_rollbacks.push(1.0);
    }

    /** Records the distance in meters of a position correction. */
    void record_correction_distance(double meters) {
        m_correction_distance.push(meters);
    }

    /** Records how many frames behind the ack is trailing the current frame. */
    void record_ack_lag(uint32_t frames) {
        m_ack_lag_frames.push((double)frames);
    }

    /** Records how many frames were replayed during a rollback. */
    void record_replayed_frames(uint32_t frames) {
        m_replayed_frames.push((double)frames);
    }

    const Series& rollbacks() const {
        return m_rollbacks;
    }

    const Series& correction_distance() const {
        return m_correction_distance;
    }

    const Series& ack_lag_frames() const {
        return m_ack_lag_frames;
    }

    const Series& replayed_frames() const {
        return m_replayed_frames;
    }
};

}
