/**
 * @file App.h
 * @brief Top-level application state and frame dispatch.
 */

#pragma once

#include "WSClient.h"
#include "Protocol.h"
#include "SignalBuffer.h"

#include <string>
#include <vector>
#include <memory>
#include <mutex>
#include <cstdint>

#include "imgui.h"

namespace StreamHubClient {

/*---------------------------------------------------------------------------*/
/* Domain types                                                               */
/*---------------------------------------------------------------------------*/

/** One signal as known to the client. */
struct Signal {
    SignalMeta   meta;
    SignalBuffer buf{1000000};
    ImVec4       color{0.4f, 0.8f, 1.0f, 1.0f};
    float        lineWidth = 1.5f;
    int          marker    = -1;   /* ImPlotMarker (-1 = none) */
    bool         visible   = true;
};

/** One connected UDPStreamer source. */
struct Source {
    std::string         id;
    std::string         label;
    std::string         addr;
    std::string         state      = "disconnected";
    uint32_t            port       = 0;
    bool                configured = false;
    int                 publishMode = 0;
    std::vector<Signal> signals;
    SourceStats         stats;
};

/** Trigger configuration and live state (hub-side trigger semantics). */
struct TriggerState {
    /* Config (sent to the hub via setTrigger) */
    std::string signalKey;            /* full key "src:sig" or "src:sig[i]" */
    int         edge        = 0;      /* 0=rising 1=falling 2=both */
    double      threshold   = 0.0;
    double      windowSec   = 0.1;    /* 100 µs .. 10 s */
    double      prePercent  = 20.0;
    bool        single      = false;  /* false=normal true=single */

    /* Live (driven by hub triggerState broadcasts) */
    std::string status      = "idle"; /* idle|armed|collecting|triggered */
    bool        stopped     = false;
    bool        hasTrigTime = false;
    double      trigTime    = 0.0;
};

/** Per-signal vertical scale state (oscilloscope style). */
struct VScale {
    int    mode      = 0;    /* 0=auto  1=range  2=manual */
    double divValue  = 1.0;  /* units per division (manual mode) */
    double offset    = 0.0;  /* raw value at center (manual mode) */
    double screenPos = 0.0;  /* position offset in divisions from center */
    bool   digitalInMixed = false; /* quantize this trace in mixed plot mode */
    /* Resolved each frame (read-only): */
    double resolvedDiv    = 1.0;
    double resolvedOffset = 0.0;
};

/** Assignment of one signal to a plot panel. */
struct PlotAssignment {
    int    sourceIdx = -1;
    int    signalIdx = -1;
    VScale vs;
};

/*---------------------------------------------------------------------------*/
/* Plot layouts                                                               */
/*---------------------------------------------------------------------------*/

/* Layout set mirrors the web UI: cols×rows */
enum class PlotLayout {
    kLayout1x1 = 0,
    kLayout2x1,   /* 2 side by side */
    kLayout1x2,   /* 2 stacked */
    kLayout3x1,
    kLayout1x3,
    kLayout2x2,
    kLayout4x1,
    kLayout1x4,
    kCount
};

static inline const char* const PlotLayoutNames[] = {
    "1×1", "2×1", "1×2", "3×1", "1×3", "2×2", "4×1", "1×4"
};

static inline void PlotLayoutDims(PlotLayout l, int& cols, int& rows) {
    switch (l) {
        case PlotLayout::kLayout1x1: cols=1; rows=1; break;
        case PlotLayout::kLayout2x1: cols=2; rows=1; break;
        case PlotLayout::kLayout1x2: cols=1; rows=2; break;
        case PlotLayout::kLayout3x1: cols=3; rows=1; break;
        case PlotLayout::kLayout1x3: cols=1; rows=3; break;
        case PlotLayout::kLayout2x2: cols=2; rows=2; break;
        case PlotLayout::kLayout4x1: cols=4; rows=1; break;
        case PlotLayout::kLayout1x4: cols=1; rows=4; break;
        default:                     cols=1; rows=1; break;
    }
}

/*---------------------------------------------------------------------------*/
/* App                                                                        */
/*---------------------------------------------------------------------------*/

class App {
public:
    App();
    ~App();

    /** @brief Initialise and start the WebSocket connection. */
    void init(const std::string& host, uint16_t port);

    /**
     * @brief Call once per ImGui frame.
     * Drains the WS receive queue, dispatches messages, renders all panels.
     */
    void update();

    /** @brief Disconnect and clean up. */
    void shutdown();

    /* ---- Accessors used by sub-panels ----------------------------------- */

