#ifdef _WIN32
    #ifndef NOMINMAX
        #define NOMINMAX
    #endif
    #include <windows.h>
#else
    #include <csignal>
    #include <dlfcn.h>
#endif

#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 <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <limits>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

namespace {

constexpr uint32_t kE502DiSyn2Mask =
    (static_cast<uint32_t>(1U) << 13U) | (static_cast<uint32_t>(1U) << 17U);
constexpr uint32_t kE502Digital1Mask = (static_cast<uint32_t>(1U) << 0U);
constexpr uint32_t kE502Digital2Mask = (static_cast<uint32_t>(1U) << 1U);
constexpr uint32_t kInternalRefSetting = X502_REF_FREQ_2000KHZ;
constexpr uint32_t kInternalRefHz = 2000000U;

struct Config {
    std::string serial;
    std::optional<uint32_t> ip_addr;

    double clock_hz = 2000000.0;
    double duration_ms = 100.0; // Legacy-only option retained for backward compatibility warnings.
    uint32_t windows = 1000;

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

    bool pullup_syn2 = false;
    bool legacy_clock_arg_used = false;
    std::string legacy_clock_arg;
    bool legacy_pullup_syn1 = false;
    bool legacy_clean_start = false;
    bool legacy_duration_ms = false;
};

struct LchmClockCount {
    uint64_t clocks = 0;
    uint64_t di2_high_clocks = 0;
    uint64_t di2_low_clocks = 0;

    void clear() {
        clocks = 0;
        di2_high_clocks = 0;
        di2_low_clocks = 0;
    }

    void add(bool di2_high) {
        ++clocks;
        if (di2_high) {
            ++di2_high_clocks;
        } else {
            ++di2_low_clocks;
        }
    }
};

struct Di1StepClockCount {
    bool di1_level = false;
    LchmClockCount count;
};

struct RunningStats {
    uint64_t count = 0;
    uint64_t clocks_min = std::numeric_limits<uint64_t>::max();
    uint64_t clocks_max = 0;
    uint64_t clocks_sum = 0;
    uint64_t high_min = std::numeric_limits<uint64_t>::max();
    uint64_t high_max = 0;
    uint64_t high_sum = 0;
    uint64_t low_min = std::numeric_limits<uint64_t>::max();
    uint64_t low_max = 0;
    uint64_t low_sum = 0;

