#ifdef _WIN32
    #ifndef NOMINMAX
        #define NOMINMAX
    #endif
#endif

#include <windows.h>

#ifdef _MSC_VER
    #pragma warning(push)
    #pragma warning(disable: 4995)
#endif
#include "x502api.h"
#include "e502api.h"
#ifdef _MSC_VER
    #pragma warning(pop)
#endif

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <deque>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

namespace {

constexpr uint32_t kLogicalChannelCount = 2;
constexpr uint32_t kStartInMask = (static_cast<uint32_t>(1U) << 15U);

struct Config {
    std::string serial;

    uint32_t mode = X502_LCH_MODE_DIFF;
    uint32_t range = X502_ADC_RANGE_5;
    uint32_t ch1 = 2;
    uint32_t ch2 = 3;

    uint32_t clock_mode = X502_SYNC_DI_SYN1_RISE;

    double clock_hz = 2000000.0;
    double gate_hz = 20000.0;

    uint32_t windows = 1000;
    uint32_t plot_windows = 50;

    uint32_t recv_block_words = 8192;
    uint32_t recv_timeout_ms = 50;
    uint32_t clock_wait_ms = 5000;
    uint32_t window_wait_ms = 5000;
    uint32_t input_buffer_words = 262144;
    uint32_t input_step_words = 8192;

    bool pullup_syn1 = false;
    bool pulldown_start_in = false;

