#pragma once

#include <atomic>
#include <cstddef>
#include <limits>
#include <memory>

/**
 * Lock-free SPSC ring buffer of (time, value) double pairs.
 *
 * The ingest thread calls push(); the render thread reads via read_window()
 * or read_latest().  Capacity is rounded up to the next power-of-two.
 * When full, the oldest samples are silently overwritten.
 *
 * Not safe for multiple simultaneous writers or multiple simultaneous readers.
 */
class SignalBuffer {
public:
    explicit SignalBuffer(size_t capacity)
        : mask_(next_pow2(capacity) - 1)
        , t_(new double[mask_ + 1])
        , v_(new double[mask_ + 1])
    {}

    size_t capacity() const noexcept { return mask_ + 1; }

    /** Total samples ever written (monotonically increasing). */
    size_t total_written() const noexcept {
        return head_.load(std::memory_order_acquire);
    }

    /** Number of valid samples (min(total_written, capacity)). */
    size_t count() const noexcept {
        const size_t h = head_.load(std::memory_order_acquire);
        return h < capacity() ? h : capacity();
    }

    /** Timestamp of the most recently pushed sample, or 0 if empty. */
    double latest_time() const noexcept {
        const size_t h = head_.load(std::memory_order_acquire);
        if (h == 0) return 0.0;
        return t_[(h - 1) & mask_];
    }

    /** Timestamp of the oldest valid sample, or 0 if empty. */
    double oldest_time() const noexcept {
        const size_t h = head_.load(std::memory_order_acquire);
        if (h == 0) return 0.0;
        const size_t n      = (h < capacity()) ? h : capacity();
        const size_t oldest = h - n;
        return t_[oldest & mask_];
    }

    // ── Writer interface (ingest thread) ─────────────────────────────

    void push(double t, double v) noexcept {
        const size_t h = head_.load(std::memory_order_relaxed);
        t_[h & mask_] = t;
        v_[h & mask_] = v;
        head_.store(h + 1, std::memory_order_release);
    }

    void push_bulk(const double* ts, const double* vs, size_t n) noexcept {
        for (size_t i = 0; i < n; ++i) push(ts[i], vs[i]);
    }

    // ── Reader interface (render thread) ─────────────────────────────

    /**
     * Copy samples with time in [t_min, t_max] into out_t/out_v.
     * Both arrays must have room for max_pts elements.
     * Returns number of samples copied.
     * Assumes timestamps are monotonically non-decreasing.
     *
     * Uses binary search to find the window start — O(log N + M) instead
     * of the previous O(N) linear scan, where M = samples in range.
     */
    size_t read_window(double t_min, double t_max,
                       double* out_t, double* out_v,
                       size_t max_pts) const noexcept {
        if (max_pts == 0) return 0;
        const size_t h = head_.load(std::memory_order_acquire);
        if (h == 0) return 0;

        const size_t n      = (h < capacity()) ? h : capacity();
        const size_t oldest = h - n;

        // Binary search for first logical index i where t_[...] >= t_min
        size_t lo = 0, hi = n;
        while (lo < hi) {
            const size_t mid = lo + (hi - lo) / 2;
            if (t_[(oldest + mid) & mask_] < t_min)
                lo = mid + 1;
            else
                hi = mid;
        }

        // Linear scan from lo to t_max
        size_t out = 0;
        for (size_t i = lo; i < n && out < max_pts; ++i) {
            const size_t idx = (oldest + i) & mask_;
            const double t   = t_[idx];
            if (t > t_max) break;
            out_t[out] = t;
            out_v[out] = v_[idx];
            ++out;
        }
        return out;
    }

    /**
     * Like read_window but strides through the data so at most max_pts samples
     * are returned, uniformly distributed across [t_min, t_max].
     *
     * When the window contains more than max_pts raw samples, only every
     * ceil(total/max_pts)-th sample is copied.  This ensures the output covers
     * the full time range even for very dense signals, instead of returning
     * only the first max_pts samples from t_min.
     *
     * Complexity: O(log N + total/stride) where total = samples in window.
     */
    size_t read_window_decimated(double t_min, double t_max,
                                 double* out_t, double* out_v,
                                 size_t max_pts) const noexcept {
        if (max_pts == 0) return 0;
        const size_t h = head_.load(std::memory_order_acquire);
        if (h == 0) return 0;

        const size_t n      = (h < capacity()) ? h : capacity();
        const size_t oldest = h - n;

        // Binary search for window start
        size_t lo = 0, hi = n;
        while (lo < hi) {
            const size_t mid = lo + (hi - lo) / 2;
            if (t_[(oldest + mid) & mask_] < t_min) lo = mid + 1;
            else hi = mid;
        }
        const size_t start = lo;

        // Binary search for window end
        lo = start; hi = n;
        while (lo < hi) {
            const size_t mid = lo + (hi - lo) / 2;
            if (t_[(oldest + mid) & mask_] <= t_max) lo = mid + 1;
            else hi = mid;
        }
        const size_t end = lo;

        const size_t total = end - start;
        if (total == 0) return 0;

        const size_t stride = (total > max_pts) ? (total + max_pts - 1) / max_pts : 1;
        size_t out = 0;
        for (size_t i = start; i < end && out < max_pts; i += stride) {
            const size_t idx = (oldest + i) & mask_;
            out_t[out] = t_[idx];
            out_v[out] = v_[idx];
            ++out;
        }
        return out;
    }