    void add(const LchmClockCount& value) {
        ++count;
        clocks_min = std::min(clocks_min, value.clocks);
        clocks_max = std::max(clocks_max, value.clocks);
        clocks_sum += value.clocks;
        high_min = std::min(high_min, value.di2_high_clocks);
        high_max = std::max(high_max, value.di2_high_clocks);
        high_sum += value.di2_high_clocks;
        low_min = std::min(low_min, value.di2_low_clocks);
        low_max = std::max(low_max, value.di2_low_clocks);
        low_sum += value.di2_low_clocks;
    }
};

[[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_ipv4(const std::string& text) {
    std::array<uint32_t, 4> parts{};
    std::stringstream ss(text);
    std::string token;
    for (std::size_t i = 0; i < parts.size(); ++i) {
        if (!std::getline(ss, token, '.')) {
            fail("Invalid IPv4 address: " + text);
        }
        parts[i] = parse_u32(token, "ip");
        if (parts[i] > 255U) {
            fail("IPv4 byte out of range: " + token);
        }
    }
    if (std::getline(ss, token, '.')) {
        fail("Invalid IPv4 address: " + text);
    }
    return (parts[0] << 24U) | (parts[1] << 16U) | (parts[2] << 8U) | parts[3];
}

std::string ipv4_to_string(uint32_t ip_addr) {
    std::ostringstream out;
    out << ((ip_addr >> 24U) & 0xFFU) << '.'
        << ((ip_addr >> 16U) & 0xFFU) << '.'
        << ((ip_addr >> 8U) & 0xFFU) << '.'
        << (ip_addr & 0xFFU);
    return out.str();
}

std::string parse_legacy_clock_mode(const std::string& text) {
    const std::string value = trim_copy(text);
    if ((value == "di_syn1_rise") || (value == "di_syn1_fall")) {
        return value;
    }
    fail("Unsupported legacy clock mode: " + text + ". Use di_syn1_rise or di_syn1_fall.");
}

void print_help(const char* exe_name) {
    std::cout
        << "Usage:\n"
        << "  " << exe_name << " [serial:SN] [ip:192.168.0.10]\n"
        << "                [clock_hz:2000000] [windows:1000]\n"
        << "                [recv_block:8192] [recv_timeout_ms:50]\n"
        << "                [clock_wait_ms:5000] [lchm_wait_ms:5000]\n"
        << "                [buffer_words:262144] [step_words:8192]\n"
        << "                [pullup_syn2]\n"
        << "\n"
        << "Fixed counting scheme:\n"
        << "  clock   -> internal only (fixed reference 2000000 Hz)\n"
        << "  DI_SYN2 -> strict LCHM window; low->high starts, high->low stops\n"
        << "  DI1     -> step delimiter; both DI1 edges split LCHM into steps\n"
        << "  DI2     -> high/low clocks counted per step and per full LCHM\n"
        << "\n"
        << "Legacy arguments are accepted but ignored with warning:\n"
        << "  clock:di_syn1_rise|di_syn1_fall, pullup_syn1, clean_start, duration_ms:...\n"
        << "\n"
        << "Example:\n"
        << "  " << exe_name << " clock_hz:2000000 windows:1000 pullup_syn2\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.legacy_pullup_syn1 = true;
            continue;
        }
        if (arg == "pullup_syn2") {
            cfg.pullup_syn2 = true;
            continue;
        }
        if (arg == "clean_start") {
            cfg.legacy_clean_start = true;
            continue;
        }
        if (starts_with(arg, "serial:")) {
            cfg.serial = arg.substr(7);
            continue;
        }
        if (starts_with(arg, "ip:")) {
            cfg.ip_addr = parse_ipv4(arg.substr(3));
            continue;
        }
        if (starts_with(arg, "clock:")) {
            cfg.legacy_clock_arg_used = true;
            cfg.legacy_clock_arg = parse_legacy_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, "duration_ms:")) {
            cfg.duration_ms = parse_double(arg.substr(12), "duration_ms");
            cfg.legacy_duration_ms = true;
            continue;
        }
        if (starts_with(arg, "windows:")) {
            cfg.windows = parse_u32(arg.substr(8), "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, "lchm_wait_ms:")) {
            cfg.lchm_wait_ms = parse_u32(arg.substr(13), "lchm_wait_ms");
            continue;
        }
        if (starts_with(arg, "window_wait_ms:")) {
            cfg.lchm_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;
        }
        fail("Unknown argument: " + arg);
    }

    if (cfg.clock_hz <= 0.0) {
        fail("clock_hz must be > 0");
    }
    if (cfg.duration_ms < 0.0) {
        fail("duration_ms must be >= 0");
    }
    if (cfg.windows == 0U) {
        fail("windows must be > 0");
    }
    if (cfg.recv_block_words == 0U) {
        fail("recv_block must be > 0");
    }
    if (cfg.input_step_words == 0U) {
        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;
}

#ifdef _WIN32
using ModuleHandle = HMODULE;
#else
using ModuleHandle = void*;
#endif

std::string dynamic_loader_error() {
#ifdef _WIN32
    return "unknown error";
#else
    const char* error = dlerror();
    return ((error != nullptr) && (*error != '\0')) ? std::string(error) : std::string("unknown error");
#endif
}

ModuleHandle open_library(const char* path) {
#ifdef _WIN32
    return LoadLibraryA(path);
#else
    dlerror();
    return dlopen(path, RTLD_LAZY | RTLD_LOCAL);
#endif
}

void close_library(ModuleHandle module) {
#ifdef _WIN32
    if (module != nullptr) {
        FreeLibrary(module);
    }
#else
    if (module != nullptr) {
        dlclose(module);
    }
#endif
}

ModuleHandle load_library_or_fail(const std::vector<std::string>& candidates,
                                  const std::string& description) {
    std::string last_error = "no candidates provided";
    for (const auto& candidate : candidates) {
        ModuleHandle module = open_library(candidate.c_str());
        if (module != nullptr) {
            return module;
        }
        last_error = dynamic_loader_error();
    }

    std::ostringstream out;
    out << "Cannot load " << description << ". Tried:";
    for (const auto& candidate : candidates) {
        out << " " << candidate;
    }
    out << ". Last error: " << last_error;
    fail(out.str());
}

template <typename Fn>
Fn load_symbol(ModuleHandle module, const char* name) {
#ifdef _WIN32
    const auto addr = GetProcAddress(module, name);
    if (addr == nullptr) {
        fail(std::string("GetProcAddress failed for symbol: ") + name);
    }
    return reinterpret_cast<Fn>(addr);
#else
    dlerror();
    void* addr = dlsym(module, name);
    const char* error = dlerror();
    if ((addr == nullptr) || (error != nullptr)) {
        std::ostringstream out;
        out << "dlsym failed for symbol " << name << ": "
            << ((error != nullptr) ? error : "unknown error");
        fail(out.str());
    }
    return reinterpret_cast<Fn>(addr);
#endif
}

template <typename Fn>
Fn try_load_symbol(ModuleHandle module, const char* name) {
#ifdef _WIN32
    const auto addr = GetProcAddress(module, name);
    return reinterpret_cast<Fn>(addr);
#else
    dlerror();
    void* addr = dlsym(module, name);
    (void) dlerror();
    return reinterpret_cast<Fn>(addr);
#endif
}

struct Api {
    ModuleHandle x502_module = nullptr;
    ModuleHandle 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_GetDevInfo) GetDevInfo = 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_SetRefFreq) SetRefFreq = nullptr;
    decltype(&X502_SetDinFreq) SetDinFreq = nullptr;
    decltype(&X502_SetStreamBufSize) SetStreamBufSize = nullptr;
    decltype(&X502_SetStreamStep) SetStreamStep = nullptr;
    decltype(&X502_SetDigInPullup) SetDigInPullup = nullptr;
    decltype(&X502_Configure) Configure = nullptr;
    decltype(&X502_StreamsEnable) StreamsEnable = nullptr;
    decltype(&X502_StreamsStart) StreamsStart = nullptr;
    decltype(&X502_GetRecvReadyCount) GetRecvReadyCount = nullptr;
    decltype(&X502_Recv) Recv = nullptr;
    decltype(&X502_ProcessData) ProcessData = nullptr;

