// streamer.hpp — MJPEG streamer (Linux) with once‑per‑second stats
// ------------------------------------------------------------------
// * Streams the most recent frame at up to `fps` (default 30).
// * Disables Nagle (TCP_NODELAY) for low latency.
// * Prints one concise line per second: pushes‑per‑sec, sends‑per‑sec, average push→send latency.
//   Example:  `[stats] push 15  send 15  lag 7 ms`.
//
#pragma once

#include <opencv2/opencv.hpp>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <mutex>
#include <numeric>
#include <sstream>
#include <string>
#include <thread>
#include <vector>

#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <unistd.h>

class streamer
{
public:
    explicit streamer(uint16_t port = 8081, int fps = 30)
        : port_(port), periodUs_(1'000'000 / std::max(1, fps)),
          lastReport_(std::chrono::steady_clock::now())
    {
        std::cout << "[streamer] start on port " << port_ << ", fps " << fps << '\n';
        srvThread_ = std::thread([this]
                                 { serve(); });
    }

    ~streamer()
    {
        stop_ = true;
        if (srvThread_.joinable())
            srvThread_.join();
        std::cout << "[streamer] stopped\n";
    }

    // Push a raw BGR frame (thread‑safe)
    void push(const cv::Mat &bgr)
    {
        auto now = std::chrono::steady_clock::now();
        lastPushMs_.store(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count());

        std::vector<uchar> jpg;
        cv::imencode(".jpg", bgr, jpg, {cv::IMWRITE_JPEG_QUALITY, 80});
        {
            std::lock_guard<std::mutex> lk(mut_);
            lastJPEG_.swap(jpg);
        }
        pushCount_++;

        // print stats once per second
        if (now - lastReport_ >= std::chrono::seconds(1))
        {
            int pc = pushCount_.exchange(0);
            int sc = sendCount_.exchange(0);
            int lag = 0;
            if (!latencyAcc_.empty())
            {
                lag = std::accumulate(latencyAcc_.begin(), latencyAcc_.end(), 0) / static_cast<int>(latencyAcc_.size());
                latencyAcc_.clear();
            }

            // tempory commented out
            // to avoid too much output in the console
            // std::cout << "[streamer][stats] push=" << pc << "fps, send=" << sc << "fps | lag " << lag << " ms\n";
            lastReport_ = now;
        }
    }

private:
    static bool sendAll(int fd, const void *buf, size_t len)
    {
        const char *p = static_cast<const char *>(buf);
        while (len)
        {
            ssize_t n = send(fd, p, len, MSG_NOSIGNAL);
            if (n <= 0)
                return false;
            p += n;
            len -= n;
        }
        return true;
    }

    void serve()
    {
        int srv = socket(AF_INET, SOCK_STREAM, 0);
        int one = 1;
        setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
        sockaddr_in addr{AF_INET, htons(port_), {INADDR_ANY}};
        bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof addr);
        listen(srv, 10);

        while (!stop_)
        {
            fd_set rfds;
            FD_ZERO(&rfds);
            FD_SET(srv, &rfds);
            timeval tv{0, 100'000};
            if (select(srv + 1, &rfds, nullptr, nullptr, &tv) > 0)
            {
                int cli = accept(srv, nullptr, nullptr);
                if (cli >= 0)
                    std::thread(&streamer::client, this, cli).detach();
            }
        }
        close(srv);
    }