    /**
     * Count samples in [t_min, t_max] without copying any data. O(log N).
     */
    size_t count_in_window(double t_min, double t_max) const noexcept {
        const size_t h = head_.load(std::memory_order_acquire);
        if (h == 0) return 0;
        const size_t n      = (h < capacity()) ? h : capacity();
        const size_t oldest = h - n;

        size_t lo = 0, hi = n;
        while (lo < hi) {
            const size_t mid = lo + (hi - lo) / 2;
            if (t_[(oldest + mid) & mask_] < t_min) lo = mid + 1;
            else hi = mid;
        }
        const size_t start = lo;

        lo = start; hi = n;
        while (lo < hi) {
            const size_t mid = lo + (hi - lo) / 2;
            if (t_[(oldest + mid) & mask_] <= t_max) lo = mid + 1;
            else hi = mid;
        }
        return lo - start;
    }

    /**
     * Min-max decimation: divide [t_min, t_max] into n_buckets equal time
     * buckets, and for each bucket record the min and max value.
     *
     * out_t/out_min/out_max must have room for n_buckets elements.
     * Empty buckets (no samples) are compacted away.
     * Returns the number of filled buckets written.
     *
     * Complexity: O(log N + M) where M = samples in window.
     */
    size_t read_window_minmax(double t_min, double t_max,
                               double* out_t, double* out_min, double* out_max,
                               size_t n_buckets) const noexcept {
        if (n_buckets == 0 || t_max <= t_min) return 0;
        const size_t h = head_.load(std::memory_order_acquire);
        if (h == 0) return 0;
        const size_t n      = (h < capacity()) ? h : capacity();
        const size_t oldest = h - n;

        // Binary search: window start
        size_t lo = 0, hi = n;
        while (lo < hi) {
            const size_t mid = lo + (hi - lo) / 2;
            if (t_[(oldest + mid) & mask_] < t_min) lo = mid + 1;
            else hi = mid;
        }
        const size_t start = lo;

        // Binary search: window end
        lo = start; hi = n;
        while (lo < hi) {
            const size_t mid = lo + (hi - lo) / 2;
            if (t_[(oldest + mid) & mask_] <= t_max) lo = mid + 1;
            else hi = mid;
        }
        const size_t end = lo;
        if (start >= end) return 0;

        const double bucket_width = (t_max - t_min) / static_cast<double>(n_buckets);
        constexpr double kBig  = std::numeric_limits<double>::max();
        constexpr double kSmall = std::numeric_limits<double>::lowest();

        // Initialise buckets
        for (size_t b = 0; b < n_buckets; ++b) {
            out_t[b]   = t_min + (static_cast<double>(b) + 0.5) * bucket_width;
            out_min[b] = kBig;
            out_max[b] = kSmall;
        }

        // Scan: assign each sample to its bucket
        for (size_t i = start; i < end; ++i) {
            const size_t idx = (oldest + i) & mask_;
            const double t   = t_[idx];
            const double v   = v_[idx];
            size_t b = static_cast<size_t>((t - t_min) / bucket_width);
            if (b >= n_buckets) b = n_buckets - 1;
            if (v < out_min[b]) out_min[b] = v;
            if (v > out_max[b]) out_max[b] = v;
        }

        // Compact: remove buckets with no samples
        size_t out = 0;
        for (size_t b = 0; b < n_buckets; ++b) {
            if (out_min[b] == kBig) continue;
            out_t[out]   = out_t[b];
            out_min[out] = out_min[b];
            out_max[out] = out_max[b];
            ++out;
        }
        return out;
    }

    /**
     * Copy the n most-recent samples into out_t/out_v (pre-allocated to n).
     * Returns actual count copied (may be < n if buffer not full).
     */
    size_t read_latest(size_t n, double* out_t, double* out_v) const noexcept {
        const size_t h     = head_.load(std::memory_order_acquire);
        const size_t avail = (h < capacity()) ? h : capacity();
        const size_t count = (n < avail) ? n : avail;
        const size_t start = h - count;

        for (size_t i = 0; i < count; ++i) {
            const size_t idx = (start + i) & mask_;
            out_t[i] = t_[idx];
            out_v[i] = v_[idx];
        }
        return count;
    }

private:
    static size_t next_pow2(size_t v) noexcept {
        if (v == 0) return 1;
        v--;
        v |= v >> 1; v |= v >> 2; v |= v >> 4;
        v |= v >> 8; v |= v >> 16; v |= v >> 32;
        return v + 1;
    }

    size_t mask_;
    std::unique_ptr<double[]> t_;
    std::unique_ptr<double[]> v_;
    std::atomic<size_t> head_{0};
};