    std::vector<Source>&  sources()       { return sources_; }
    const std::vector<Source>& sources() const { return sources_; }
    TriggerState&         trigger()       { return trigger_; }
    WSClient&             ws()            { return ws_; }
    uint32_t              maxPoints()     const { return maxPoints_; }

    /** @brief Last trigger capture received from the hub (nullptr if none). */
    const CaptureFrame*   capture() const { return hasCapture_ ? &capture_ : nullptr; }
    void clearCapture() { hasCapture_ = false; }

    /** @brief Get the plot assignment vector for plot index i. */
    std::vector<PlotAssignment>& plotSlot(int i) { return plotSlots_[i]; }
    int numPlotSlots() const;

    PlotLayout plotLayout() const { return layout_; }
    void setLayout(PlotLayout l);

    /** @brief True if all plots are paused (global pause). */
    bool globalPaused() const { return globalPaused_; }
    void setGlobalPaused(bool p) { globalPaused_ = p; }

    /** @brief Live-follow state per plot (true = auto-scroll with newest data). */
    bool& liveFollow(int i) { return liveFollow_[i]; }

    /** @brief Which slot index is "active" (selected) in the given plot (-1 = none). */
    int& activeSlot(int i) { return activeSlot_[i]; }

    /** @brief Time window width in seconds used by live scroll. */
    double  windowSec() const  { return windowSec_; }
    void setWindowSec(double s){ if (s >= 1e-4 && s <= 3600.0) windowSec_ = s; }

    /** @brief Stored X range for non-live (zoom/pan) mode per plot. */
    double plotXMin(int i) const { return plotXMin_[i]; }
    double plotXMax(int i) const { return plotXMax_[i]; }
    void   setPlotX(int i, double mn, double mx) {
        if (mx > mn + 1e-9) { plotXMin_[i] = mn; plotXMax_[i] = mx; }
    }
    /** @brief Call when transitioning live→non-live to seed the stored X range. */
    void initPlotX(int i, double tMax) {
        plotXMin_[i] = tMax - windowSec_;
        plotXMax_[i] = tMax;
    }

    /** @brief Per-plot vertical normalisation: 0=normal 1=digital 2=mixed. */
    int& plotVMode(int i) { return plotVMode_[i]; }

    /* ---- Cursors A/B (global: shared & synchronised across all plots) ---- */
    bool&   cursorsOn() { return cursorsOn_; }
    double& cursorA()   { return cursorA_; }
    double& cursorB()   { return cursorB_; }

    /* ---- Paused view snapshot (per plot) ---------------------------------- *
     * Double-buffer scheme: the per-signal ring buffers stay "active" (always
     * written by the WS thread-drain); a paused plot freezes a "view" copy of
     * its slots' data at pause time and renders from it, so the picture does
     * not drain away while the rings keep rolling. */
    struct PlotSnapshot {
        bool valid = false;
        std::vector<std::string>         keys;  /* slot key "src:sig" */
        std::vector<std::vector<double>> t, v;
    };
    PlotSnapshot& plotSnap(int i) { return plotSnap_[i]; }

    /* ---- Zoom history (per plot) ----------------------------------------- */
    std::vector<std::pair<double,double>>& zoomHist(int i) { return zoomHist_[i]; }
    /** @brief Push the current stored X range onto the history (capped). */
    void pushZoomHist(int i) {
        auto& h = zoomHist_[i];
        if (!h.empty() && h.back().first == plotXMin_[i] &&
            h.back().second == plotXMax_[i]) { return; }
        if (h.size() >= 64) { h.erase(h.begin()); }
        h.emplace_back(plotXMin_[i], plotXMax_[i]);
    }

    /* ---- Hi-res WS zoom (per plot) ---------------------------------------- */
    struct PlotZoomCache {
        bool     valid   = false;  /* signals[] match [t0,t1] */
        bool     pending = false;  /* request in flight */
        uint32_t reqId   = 0;
        double   t0 = 0.0, t1 = 0.0;          /* range of valid data */
        double   reqT0 = 0.0, reqT1 = 0.0;    /* range of pending request */
        std::vector<ZoomSignal> signals;
    };
    PlotZoomCache& zoomCache(int i) { return zoomCache_[i]; }

    /** @brief Send a hub zoom request for plot i over [t0,t1] (2400 pts). */
    void requestZoom(int plotIdx, double t0, double t1,
                     const std::string& signalsCsv);

    /** @brief Full key "src:sig" for an assignment (empty if invalid). */
    std::string slotKey(const PlotAssignment& a) const;

    /* ---- History ------------------------------------------------------------ */

    /** @brief History info received from the hub. */
    const HistoryInfoMsg& historyInfo() const { return historyInfo_; }