    decltype(&E502_OpenUsb) OpenUsb = nullptr;
    decltype(&E502_OpenByIpAddr) OpenByIpAddr = nullptr;

    Api() {
        x502_module = load_library_or_fail(
#ifdef _WIN32
            {"x502api.dll"},
#else
            {"libx502api.so", "x502api.so", "./libx502api.so", "./x502api.so"},
#endif
            "x502 API library");
        e502_module = load_library_or_fail(
#ifdef _WIN32
            {"e502api.dll"},
#else
            {"libe502api.so", "e502api.so", "./libe502api.so", "./e502api.so"},
#endif
            "e502 API library");

        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");
        GetDevInfo = try_load_symbol<decltype(GetDevInfo)>(x502_module, "X502_GetDevInfo");
        GetDevInfo2 = try_load_symbol<decltype(GetDevInfo2)>(x502_module, "X502_GetDevInfo2");
        if ((GetDevInfo == nullptr) && (GetDevInfo2 == nullptr)) {
            fail("Neither X502_GetDevInfo nor X502_GetDevInfo2 is available in x502 API library");
        }
        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");
        SetRefFreq = load_symbol<decltype(SetRefFreq)>(x502_module, "X502_SetRefFreq");
        SetDinFreq = load_symbol<decltype(SetDinFreq)>(x502_module, "X502_SetDinFreq");
        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");
        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");
        GetRecvReadyCount = load_symbol<decltype(GetRecvReadyCount)>(x502_module, "X502_GetRecvReadyCount");
        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");
        OpenByIpAddr = load_symbol<decltype(OpenByIpAddr)>(e502_module, "E502_OpenByIpAddr");
    }

    ~Api() {
        close_library(e502_module);
        close_library(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));
    }
}

using TickMs = uint64_t;

TickMs tick_count_ms() {
#ifdef _WIN32
    return static_cast<TickMs>(GetTickCount64());
#else
    using namespace std::chrono;
    return static_cast<TickMs>(
        duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count());
#endif
}

TickMs elapsed_ms(TickMs now, TickMs start) {
    return (now >= start) ? (now - start) : 0U;
}

#ifdef _WIN32
volatile LONG g_console_stop_requested = 0;

BOOL WINAPI console_ctrl_handler(DWORD ctrl_type) {
    if ((ctrl_type == CTRL_C_EVENT) || (ctrl_type == CTRL_BREAK_EVENT) || (ctrl_type == CTRL_CLOSE_EVENT)) {
        InterlockedExchange(&g_console_stop_requested, 1);
        return TRUE;
    }
    return FALSE;
}

bool console_stop_requested() {
    return InterlockedCompareExchange(&g_console_stop_requested, 0, 0) != 0;
}
#else
volatile std::sig_atomic_t g_console_stop_requested = 0;