    std::string csv_path = "gate_capture.csv";
    std::string svg_path = "gate_capture.svg";
};

struct Frame {
    double ch1 = 0.0;
    double ch2 = 0.0;
};

struct WindowData {
    std::vector<double> ch1;
    std::vector<double> ch2;
};

struct AverageTrace {
    std::vector<double> ch1;
    std::vector<double> ch2;
    std::vector<uint32_t> counts;
};

[[noreturn]] void fail(const std::string& message) {
    throw std::runtime_error(message);
}

std::string trim_copy(const std::string& text) {
    const auto first = text.find_first_not_of(" \t\r\n");
    if (first == std::string::npos) {
        return {};
    }
    const auto last = text.find_last_not_of(" \t\r\n");
    return text.substr(first, last - first + 1);
}

bool starts_with(const std::string& value, const std::string& prefix) {
    return value.rfind(prefix, 0) == 0;
}

uint32_t parse_u32(const std::string& text, const std::string& field_name) {
    const std::string clean = trim_copy(text);
    char* end = nullptr;
    const auto value = std::strtoull(clean.c_str(), &end, 0);
    if ((end == clean.c_str()) || (*end != '\0') || (value > std::numeric_limits<uint32_t>::max())) {
        fail("Invalid integer for " + field_name + ": " + text);
    }
    return static_cast<uint32_t>(value);
}

double parse_double(const std::string& text, const std::string& field_name) {
    const std::string clean = trim_copy(text);
    char* end = nullptr;
    const double value = std::strtod(clean.c_str(), &end);
    if ((end == clean.c_str()) || (*end != '\0') || !std::isfinite(value)) {
        fail("Invalid floating point value for " + field_name + ": " + text);
    }
    return value;
}

uint32_t parse_range(const std::string& text) {
    const double value = parse_double(text, "range");
    if (std::fabs(value - 10.0) < 1e-9) {
        return X502_ADC_RANGE_10;
    }
    if (std::fabs(value - 5.0) < 1e-9) {
        return X502_ADC_RANGE_5;
    }
    if (std::fabs(value - 2.0) < 1e-9) {
        return X502_ADC_RANGE_2;
    }
    if (std::fabs(value - 1.0) < 1e-9) {
        return X502_ADC_RANGE_1;
    }
    if (std::fabs(value - 0.5) < 1e-9) {
        return X502_ADC_RANGE_05;
    }
    if (std::fabs(value - 0.2) < 1e-9) {
        return X502_ADC_RANGE_02;
    }
    fail("Unsupported E-502 range: " + text);
}

double range_to_volts(uint32_t range) {
    switch (range) {
    case X502_ADC_RANGE_10:
        return 10.0;
    case X502_ADC_RANGE_5:
        return 5.0;
    case X502_ADC_RANGE_2:
        return 2.0;
    case X502_ADC_RANGE_1:
        return 1.0;
    case X502_ADC_RANGE_05:
        return 0.5;
    case X502_ADC_RANGE_02:
        return 0.2;
    default:
        fail("Unknown ADC range enum");
    }
}

uint32_t parse_mode(const std::string& text) {
    const std::string value = trim_copy(text);
    if ((value == "diff") || (value == "differential")) {
        return X502_LCH_MODE_DIFF;
    }
    fail("gate_capture supports only mode:diff");
}

uint32_t parse_clock_mode(const std::string& text) {
    const std::string value = trim_copy(text);
    if (value == "di_syn1_rise") {
        return X502_SYNC_DI_SYN1_RISE;
    }
    if (value == "di_syn1_fall") {
        return X502_SYNC_DI_SYN1_FALL;
    }
    fail("Only clock:di_syn1_rise or clock:di_syn1_fall are supported");
}

std::string clock_mode_to_string(uint32_t mode) {
    switch (mode) {
    case X502_SYNC_DI_SYN1_RISE:
        return "di_syn1_rise";
    case X502_SYNC_DI_SYN1_FALL:
        return "di_syn1_fall";
    default:
        return "unknown";
    }
}

std::string phy_pair_name(uint32_t phy_ch) {
    return "X" + std::to_string(phy_ch + 1) + "-Y" + std::to_string(phy_ch + 1);
}

void print_help(const char* exe_name) {
    std::cout
        << "Usage:\n"
        << "  " << exe_name << " [serial:SN] [mode:diff] [ch1:2] [ch2:3] [range:5]\n"
        << "                [clock:di_syn1_rise] [clock_hz:2000000] [gate_hz:20000]\n"
        << "                [windows:1000] [plot_windows:50]\n"
        << "                [csv:gate_capture.csv] [svg:gate_capture.svg]\n"
        << "                [recv_block:8192] [recv_timeout_ms:50]\n"
        << "                [clock_wait_ms:5000] [window_wait_ms:5000]\n"
        << "                [buffer_words:262144] [step_words:8192]\n"
        << "                [pullup_syn1] [pulldown_start_in]\n"
        << "\n"
        << "Fixed sync scheme for this utility:\n"
        << "  DI_SYN1   -> external ADC clock\n"
        << "  START_IN  -> gate input read from synchronous digital input (DI16 mirror)\n"
        << "  streams   -> start immediately; windows are cut in software while START_IN is high\n"
        << "\n"
        << "Defaults for E-502:\n"
        << "  mode:diff         -> differential measurement only\n"
        << "  ch1:2, ch2:3      -> X3-Y3 and X4-Y4\n"
        << "  range:5           -> +/-5 V range\n"
        << "  clock:di_syn1_rise\n"
        << "  clock_hz:2000000  -> nominal external clock frequency for timing/plots\n"
        << "  gate_hz:20000     -> nominal square gate frequency for reporting\n"
        << "  windows:1000      -> capture 1000 complete gate windows\n"
        << "  plot_windows:50   -> overlay first 50 windows in SVG\n"
        << "\n"
        << "Differential physical channel mapping:\n"
        << "  0..15 -> X1-Y1 .. X16-Y16\n"
        << "\n"
        << "Important behavior:\n"
        << "  The program ignores a gate that is already high at startup and waits for the next\n"
        << "  clean low->high transition. This avoids saving a partial first window.\n"
        << "\n"
        << "Recommended example:\n"
        << "  " << exe_name
        << " clock:di_syn1_rise clock_hz:2000000 gate_hz:20000 windows:1000"
        << " csv:gate_capture.csv svg:gate_capture.svg\n";
}

Config parse_args(int argc, char** argv) {
    Config cfg;

    for (int i = 1; i < argc; ++i) {
        const std::string arg = argv[i];
        if ((arg == "help") || (arg == "--help") || (arg == "-h")) {
            print_help(argv[0]);
            std::exit(0);
        }
        if (arg == "pullup_syn1") {
            cfg.pullup_syn1 = true;
            continue;
        }
        if (arg == "pulldown_start_in") {
            cfg.pulldown_start_in = true;
            continue;
        }
        if (starts_with(arg, "serial:")) {
            cfg.serial = arg.substr(7);
            continue;
        }
        if (starts_with(arg, "mode:")) {
            cfg.mode = parse_mode(arg.substr(5));
            continue;
        }
        if (starts_with(arg, "range:")) {
            cfg.range = parse_range(arg.substr(6));
            continue;
        }
        if (starts_with(arg, "ch1:")) {
            cfg.ch1 = parse_u32(arg.substr(4), "ch1");
            continue;
        }
        if (starts_with(arg, "ch2:")) {
            cfg.ch2 = parse_u32(arg.substr(4), "ch2");
            continue;
        }
        if (starts_with(arg, "clock:")) {
            cfg.clock_mode = parse_clock_mode(arg.substr(6));
            continue;
        }
        if (starts_with(arg, "clock_hz:")) {
            cfg.clock_hz = parse_double(arg.substr(9), "clock_hz");
            continue;
        }
        if (starts_with(arg, "sample_clock_hz:")) {
            cfg.clock_hz = parse_double(arg.substr(16), "sample_clock_hz");
            continue;
        }
        if (starts_with(arg, "gate_hz:")) {
            cfg.gate_hz = parse_double(arg.substr(8), "gate_hz");
            continue;
        }
        if (starts_with(arg, "windows:")) {
            cfg.windows = parse_u32(arg.substr(8), "windows");
            continue;
        }
        if (starts_with(arg, "plot_windows:")) {
            cfg.plot_windows = parse_u32(arg.substr(13), "plot_windows");
            continue;
        }
        if (starts_with(arg, "recv_block:")) {
            cfg.recv_block_words = parse_u32(arg.substr(11), "recv_block");
            continue;
        }
        if (starts_with(arg, "recv_timeout_ms:")) {
            cfg.recv_timeout_ms = parse_u32(arg.substr(16), "recv_timeout_ms");
            continue;
        }
        if (starts_with(arg, "clock_wait_ms:")) {
            cfg.clock_wait_ms = parse_u32(arg.substr(14), "clock_wait_ms");
            continue;
        }
        if (starts_with(arg, "window_wait_ms:")) {
            cfg.window_wait_ms = parse_u32(arg.substr(15), "window_wait_ms");
            continue;
        }
        if (starts_with(arg, "buffer_words:")) {
            cfg.input_buffer_words = parse_u32(arg.substr(13), "buffer_words");
            continue;
        }
        if (starts_with(arg, "step_words:")) {
            cfg.input_step_words = parse_u32(arg.substr(11), "step_words");
            continue;
        }
        if (starts_with(arg, "csv:")) {
            cfg.csv_path = arg.substr(4);
            continue;
        }
        if (starts_with(arg, "svg:")) {
            cfg.svg_path = arg.substr(4);
            continue;
        }
        fail("Unknown argument: " + arg);
    }

    if (cfg.mode != X502_LCH_MODE_DIFF) {
        fail("gate_capture supports only differential inputs");
    }
    if ((cfg.ch1 >= X502_ADC_DIFF_CH_CNT) || (cfg.ch2 >= X502_ADC_DIFF_CH_CNT)) {
        fail("Differential E-502 channels must be in range 0..15");
    }
    if (cfg.ch1 == cfg.ch2) {
        fail("ch1 and ch2 must be different differential pairs");
    }
    if (cfg.clock_hz <= 0.0) {
        fail("clock_hz must be > 0");
    }
    if (cfg.gate_hz <= 0.0) {
        fail("gate_hz must be > 0");
    }
    if (cfg.windows == 0) {
        fail("windows must be > 0");
    }
    if (cfg.plot_windows == 0) {
        fail("plot_windows must be > 0");
    }
    if (cfg.recv_block_words == 0) {
        fail("recv_block must be > 0");
    }
    if (cfg.input_step_words == 0) {
        cfg.input_step_words = cfg.recv_block_words;
    }
    if (cfg.input_buffer_words < cfg.recv_block_words) {
        cfg.input_buffer_words = cfg.recv_block_words;
    }

    return cfg;
}

template <typename Fn>
Fn load_symbol(HMODULE module, const char* name) {
    const auto addr = GetProcAddress(module, name);
    if (addr == nullptr) {
        fail(std::string("GetProcAddress failed for symbol: ") + name);
    }
    return reinterpret_cast<Fn>(addr);
}

struct Api {
    HMODULE x502_module = nullptr;
    HMODULE e502_module = nullptr;