    /** @brief Send a historyZoom request. */
    void requestHistoryZoom(int plotIdx, double t0, double t1,
                            const std::string& signalsCsv);

    /** @brief Hi-res history zoom cache (per plot, parallel to zoomCache_). */
    PlotZoomCache& histZoomCache(int i) { return histZoomCache_[i]; }

    /* ---- Trigger zoom (per plot, used in trigger capture view) ----------- */
    bool&  trigZoomed(int i) { return trigZoomed_[i]; }
    void   resetTrigZoom()   { for (int i = 0; i < kMaxPlotSlots; i++) trigZoomed_[i] = false; }

    /** @brief True if the stats panel is open. */
    bool& showStats()    { return showStats_; }
    bool& showAddSrc()   { return showAddSrc_; }
    bool& showTrigBar()  { return showTrigBar_; }
    bool& showHistBar()  { return showHistBar_; }

    /** @brief Find source index by id (-1 if not found). */
    int findSource(const std::string& id) const;

    /** @brief Find signal index in source by name (-1 if not found). */
    int findSignal(const Source& src, const std::string& name) const;

private:
    /* ---- WS message dispatch ------------------------------------------- */
    void handleJSON(const std::string& json);
    void handleBinary(const uint8_t* data, size_t len);

    void onSources(const std::string& json);
    void onConfig(const std::string& json);
    void onStats(const std::string& json);
    void onTriggerState(const std::string& json);
    void onZoom(const std::string& json);
    void onHistoryZoom(const std::string& json);
    void onHistoryInfo(const std::string& json);
    void onMaxPointsUpdated(const std::string& json);

    /* ---- Rendering ------------------------------------------------------ */
    void renderToolbar();
    void renderHistoryBar();
    void renderTriggerBar();
    void renderSidebar();
    void renderPlotGrid();
    void renderStatsModal();
    void renderAddSourceModal();

    /* ---- State ---------------------------------------------------------- */
    WSClient             ws_;
    std::vector<Source>  sources_;
    mutable std::mutex   srcMutex_;   /* protects sources_ */

    TriggerState         trigger_;
    PlotLayout           layout_     = PlotLayout::kLayout1x1;
    uint32_t             maxPoints_  = 1000000u;
    bool                 globalPaused_ = false;

    /* Plot slots: one vector of assignments per plot panel */
    static const int kMaxPlotSlots = 8;
    std::vector<PlotAssignment> plotSlots_[kMaxPlotSlots];
    bool  plotPaused_[kMaxPlotSlots]    = {};
    bool  liveFollow_[kMaxPlotSlots]    = {};   /* true = live-scroll X axis */
    int   activeSlot_[kMaxPlotSlots];           /* which slot is "active" (-1 = none) */
    double windowSec_                    = 10.0; /* live scroll window width */
    double plotXMin_[kMaxPlotSlots]     = {};   /* stored X min for non-live mode */
    double plotXMax_[kMaxPlotSlots]     = {};   /* stored X max for non-live mode */
    int    plotVMode_[kMaxPlotSlots]    = {};   /* 0=normal 1=digital 2=mixed */

    /* Cursors (global) */
    bool   cursorsOn_ = false;
    double cursorA_   = 0.0;
    double cursorB_   = 0.0;

    /* Trigger zoom per plot (capture view zoom state) */
    bool   trigZoomed_[kMaxPlotSlots] = {};

    /* Frozen view per plot (valid while the plot is paused) */
    PlotSnapshot plotSnap_[kMaxPlotSlots];

    /* Zoom history + hi-res zoom cache */
    std::vector<std::pair<double,double>> zoomHist_[kMaxPlotSlots];
    PlotZoomCache zoomCache_[kMaxPlotSlots];
    PlotZoomCache histZoomCache_[kMaxPlotSlots];
    uint32_t      nextZoomReqId_ = 1;

    /* History info from the hub */
    HistoryInfoMsg historyInfo_;
    double         lastHistInfoReq_ = 0.0;

    /* UI state */
    bool showStats_   = false;
    bool showAddSrc_  = false;
    bool showTrigBar_ = false;
    bool showHistBar_ = false;
    bool sidebarOpen_ = true;

    /* Connection fields (populated by init(), editable in menu bar) */
    char hostBuf_[64] = "127.0.0.1";
    char portBuf_[8]  = "8090";

    /* Track connect transition to send initial queries */
    bool prevConnected_ = false;

    /* Last trigger capture (version=2 binary frame; render thread only) */
    CaptureFrame   capture_;
    bool           hasCapture_ = false;
};

} /* namespace StreamHubClient */