void console_ctrl_handler(int) {
    g_console_stop_requested = 1;
}

bool console_stop_requested() {
    return g_console_stop_requested != 0;
}
#endif

struct ConsoleCtrlGuard {
    bool installed = false;

    ConsoleCtrlGuard() {
#ifdef _WIN32
        installed = SetConsoleCtrlHandler(console_ctrl_handler, TRUE) != 0;
#else
        struct sigaction action {};
        action.sa_handler = console_ctrl_handler;
        sigemptyset(&action.sa_mask);
        installed = (sigaction(SIGINT, &action, nullptr) == 0);
#endif
    }

    ~ConsoleCtrlGuard() {
#ifdef _WIN32
        if (installed) {
            SetConsoleCtrlHandler(console_ctrl_handler, FALSE);
        }
#else
        if (installed) {
            struct sigaction action {};
            action.sa_handler = SIG_DFL;
            sigemptyset(&action.sa_mask);
            sigaction(SIGINT, &action, nullptr);
        }
#endif
    }
};

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 >> 8U) << "."
              << static_cast<unsigned>(info.fpga_ver & 0xFFU) << "\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";
}

void print_summary(const RunningStats& stats) {
    if (stats.count == 0U) {
        std::cout << "No complete LCHM windows captured\n";
        return;
    }

    const auto avg = [count = stats.count](uint64_t sum) {
        return static_cast<double>(sum) / static_cast<double>(count);
    };

    std::cout << std::fixed << std::setprecision(3)
              << "Summary: windows=" << stats.count << "\n"
              << "  clocks: min=" << stats.clocks_min
              << ", avg=" << avg(stats.clocks_sum)
              << ", max=" << stats.clocks_max << "\n"
              << "  di2_high_clocks: min=" << stats.high_min
              << ", avg=" << avg(stats.high_sum)
              << ", max=" << stats.high_max << "\n"
              << "  di2_low_clocks: min=" << stats.low_min
              << ", avg=" << avg(stats.low_sum)
              << ", max=" << stats.low_max << "\n";
}