    decltype(&X502_Create) Create = nullptr;
    decltype(&X502_Free) Free = nullptr;
    decltype(&X502_Close) Close = nullptr;
    decltype(&X502_GetErrorString) GetErrorString = nullptr;
    decltype(&X502_GetDevInfo2) GetDevInfo2 = nullptr;
    decltype(&X502_SetMode) SetMode = nullptr;
    decltype(&X502_StreamsStop) StreamsStop = nullptr;
    decltype(&X502_StreamsDisable) StreamsDisable = nullptr;
    decltype(&X502_SetSyncMode) SetSyncMode = nullptr;
    decltype(&X502_SetSyncStartMode) SetSyncStartMode = nullptr;
    decltype(&X502_SetLChannelCount) SetLChannelCount = nullptr;
    decltype(&X502_SetLChannel) SetLChannel = nullptr;
    decltype(&X502_SetAdcFreqDivider) SetAdcFreqDivider = nullptr;
    decltype(&X502_SetAdcInterframeDelay) SetAdcInterframeDelay = nullptr;
    decltype(&X502_SetDinFreqDivider) SetDinFreqDivider = nullptr;
    decltype(&X502_SetStreamBufSize) SetStreamBufSize = nullptr;
    decltype(&X502_SetStreamStep) SetStreamStep = nullptr;
    decltype(&X502_SetDigInPullup) SetDigInPullup = nullptr;
    decltype(&X502_SetExtRefFreqValue) SetExtRefFreqValue = nullptr;
    decltype(&X502_Configure) Configure = nullptr;
    decltype(&X502_StreamsEnable) StreamsEnable = nullptr;
    decltype(&X502_StreamsStart) StreamsStart = nullptr;
    decltype(&X502_Recv) Recv = nullptr;
    decltype(&X502_ProcessData) ProcessData = nullptr;

    decltype(&E502_OpenUsb) OpenUsb = nullptr;