    // ------------------------------------------------------------------ per‑client loop
    void client(int sock)
    {
        int one = 1;
        setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));

        char req[1024];
        ssize_t reqLen = read(sock, req, sizeof req);
        std::string request(req, reqLen > 0 ? static_cast<size_t>(reqLen) : 0);
        std::string path = "/";
        size_t firstSpace = request.find(' ');
        if (firstSpace != std::string::npos)
        {
            size_t secondSpace = request.find(' ', firstSpace + 1);
            if (secondSpace != std::string::npos)
            {
                path = request.substr(firstSpace + 1, secondSpace - firstSpace - 1);
            }
        }

        bool wantsStream = path == "/stream" || path == "/stream.mjpg" || path.find("stream=1") != std::string::npos;
        if (!wantsStream)
        {
            std::ostringstream html;
            html << "<!doctype html><html><head><meta charset=\"utf-8\">"
                 << "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
                 << "<title>OpenDartboard</title><style>"
                 << "html,body{margin:0;height:100%;background:#050607;color:#eef3f5;overflow:hidden;font-family:Arial,sans-serif;}"
                 << "body{display:grid;place-items:start center;}"
                 << ".topbar{position:fixed;top:0;left:0;right:0;z-index:5;height:58px;background:linear-gradient(180deg,rgba(5,6,7,.94),rgba(5,6,7,.62));"
                 << "border-bottom:1px solid rgba(255,255,255,.08);display:flex;align-items:center;justify-content:center;pointer-events:none;}"
                 << ".bar{display:flex;gap:8px;align-items:center;padding:8px 10px;background:rgba(17,22,26,.88);border:1px solid rgba(255,255,255,.16);"
                 << "border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.42);pointer-events:auto;}"
                 << ".state{min-width:78px;color:#c9d2d8;font-size:13px;text-align:center}.ok{color:#46c37b}.bad{color:#e46d6d}"
                 << "button{min-width:82px;border:1px solid #3a4a54;border-radius:6px;padding:8px 11px;background:#24313a;color:#fff;cursor:pointer;font-weight:700}"
                 << "button.start{background:#1f6f43;border-color:#2e965d}button.stop{background:#743033;border-color:#9a4549}"
                 << "button:hover{filter:brightness(1.12)}button:disabled{opacity:.55;cursor:wait;filter:none}"
                 << "img{width:100vw;height:calc(100vh - 58px);margin-top:58px;object-fit:contain;object-position:top center;display:block;}"
                 << "@media(max-width:560px){.topbar{height:96px}.bar{flex-wrap:wrap;justify-content:center}.state{width:100%;min-width:0}img{height:calc(100vh - 96px);margin-top:96px}}"
                 << "</style></head><body>"
                 << "<div class=\"topbar\"><div class=\"bar\"><span id=\"state\" class=\"state\">kollar...</span>"
                 << "<button id=\"start\" class=\"start\" type=\"button\">Starta</button>"
                 << "<button id=\"stop\" class=\"stop\" type=\"button\">Stäng av</button>"
                 << "<button id=\"control\" type=\"button\">Kontroll</button></div></div>"
                 << "<img src=\"/stream.mjpg\" alt=\"OpenDartboard stream\">"
                 << "<script>"
                 << "const base='http://'+location.hostname+':8090';"
                 << "const state=document.getElementById('state'),start=document.getElementById('start'),stop=document.getElementById('stop');"
                 << "function set(d){state.textContent=d.running?'igång':'stoppad';state.className='state '+(d.running?'ok':'bad');start.disabled=!!d.running;stop.disabled=!d.running;}"
                 << "async function stat(){try{const r=await fetch(base+'/status',{cache:'no-store'});set(await r.json());}catch(e){state.textContent='fel';state.className='state bad';}}"
                 << "async function act(a){start.disabled=true;stop.disabled=true;state.textContent=a==='start'?'startar...':'stoppar...';try{await fetch(base+'/'+a,{method:'POST'});}catch(e){}if(a==='stop'){location.href=base;return;}setTimeout(stat,1200);}"
                 << "start.onclick=()=>act('start');stop.onclick=()=>act('stop');document.getElementById('control').onclick=()=>{location.href=base};"
                 << "stat();setInterval(stat,5000);"
                 << "</script></body></html>";

            std::string body = html.str();
            std::ostringstream head;
            head << "HTTP/1.0 200 OK\r\n"
                 << "Cache-Control: no-cache\r\n"
                 << "Content-Type: text/html; charset=utf-8\r\n"
                 << "Content-Length: " << body.size() << "\r\n\r\n";
            sendAll(sock, head.str().c_str(), head.str().size());
            sendAll(sock, body.c_str(), body.size());
            close(sock);
            return;
        }

        static constexpr char hdr[] =
            "HTTP/1.0 200 OK\r\n"
            "Cache-Control: no-cache\r\n"
            "Pragma: no-cache\r\n"
            "Content-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n";
        if (!sendAll(sock, hdr, sizeof(hdr) - 1))
        {
            close(sock);
            return;
        }

        std::vector<uchar> cached;
        auto lastSent = std::chrono::steady_clock::now() - std::chrono::microseconds(periodUs_);

        while (!stop_)
        {
            {
                std::lock_guard<std::mutex> lk(mut_);
                if (!lastJPEG_.empty())
                    cached = lastJPEG_;
            }
            auto now = std::chrono::steady_clock::now();
            if (now - lastSent >= std::chrono::microseconds(periodUs_))
            {
                if (!cached.empty())
                {
                    std::ostringstream head;
                    head << "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
                         << cached.size() << "\r\n\r\n";
                    if (!sendAll(sock, head.str().c_str(), head.str().size()))
                        break;
                    if (!sendAll(sock, cached.data(), cached.size()))
                        break;
                    if (!sendAll(sock, "\r\n", 2))
                        break;

                    sendCount_++;
                    int pushMs = static_cast<int>(lastPushMs_.load());
                    int nowMs = static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count());
                    latencyAcc_.push_back(nowMs - pushMs);
                    lastSent = now;
                }
            }
            std::this_thread::sleep_for(std::chrono::microseconds(1000));
        }
        close(sock);
    }

    // ------------------------------------------------------------------ data members
    uint16_t port_;
    int periodUs_;

    std::atomic<bool> stop_{false};
    std::thread srvThread_;

    std::mutex mut_;
    std::vector<uchar> lastJPEG_;

    // stats
    std::atomic<int> pushCount_{0};
    std::atomic<int> sendCount_{0};
    std::vector<int> latencyAcc_;
    std::atomic<long long> lastPushMs_{0};
    std::chrono::steady_clock::time_point lastReport_;
};