int run(const Config& cfg) {
    if (cfg.legacy_clock_arg_used) {
        std::cerr << "Warning: legacy argument clock:" << cfg.legacy_clock_arg
                  << " is ignored. lchm_clock_counter uses internal clock only.\n";
    }
    if (cfg.legacy_pullup_syn1) {
        std::cerr << "Warning: legacy argument pullup_syn1 is ignored in internal-only mode.\n";
    }
    if (cfg.legacy_clean_start) {
        std::cerr << "Warning: legacy argument clean_start is ignored. "
                  << "LCHM starts strictly on DI_SYN2 low->high.\n";
    }
    if (cfg.legacy_duration_ms) {
        std::cerr << "Warning: legacy argument duration_ms:" << cfg.duration_ms
                  << " is ignored. LCHM closes only on DI_SYN2 high->low.\n";
    }

    Api api;
    DeviceHandle device(api);

    int32_t open_err = X502_ERR_OK;
    if (cfg.ip_addr.has_value()) {
        open_err = api.OpenByIpAddr(device.hnd, *cfg.ip_addr, 0, 5000);
    } else {
        open_err = api.OpenUsb(device.hnd, cfg.serial.empty() ? nullptr : cfg.serial.c_str());
    }
    expect_ok(api, open_err, cfg.ip_addr.has_value()
        ? ("Open device by IP " + ipv4_to_string(*cfg.ip_addr))
        : std::string("Open device over USB"));
    device.opened = true;

    t_x502_info info {};
    int32_t info_err = X502_ERR_OK;
    if (api.GetDevInfo2 != nullptr) {
        info_err = api.GetDevInfo2(device.hnd, &info, sizeof(info));
    } else {
        info_err = api.GetDevInfo(device.hnd, &info);
    }
    expect_ok(api, info_err, "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, X502_SYNC_INTERNAL), "Set internal sync mode");
    expect_ok(api, api.SetSyncStartMode(device.hnd, X502_SYNC_INTERNAL), "Set immediate stream start");
    expect_ok(api, api.SetRefFreq(device.hnd, kInternalRefSetting), "Set internal reference frequency");
    double actual_din_freq_hz = cfg.clock_hz;
    expect_ok(api, api.SetDinFreq(device.hnd, &actual_din_freq_hz), "Set DIN frequency");
    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_syn2) {
        pullups |= X502_PULLUPS_DI_SYN2;
    }
    expect_ok(api, api.SetDigInPullup(device.hnd, pullups), "Set digital input pullups");

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

    std::cout << "LCHM clock counter settings:\n"
              << "  clock source: internal\n"
              << "  internal ref: " << kInternalRefHz << " Hz\n"
              << "  requested DIN clock: " << cfg.clock_hz << " Hz\n"
              << "  effective DIN clock: " << actual_din_freq_hz << " Hz\n"
              << "  LCHM gate: strict DI_SYN2 low->high start, high->low stop\n"
              << "  DI1 step segmentation: both edges\n"
              << "  DI2 split: enabled per step and per full LCHM\n"
              << "  duration limit: disabled (legacy duration_ms ignored)"
              << "\n"
              << "  target windows: " << cfg.windows << "\n"
              << "  recv block words: " << cfg.recv_block_words << "\n"
              << "  input step words: " << cfg.input_step_words << "\n"
              << "  input buffer words: " << cfg.input_buffer_words << "\n";
    if (std::fabs(actual_din_freq_hz - cfg.clock_hz) > std::max(0.5, cfg.clock_hz * 1e-6)) {
        std::cerr << "Warning: effective DIN clock differs from requested value: requested="
                  << cfg.clock_hz << " Hz, effective=" << actual_din_freq_hz << " Hz\n";
    }

    ConsoleCtrlGuard console_guard;
    if (!console_guard.installed) {
        std::cerr << "Warning: Ctrl+C handler could not be installed; stop may be abrupt.\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<uint32_t> din_buffer(cfg.recv_block_words);

    bool gate_initialized = false;
    bool last_gate = false;
    bool di1_initialized = false;
    bool last_di1_level = false;
    bool in_lchm = false;
    LchmClockCount current;
    LchmClockCount current_step;
    bool current_step_level = false;
    std::vector<Di1StepClockCount> current_steps;
    RunningStats stats;

    const TickMs session_start = tick_count_ms();
    TickMs last_stream_activity = session_start;
    TickMs last_lchm_complete = session_start;
    TickMs last_din_activity = session_start;
    TickMs last_gate_edge = session_start;

    uint64_t total_raw_words = 0;
    uint64_t total_din_words = 0;
    uint64_t total_gate_edges = 0;

    auto start_lchm = [&](bool di1_level) {
        in_lchm = true;
        current.clear();
        current_step.clear();
        current_step_level = di1_level;
        current_steps.clear();
    };

    auto finalize_current_step = [&]() {
        if (current_step.clocks == 0U) {
            return;
        }
        Di1StepClockCount step;
        step.di1_level = current_step_level;
        step.count = current_step;
        current_steps.push_back(step);
        current_step.clear();
    };

    auto finalize_lchm = [&](const char* close_reason, TickMs now) {
        finalize_current_step();

        if (current.clocks != (current.di2_high_clocks + current.di2_low_clocks)) {
            std::ostringstream message;
            message << "DI2 clock split invariant failed: clocks=" << current.clocks
                    << ", high=" << current.di2_high_clocks
                    << ", low=" << current.di2_low_clocks;
            fail(message.str());
        }

        uint64_t step_sum_clocks = 0;
        uint64_t step_sum_high = 0;
        uint64_t step_sum_low = 0;
        for (const auto& step : current_steps) {
            step_sum_clocks += step.count.clocks;
            step_sum_high += step.count.di2_high_clocks;
            step_sum_low += step.count.di2_low_clocks;
        }
        if ((step_sum_clocks != current.clocks) ||
            (step_sum_high != current.di2_high_clocks) ||
            (step_sum_low != current.di2_low_clocks)) {
            std::ostringstream message;
            message << "DI1 step split invariant failed: total clocks/high/low="
                    << current.clocks << "/" << current.di2_high_clocks << "/" << current.di2_low_clocks
                    << ", step sum clocks/high/low="
                    << step_sum_clocks << "/" << step_sum_high << "/" << step_sum_low;
            fail(message.str());
        }

        if (current.clocks != 0U) {
            const uint64_t lchm_index = stats.count + 1U;
            stats.add(current);
            std::cout << "LCHM " << lchm_index
                      << ": clocks=" << current.clocks
                      << ", di2_high_clocks=" << current.di2_high_clocks
                      << ", di2_low_clocks=" << current.di2_low_clocks
                      << ", close_reason=" << close_reason
                      << "\n";
            for (std::size_t step_index = 0; step_index < current_steps.size(); ++step_index) {
                const auto& step = current_steps[step_index];
                std::cout << "  step " << (step_index + 1U)
                          << ": di1_level=" << (step.di1_level ? "HIGH" : "LOW")
                          << ", clocks=" << step.count.clocks
                          << ", di2_high_clocks=" << step.count.di2_high_clocks
                          << ", di2_low_clocks=" << step.count.di2_low_clocks
                          << "\n";
            }
            last_lchm_complete = now;
        }
        current.clear();
        current_step.clear();
        current_steps.clear();
    };

    auto fail_waiting_for_lchm = [&](TickMs now) {
        std::ostringstream message;
        message << "ADC/DIN clock is present, but no complete DI_SYN2 LCHM window was captured within "
                << cfg.lchm_wait_ms << " ms. "
                << "DIN samples=" << total_din_words
                << ", gate edges=" << total_gate_edges << ". ";

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

        message << "Check DI_SYN2 wiring, common DGND, and signal level around 0/3.3 V. "
                << "Progress: last DIN activity " << elapsed_ms(now, last_din_activity)
                << " ms ago, last DI_SYN2 edge " << elapsed_ms(now, last_gate_edge) << " ms ago.";
        fail(message.str());
    };

    while ((stats.count < cfg.windows) && !console_stop_requested()) {
        uint32_t recv_request_words = cfg.recv_block_words;
        uint32_t recv_timeout_ms = cfg.recv_timeout_ms;
        uint32_t ready_words = 0;
        const int32_t ready_err = api.GetRecvReadyCount(device.hnd, &ready_words);
        if ((ready_err == X502_ERR_OK) && (ready_words != 0U)) {
            recv_request_words = std::min<uint32_t>(ready_words, cfg.recv_block_words);
            recv_timeout_ms = 0;
        }

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

        const TickMs now = tick_count_ms();
        if (recvd == 0) {
            if (elapsed_ms(now, last_stream_activity) >= cfg.clock_wait_ms) {
                fail("Timeout waiting for DIN stream data in internal clock mode. "
                     "Check device state and DIN stream configuration.");
            }
            if (elapsed_ms(now, last_lchm_complete) >= cfg.lchm_wait_ms) {
                fail_waiting_for_lchm(now);
            }
            continue;
        }

        last_stream_activity = now;
        total_raw_words += static_cast<uint64_t>(recvd);

        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),
                                  0U,
                                  nullptr,
                                  nullptr,
                                  din_buffer.data(),
                                  &din_count),
                  "Process DIN data");

        if (din_count != 0U) {
            total_din_words += din_count;
            last_din_activity = now;
        }

        for (uint32_t i = 0; (i < din_count) && (stats.count < cfg.windows); ++i) {
            const uint32_t din_value = din_buffer[i];
            const bool gate = (din_value & kE502DiSyn2Mask) != 0U;
            const bool di1_level = (din_value & kE502Digital1Mask) != 0U;
            const bool di2_high = (din_value & kE502Digital2Mask) != 0U;
            bool di1_changed = false;

            if (!di1_initialized) {
                di1_initialized = true;
                last_di1_level = di1_level;
            } else if (di1_level != last_di1_level) {
                di1_changed = true;
                last_di1_level = di1_level;
            }

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

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

            if (!in_lchm && gate_initialized && !last_gate && gate) {
                start_lchm(di1_level);
            }

            if (in_lchm && last_gate && !gate) {
                finalize_lchm("di_syn2_fall", now);
                in_lchm = false;
            }

            if (in_lchm && gate) {
                if (di1_changed && (current_step.clocks != 0U)) {
                    finalize_current_step();
                    current_step_level = di1_level;
                }
                current.add(di2_high);
                current_step.add(di2_high);
            }

            last_gate = gate;
        }

        const TickMs now_after_block = tick_count_ms();
        if ((stats.count < cfg.windows) && (elapsed_ms(now_after_block, last_lchm_complete) >= cfg.lchm_wait_ms)) {
            fail_waiting_for_lchm(now_after_block);
        }
    }

    if (console_stop_requested() && in_lchm) {
        finalize_lchm("user_stop", tick_count_ms());
    }

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

    std::cout << "Raw words read: " << total_raw_words
              << ", DIN samples: " << total_din_words
              << ", DI_SYN2 edges: " << total_gate_edges << "\n";
    print_summary(stats);

    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;
    }
}