    Api() {
        x502_module = LoadLibraryA("x502api.dll");
        if (x502_module == nullptr) {
            fail("Cannot load x502api.dll");
        }
        e502_module = LoadLibraryA("e502api.dll");
        if (e502_module == nullptr) {
            fail("Cannot load e502api.dll");
        }

        Create = load_symbol<decltype(Create)>(x502_module, "X502_Create");
        Free = load_symbol<decltype(Free)>(x502_module, "X502_Free");
        Close = load_symbol<decltype(Close)>(x502_module, "X502_Close");
        GetErrorString = load_symbol<decltype(GetErrorString)>(x502_module, "X502_GetErrorString");
        GetDevInfo2 = load_symbol<decltype(GetDevInfo2)>(x502_module, "X502_GetDevInfo2");
        SetMode = load_symbol<decltype(SetMode)>(x502_module, "X502_SetMode");
        StreamsStop = load_symbol<decltype(StreamsStop)>(x502_module, "X502_StreamsStop");
        StreamsDisable = load_symbol<decltype(StreamsDisable)>(x502_module, "X502_StreamsDisable");
        SetSyncMode = load_symbol<decltype(SetSyncMode)>(x502_module, "X502_SetSyncMode");
        SetSyncStartMode = load_symbol<decltype(SetSyncStartMode)>(x502_module, "X502_SetSyncStartMode");
        SetLChannelCount = load_symbol<decltype(SetLChannelCount)>(x502_module, "X502_SetLChannelCount");
        SetLChannel = load_symbol<decltype(SetLChannel)>(x502_module, "X502_SetLChannel");
        SetAdcFreqDivider = load_symbol<decltype(SetAdcFreqDivider)>(x502_module, "X502_SetAdcFreqDivider");
        SetAdcInterframeDelay = load_symbol<decltype(SetAdcInterframeDelay)>(x502_module, "X502_SetAdcInterframeDelay");
        SetDinFreqDivider = load_symbol<decltype(SetDinFreqDivider)>(x502_module, "X502_SetDinFreqDivider");
        SetStreamBufSize = load_symbol<decltype(SetStreamBufSize)>(x502_module, "X502_SetStreamBufSize");
        SetStreamStep = load_symbol<decltype(SetStreamStep)>(x502_module, "X502_SetStreamStep");
        SetDigInPullup = load_symbol<decltype(SetDigInPullup)>(x502_module, "X502_SetDigInPullup");
        SetExtRefFreqValue = load_symbol<decltype(SetExtRefFreqValue)>(x502_module, "X502_SetExtRefFreqValue");
        Configure = load_symbol<decltype(Configure)>(x502_module, "X502_Configure");
        StreamsEnable = load_symbol<decltype(StreamsEnable)>(x502_module, "X502_StreamsEnable");
        StreamsStart = load_symbol<decltype(StreamsStart)>(x502_module, "X502_StreamsStart");
        Recv = load_symbol<decltype(Recv)>(x502_module, "X502_Recv");
        ProcessData = load_symbol<decltype(ProcessData)>(x502_module, "X502_ProcessData");

        OpenUsb = load_symbol<decltype(OpenUsb)>(e502_module, "E502_OpenUsb");
    }

    ~Api() {
        if (e502_module != nullptr) {
            FreeLibrary(e502_module);
        }
        if (x502_module != nullptr) {
            FreeLibrary(x502_module);
        }
    }
};

std::string x502_error(const Api& api, int32_t err) {
    const char* text = api.GetErrorString ? api.GetErrorString(err) : nullptr;
    std::ostringstream out;
    out << "err=" << err;
    if ((text != nullptr) && (*text != '\0')) {
        out << " (" << text << ")";
    }
    return out.str();
}

void expect_ok(const Api& api, int32_t err, const std::string& what) {
    if (err != X502_ERR_OK) {
        fail(what + ": " + x502_error(api, err));
    }
}

struct DeviceHandle {
    const Api& api;
    t_x502_hnd hnd = nullptr;
    bool opened = false;
    bool streams_started = false;

    explicit DeviceHandle(const Api& api_ref) : api(api_ref), hnd(api.Create()) {
        if (hnd == nullptr) {
            fail("X502_Create failed");
        }
    }

    ~DeviceHandle() {
        if (hnd != nullptr) {
            if (streams_started) {
                api.StreamsStop(hnd);
            }
            if (opened) {
                api.Close(hnd);
            }
            api.Free(hnd);
        }
    }
};

void print_device_info(const t_x502_info& info) {
    std::cout << "Device: " << info.name << "\n"
              << "Serial: " << info.serial << "\n"
              << "FPGA version: " << static_cast<unsigned>(info.fpga_ver >> 8) << "."
              << static_cast<unsigned>(info.fpga_ver & 0xFF) << "\n"
              << "PLDA version: " << static_cast<unsigned>(info.plda_ver) << "\n"
              << "Board revision: " << static_cast<unsigned>(info.board_rev) << "\n"
              << "MCU firmware: " << info.mcu_firmware_ver << "\n";
}

AverageTrace build_average_trace(const std::vector<WindowData>& windows) {
    AverageTrace result;
    std::size_t max_len = 0;
    for (const auto& window : windows) {
        max_len = std::max(max_len, std::min(window.ch1.size(), window.ch2.size()));
    }

    result.ch1.assign(max_len, 0.0);
    result.ch2.assign(max_len, 0.0);
    result.counts.assign(max_len, 0U);

    for (const auto& window : windows) {
        const std::size_t len = std::min(window.ch1.size(), window.ch2.size());
        for (std::size_t i = 0; i < len; ++i) {
            result.ch1[i] += window.ch1[i];
            result.ch2[i] += window.ch2[i];
            ++result.counts[i];
        }
    }

    for (std::size_t i = 0; i < max_len; ++i) {
        if (result.counts[i] != 0U) {
            result.ch1[i] /= static_cast<double>(result.counts[i]);
            result.ch2[i] /= static_cast<double>(result.counts[i]);
        }
    }

    return result;
}

std::string build_polyline_points(const std::vector<double>& data,
                                  double frame_freq_hz,
                                  std::size_t max_samples,
                                  double y_min,
                                  double y_max,
                                  double left,
                                  double top,
                                  double width,
                                  double height) {
    std::ostringstream out;
    out << std::fixed << std::setprecision(3);

    const std::size_t len = data.size();
    const double max_time_us = (max_samples > 1)
        ? ((static_cast<double>(max_samples - 1) * 1e6) / frame_freq_hz)
        : 1.0;
    const double y_span = std::max(1e-12, y_max - y_min);

    for (std::size_t i = 0; i < len; ++i) {
        const double time_us = static_cast<double>(i) * 1e6 / frame_freq_hz;
        const double x = left + (time_us / max_time_us) * width;
        const double y = top + height - ((data[i] - y_min) / y_span) * height;
        out << x << "," << y << " ";
    }
    return out.str();
}

void write_csv(const std::string& path,
               const std::vector<WindowData>& windows,
               double frame_freq_hz) {
    std::ofstream file(path, std::ios::binary);
    if (!file) {
        fail("Cannot open CSV for writing: " + path);
    }

    file << "window_idx,sample_in_window,time_us,ch1_v,ch2_v\n";
    file << std::fixed << std::setprecision(9);

    for (std::size_t w = 0; w < windows.size(); ++w) {
        const std::size_t len = std::min(windows[w].ch1.size(), windows[w].ch2.size());
        for (std::size_t i = 0; i < len; ++i) {
            const double time_us = static_cast<double>(i) * 1e6 / frame_freq_hz;
            file << w << "," << i << "," << time_us << "," << windows[w].ch1[i] << "," << windows[w].ch2[i]
                 << "\n";
        }
    }
}

void write_svg(const std::string& path,
               const std::vector<WindowData>& windows,
               double frame_freq_hz,
               double nominal_range_v,
               uint32_t plot_windows,
               const Config& cfg) {
    std::ofstream file(path, std::ios::binary);
    if (!file) {
        fail("Cannot open SVG for writing: " + path);
    }

    const AverageTrace avg = build_average_trace(windows);

    std::size_t max_samples = 0;
    double min_y = std::numeric_limits<double>::infinity();
    double max_y = -std::numeric_limits<double>::infinity();

    for (const auto& window : windows) {
        const std::size_t len = std::min(window.ch1.size(), window.ch2.size());
        max_samples = std::max(max_samples, len);
        for (std::size_t i = 0; i < len; ++i) {
            min_y = std::min(min_y, std::min(window.ch1[i], window.ch2[i]));
            max_y = std::max(max_y, std::max(window.ch1[i], window.ch2[i]));
        }
    }

    if (!std::isfinite(min_y) || !std::isfinite(max_y) || (min_y == max_y)) {
        min_y = -nominal_range_v;
        max_y = nominal_range_v;
    } else {
        const double pad = std::max(0.1, (max_y - min_y) * 0.10);
        min_y -= pad;
        max_y += pad;
    }

    if (max_samples < 2) {
        max_samples = 2;
    }

    const std::size_t overlay_count = std::min<std::size_t>(plot_windows, windows.size());
    const double max_time_us = (static_cast<double>(max_samples - 1) * 1e6) / frame_freq_hz;

    const double width = 1500.0;
    const double height = 980.0;
    const double left = 95.0;
    const double right = 40.0;
    const double top = 55.0;
    const double gap = 85.0;
    const double bottom = 75.0;
    const double plot_width = width - left - right;
    const double panel_height = (height - top - bottom - gap) / 2.0;
    const double overlay_top = top;
    const double average_top = top + panel_height + gap;

    auto write_panel = [&](double panel_top, const std::string& title) {
        file << "  <rect x=\"" << left << "\" y=\"" << panel_top << "\" width=\"" << plot_width
             << "\" height=\"" << panel_height << "\" fill=\"#fbfcfe\" stroke=\"#ccd4dd\"/>\n";
        for (int i = 0; i <= 10; ++i) {
            const double x = left + (plot_width * i / 10.0);
            const double y = panel_top + (panel_height * i / 10.0);
            file << "  <line x1=\"" << x << "\" y1=\"" << panel_top << "\" x2=\"" << x << "\" y2=\""
                 << (panel_top + panel_height) << "\" stroke=\"#edf1f5\" stroke-width=\"1\"/>\n";
            file << "  <line x1=\"" << left << "\" y1=\"" << y << "\" x2=\"" << (left + plot_width)
                 << "\" y2=\"" << y << "\" stroke=\"#edf1f5\" stroke-width=\"1\"/>\n";
        }

        if ((0.0 >= min_y) && (0.0 <= max_y)) {
            const double zero_y = panel_top + panel_height
                - ((0.0 - min_y) / std::max(1e-12, max_y - min_y)) * panel_height;
            file << "  <line x1=\"" << left << "\" y1=\"" << zero_y << "\" x2=\"" << (left + plot_width)
                 << "\" y2=\"" << zero_y << "\" stroke=\"#8fa1b3\" stroke-width=\"1.2\"/>\n";
        }

        file << "  <text x=\"" << left << "\" y=\"" << (panel_top - 12)
             << "\" font-size=\"20\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">"
             << title << "</text>\n";

        file << std::fixed << std::setprecision(3);
        for (int i = 0; i <= 10; ++i) {
            const double x = left + (plot_width * i / 10.0);
            const double t = max_time_us * i / 10.0;
            const double y = panel_top + panel_height - (panel_height * i / 10.0);
            const double v = min_y + (max_y - min_y) * i / 10.0;

            file << "  <text x=\"" << x << "\" y=\"" << (panel_top + panel_height + 24)
                 << "\" text-anchor=\"middle\" font-size=\"12\" font-family=\"Segoe UI, Arial, sans-serif\""
                 << " fill=\"#607080\">" << t << "</text>\n";
            file << "  <text x=\"" << (left - 10) << "\" y=\"" << (y + 4)
                 << "\" text-anchor=\"end\" font-size=\"12\" font-family=\"Segoe UI, Arial, sans-serif\""
                 << " fill=\"#607080\">" << v << "</text>\n";
        }
    };

    file << "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" << width << "\" height=\"" << height
         << "\" viewBox=\"0 0 " << width << " " << height << "\">\n";
    file << "  <rect width=\"100%\" height=\"100%\" fill=\"#ffffff\"/>\n";
    file << "  <text x=\"" << left << "\" y=\"28\" font-size=\"24\" font-family=\"Segoe UI, Arial, sans-serif\""
         << " fill=\"#203040\">E-502 gate capture</text>\n";
    file << "  <text x=\"" << left << "\" y=\"48\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\""
         << " fill=\"#425466\">"
         << phy_pair_name(cfg.ch1) << ", " << phy_pair_name(cfg.ch2)
         << ", clock=" << cfg.clock_hz << " Hz, gate=" << cfg.gate_hz << " Hz, windows=" << windows.size()
         << "</text>\n";

    write_panel(overlay_top, "Overlay of first windows");
    write_panel(average_top, "Average waveform");

    for (std::size_t i = 0; i < overlay_count; ++i) {
        const std::size_t len = std::min(windows[i].ch1.size(), windows[i].ch2.size());
        if (len < 2) {
            continue;
        }

        const std::vector<double> ch1(windows[i].ch1.begin(), windows[i].ch1.begin() + static_cast<std::ptrdiff_t>(len));
        const std::vector<double> ch2(windows[i].ch2.begin(), windows[i].ch2.begin() + static_cast<std::ptrdiff_t>(len));

        file << "  <polyline fill=\"none\" stroke=\"#005bbb\" stroke-opacity=\"0.20\" stroke-width=\"1.1\" points=\""
             << build_polyline_points(ch1, frame_freq_hz, max_samples, min_y, max_y, left, overlay_top, plot_width, panel_height)
             << "\"/>\n";
        file << "  <polyline fill=\"none\" stroke=\"#d62828\" stroke-opacity=\"0.20\" stroke-width=\"1.1\" points=\""
             << build_polyline_points(ch2, frame_freq_hz, max_samples, min_y, max_y, left, overlay_top, plot_width, panel_height)
             << "\"/>\n";
    }

    if (avg.ch1.size() >= 2U) {
        file << "  <polyline fill=\"none\" stroke=\"#005bbb\" stroke-width=\"2.4\" points=\""
             << build_polyline_points(avg.ch1, frame_freq_hz, max_samples, min_y, max_y, left, average_top, plot_width, panel_height)
             << "\"/>\n";
        file << "  <polyline fill=\"none\" stroke=\"#d62828\" stroke-width=\"2.4\" points=\""
             << build_polyline_points(avg.ch2, frame_freq_hz, max_samples, min_y, max_y, left, average_top, plot_width, panel_height)
             << "\"/>\n";
    }

    file << "  <text x=\"" << (width - 250) << "\" y=\"" << (height - 48)
         << "\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">CH1</text>\n";
    file << "  <line x1=\"" << (width - 320) << "\" y1=\"" << (height - 52)
         << "\" x2=\"" << (width - 270) << "\" y2=\"" << (height - 52)
         << "\" stroke=\"#005bbb\" stroke-width=\"3\"/>\n";
    file << "  <text x=\"" << (width - 110) << "\" y=\"" << (height - 48)
         << "\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">CH2</text>\n";
    file << "  <line x1=\"" << (width - 180) << "\" y1=\"" << (height - 52)
         << "\" x2=\"" << (width - 130) << "\" y2=\"" << (height - 52)
         << "\" stroke=\"#d62828\" stroke-width=\"3\"/>\n";

    file << "  <text x=\"" << left << "\" y=\"" << (height - 18)
         << "\" font-size=\"16\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#425466\">time within window, us</text>\n";
    file << "  <text x=\"18\" y=\"" << (overlay_top + 16)
         << "\" font-size=\"16\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#425466\">V</text>\n";
    file << "  <text x=\"18\" y=\"" << (average_top + 16)
         << "\" font-size=\"16\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#425466\">V</text>\n";
    file << "</svg>\n";
}

int run(const Config& cfg) {
    Api api;
    DeviceHandle device(api);

    const int32_t open_err = api.OpenUsb(device.hnd, cfg.serial.empty() ? nullptr : cfg.serial.c_str());
    expect_ok(api, open_err, "Open device over USB");
    device.opened = true;

    t_x502_info info{};
    expect_ok(api, api.GetDevInfo2(device.hnd, &info, sizeof(info)), "Get device info");
    print_device_info(info);

    expect_ok(api, api.SetMode(device.hnd, X502_MODE_FPGA), "Set FPGA mode");
    api.StreamsStop(device.hnd);
    api.StreamsDisable(device.hnd, X502_STREAM_ALL_IN | X502_STREAM_ALL_OUT);

    expect_ok(api, api.SetSyncMode(device.hnd, cfg.clock_mode), "Set external clock on DI_SYN1");
    expect_ok(api, api.SetSyncStartMode(device.hnd, X502_SYNC_INTERNAL), "Set immediate stream start");

    const int32_t ext_ref_err = api.SetExtRefFreqValue(device.hnd, cfg.clock_hz);
    if (ext_ref_err != X502_ERR_OK) {
        if (cfg.clock_hz <= 1500000.0) {
            expect_ok(api, ext_ref_err, "Set external reference frequency");
        } else {
            std::cerr << "Warning: X502_SetExtRefFreqValue(" << cfg.clock_hz
                      << ") failed, continuing with nominal timing only: "
                      << x502_error(api, ext_ref_err) << "\n";
        }
    }

    expect_ok(api, api.SetLChannelCount(device.hnd, kLogicalChannelCount), "Set logical channel count");
    expect_ok(api, api.SetLChannel(device.hnd, 0, cfg.ch1, cfg.mode, cfg.range, 1), "Set logical channel 0");
    expect_ok(api, api.SetLChannel(device.hnd, 1, cfg.ch2, cfg.mode, cfg.range, 1), "Set logical channel 1");
    expect_ok(api, api.SetAdcFreqDivider(device.hnd, 1), "Set ADC frequency divider");
    expect_ok(api, api.SetAdcInterframeDelay(device.hnd, 0), "Set ADC interframe delay");
    expect_ok(api, api.SetDinFreqDivider(device.hnd, kLogicalChannelCount), "Set DIN frequency divider");
    expect_ok(api, api.SetStreamBufSize(device.hnd, X502_STREAM_CH_IN, cfg.input_buffer_words), "Set input buffer size");
    expect_ok(api, api.SetStreamStep(device.hnd, X502_STREAM_CH_IN, cfg.input_step_words), "Set input stream step");

    uint32_t pullups = 0;
    if (cfg.pullup_syn1) {
        pullups |= X502_PULLUPS_DI_SYN1;
    }
    if (cfg.pulldown_start_in) {
        pullups |= X502_PULLDOWN_START_IN;
    }
    expect_ok(api, api.SetDigInPullup(device.hnd, pullups), "Set digital input pullups/pulldowns");

    expect_ok(api, api.Configure(device.hnd, 0), "Configure device");
    expect_ok(api, api.StreamsEnable(device.hnd, X502_STREAM_ADC | X502_STREAM_DIN), "Enable ADC+DIN stream");

    const double frame_freq_hz = cfg.clock_hz / static_cast<double>(kLogicalChannelCount);
    const double expected_window_us = 1e6 / (2.0 * cfg.gate_hz);
    const double expected_frames_per_window = frame_freq_hz / (2.0 * cfg.gate_hz);

    std::cout << "Capture settings:\n"
              << "  clock source: " << clock_mode_to_string(cfg.clock_mode) << "\n"
              << "  gate source: start_in (read from synchronous DIN)\n"
              << "  sample clock: " << cfg.clock_hz << " Hz\n"
              << "  per-channel frame rate: " << frame_freq_hz << " Hz\n"
              << "  gate frequency: " << cfg.gate_hz << " Hz\n"
              << "  expected high window: " << expected_window_us << " us\n"
              << "  expected frames per window: " << expected_frames_per_window << "\n"
              << "  channel 1: " << phy_pair_name(cfg.ch1) << "\n"
              << "  channel 2: " << phy_pair_name(cfg.ch2) << "\n"
              << "  ADC range: +/-" << range_to_volts(cfg.range) << " V\n"
              << "  target windows: " << cfg.windows << "\n";

    expect_ok(api, api.StreamsStart(device.hnd), "Start streams");
    device.streams_started = true;

    std::vector<uint32_t> raw(cfg.recv_block_words);
    std::vector<double> adc_buffer(cfg.recv_block_words);
    std::vector<uint32_t> din_buffer(cfg.recv_block_words);

    std::vector<WindowData> windows;
    windows.reserve(cfg.windows);

    std::deque<Frame> pending_frames;
    std::deque<bool> pending_gate;

    bool have_partial_ch1 = false;
    double partial_ch1 = 0.0;
    uint32_t next_lch = 0;

    bool gate_initialized = false;
    bool last_gate = false;
    bool saw_low_before_capture = false;
    bool in_window = false;

    WindowData current_window;

    const ULONGLONG session_start = GetTickCount64();
    ULONGLONG last_stream_activity = session_start;
    ULONGLONG last_window_complete = session_start;
    ULONGLONG last_din_activity = session_start;
    ULONGLONG last_gate_edge = session_start;

    uint64_t total_frames_seen = 0;
    uint64_t total_din_words = 0;
    uint64_t total_gate_edges = 0;

    auto finalize_window = [&]() {
        const std::size_t len = std::min(current_window.ch1.size(), current_window.ch2.size());
        current_window.ch1.resize(len);
        current_window.ch2.resize(len);
        if (len != 0U) {
            windows.push_back(std::move(current_window));
            current_window = WindowData{};
            last_window_complete = GetTickCount64();
        } else {
            current_window = WindowData{};
        }
    };

    auto fail_waiting_for_gate = [&](ULONGLONG now) {
        std::ostringstream message;
        message << "ADC clock is present, but no complete START_IN windows were captured within "
                << cfg.window_wait_ms << " ms. "
                << "Processed frames=" << total_frames_seen
                << ", DIN samples=" << total_din_words
                << ", gate edges=" << total_gate_edges << ". ";

        if (total_din_words == 0) {
            message << "No synchronous DIN words were decoded, so START_IN is not being sampled. ";
        } else if (!gate_initialized) {
            message << "DIN data is present, but START_IN state was not initialized yet. ";
        } else if (total_gate_edges == 0) {
            message << "START_IN appears stuck " << (last_gate ? "HIGH" : "LOW") << ". ";
        } else if (in_window) {
            message << "START_IN produced a rising edge, but no matching falling edge closed the window. ";
        } else {
            message << "START_IN toggled, but no complete low->high->low window was accepted. ";
        }

        message << "Check START_IN wiring, common DGND with the generator, and signal level around 0/3.3 V. "
                << "Gate progress: last DIN activity "
                << (now - last_din_activity)
                << " ms ago, last gate edge "
                << (now - last_gate_edge)
                << " ms ago.";
        fail(message.str());
    };

    while (windows.size() < cfg.windows) {
        const int32_t recvd = api.Recv(device.hnd, raw.data(), cfg.recv_block_words, cfg.recv_timeout_ms);
        if (recvd < 0) {
            fail("X502_Recv failed: " + x502_error(api, recvd));
        }

        const ULONGLONG now = GetTickCount64();
        if (recvd == 0) {
            if ((now - last_stream_activity) >= cfg.clock_wait_ms) {
                if (windows.empty() && !gate_initialized) {
                    fail("Timeout waiting for external ADC clock on DI_SYN1. "
                         "Streams start immediately in this program, so a timeout here usually means "
                         "there is no valid clock on DI_SYN1.");
                }
                fail("External ADC clock stalled before capture completed.");
            }

            if ((windows.size() < cfg.windows) && ((now - last_window_complete) >= cfg.window_wait_ms)) {
                fail_waiting_for_gate(now);
            }
            continue;
        }

        last_stream_activity = now;

        uint32_t adc_count = static_cast<uint32_t>(adc_buffer.size());
        uint32_t din_count = static_cast<uint32_t>(din_buffer.size());
        expect_ok(api,
                  api.ProcessData(device.hnd,
                                  raw.data(),
                                  static_cast<uint32_t>(recvd),
                                  X502_PROC_FLAGS_VOLT,
                                  adc_buffer.data(),
                                  &adc_count,
                                  din_buffer.data(),
                                  &din_count),
                  "Process ADC+DIN data");

        for (uint32_t i = 0; i < adc_count; ++i) {
            const double value = adc_buffer[i];
            if (next_lch == 0U) {
                partial_ch1 = value;
                have_partial_ch1 = true;
                next_lch = 1U;
            } else {
                if (!have_partial_ch1) {
                    fail("Internal frame assembly error: missing channel 1 sample");
                }
                pending_frames.push_back(Frame{partial_ch1, value});
                ++total_frames_seen;
                have_partial_ch1 = false;
                next_lch = 0U;
            }
        }

        for (uint32_t i = 0; i < din_count; ++i) {
            pending_gate.push_back((din_buffer[i] & kStartInMask) != 0U);
        }
        if (din_count != 0U) {
            total_din_words += din_count;
            last_din_activity = now;
        }

        if ((pending_frames.size() > 1000000U) || (pending_gate.size() > 1000000U)) {
            fail("Internal backlog grew too large while waiting for gate windows. "
                 "ADC frames and DIN samples are not lining up as expected.");
        }

        while (!pending_frames.empty() && !pending_gate.empty() && (windows.size() < cfg.windows)) {
            const Frame frame = pending_frames.front();
            pending_frames.pop_front();

            const bool gate = pending_gate.front();
            pending_gate.pop_front();

            if (!gate_initialized) {
                gate_initialized = true;
                last_gate = gate;
                saw_low_before_capture = !gate;
                last_gate_edge = now;
            } else if (!saw_low_before_capture && !gate) {
                saw_low_before_capture = true;
            }

            if (gate_initialized && (gate != last_gate)) {
                ++total_gate_edges;
                last_gate_edge = now;
            }

            if (!in_window && saw_low_before_capture && gate_initialized && !last_gate && gate) {
                in_window = true;
                current_window = WindowData{};
            }

            if (in_window && !gate) {
                finalize_window();
                in_window = false;
            }

            if (in_window && gate) {
                current_window.ch1.push_back(frame.ch1);
                current_window.ch2.push_back(frame.ch2);
            }

            last_gate = gate;
        }

        if ((windows.size() < cfg.windows) && ((now - last_window_complete) >= cfg.window_wait_ms)) {
            fail_waiting_for_gate(now);
        }
    }

    if (in_window) {
        finalize_window();
    }

    expect_ok(api, api.StreamsStop(device.hnd), "Stop streams");
    device.streams_started = false;

    if (windows.size() > cfg.windows) {
        windows.resize(cfg.windows);
    }
    if (windows.empty()) {
        fail("No complete gate windows were captured");
    }

    write_csv(cfg.csv_path, windows, frame_freq_hz);
    write_svg(cfg.svg_path, windows, frame_freq_hz, range_to_volts(cfg.range), cfg.plot_windows, cfg);

    std::size_t min_len = std::numeric_limits<std::size_t>::max();
    std::size_t max_len = 0;
    std::size_t total_len = 0;
    for (const auto& window : windows) {
        const std::size_t len = std::min(window.ch1.size(), window.ch2.size());
        min_len = std::min(min_len, len);
        max_len = std::max(max_len, len);
        total_len += len;
    }
    const double avg_len = static_cast<double>(total_len) / static_cast<double>(windows.size());

    std::cout << "Captured windows: " << windows.size() << "\n"
              << "Samples per window: min=" << min_len << ", avg=" << avg_len << ", max=" << max_len << "\n"
              << "CSV: " << cfg.csv_path << "\n"
              << "SVG: " << cfg.svg_path << "\n";

    return 0;
}

} // namespace

int main(int argc, char** argv) {
    try {
        const Config cfg = parse_args(argc, argv);
        return run(cfg);
    } catch (const std::exception& ex) {
        std::cerr << "Error: " << ex.what() << "\n";
        return 1;
    }
}
