/** @file
    SDR input from RTLSDR or SoapySDR.

    Copyright (C) 2018 Christian Zuckschwerdt
    based on code
    Copyright (C) 2012 by Steve Markgraf <steve@steve-m.de>
    Copyright (C) 2014 by Kyle Keen <keenerd@gmail.com>
    Copyright (C) 2016 by Robert X. Seger

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include "sdr.h"
#include "r_util.h"
#include "optparse.h"
#include "logger.h"
#include "fatal.h"
#include "compat_pthread.h"
#ifdef RTLSDR
#include <rtl-sdr.h>
#if defined(__linux__) && (defined(__GNUC__) || defined(__clang__))
// not available in rtlsdr 0.5.3, allow weak link for Linux
int __attribute__((weak)) rtlsdr_set_bias_tee(rtlsdr_dev_t *dev, int on);
#endif
#ifdef LIBUSB1
#include <libusb.h> /* libusb_error_name(), libusb_strerror() */
#endif
#endif
#ifdef SOAPYSDR
#include <SoapySDR/Version.h>
#include <SoapySDR/Device.h>
#include <SoapySDR/Formats.h>
#include <SoapySDR/Logger.h>
#if (SOAPY_SDR_API_VERSION < 0x00080000)
#define SoapySDR_free(ptr) free(ptr)
#endif
#endif

#ifndef _MSC_VER
#include <unistd.h>
#endif

#ifdef _WIN32
    #if !defined(_WIN32_WINNT) || (_WIN32_WINNT < 0x0600)
    #undef _WIN32_WINNT
    #define _WIN32_WINNT 0x0600   /* Needed to pull in 'struct sockaddr_storage' */
    #endif

    #include <winsock2.h>
    #include <ws2tcpip.h>
    #define SHUT_RDWR SD_BOTH
    #define perror(str)  ws2_perror(str)

    static void ws2_perror (const char *str)
    {
        if (str && *str)
            fprintf(stderr, "%s: ", str);
        fprintf(stderr, "Winsock error %d.\n", WSAGetLastError());
    }
#else
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netdb.h>
    #include <netinet/in.h>

    #define SOCKET          int
    #define INVALID_SOCKET  (-1)
    #define closesocket(x)  close(x)
#endif

#define GAIN_STR_MAX_SIZE 64

struct sdr_dev {
    SOCKET rtl_tcp;
    uint32_t rtl_tcp_freq; ///< last known center frequency, rtl_tcp only.
    uint32_t rtl_tcp_rate; ///< last known sample rate, rtl_tcp only.

#ifdef SOAPYSDR
    SoapySDRDevice *soapy_dev;
    SoapySDRStream *soapy_stream;
    double fullScale;
#endif

#ifdef RTLSDR
    rtlsdr_dev_t *rtlsdr_dev;
    sdr_event_cb_t rtlsdr_cb;
    void *rtlsdr_cb_ctx;
#endif

    char *dev_info;

    int running;
    uint8_t *buffer; ///< sdr data buffer current and past frames
    size_t buffer_size; ///< sdr data buffer overall size (num * len)
    size_t buffer_pos; ///< sdr data buffer next write position

    int sample_size;
    int sample_signed;

    uint32_t sample_rate;
    uint32_t center_frequency;

#ifdef THREADS
    pthread_t thread;
    pthread_mutex_t lock; ///< lock for exit_acquire
    int exit_acquire;

    // acquire thread args
    sdr_event_cb_t async_cb;
    void *async_ctx;
    uint32_t buf_num;
    uint32_t buf_len;
#endif
};

/* rtl_tcp helpers */

#pragma pack(push, 1)
struct rtl_tcp_info {
    char magic[4];             // "RTL0"
    uint32_t tuner_number;     // big endian
    uint32_t tuner_gain_count; // big endian
};
#pragma pack(pop)

static int rtltcp_open(sdr_dev_t **out_dev, char const *dev_query, int verbose)
{
    UNUSED(verbose);
    char const *host = "localhost";
    char const *port = "1234";
    char hostport[280]; // 253 chars DNS name plus extra chars

    char *param = arg_param(dev_query); // strip scheme
    hostport[0] = '\0';
    if (param) {
        snprintf(hostport, sizeof(hostport), "%s", param);
    }
    hostport_param(hostport, &host, &port);

    print_logf(LOG_CRITICAL, "SDR", "rtl_tcp input from %s port %s", host, port);

#ifdef _WIN32
    WSADATA wsa;

    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        perror("WSAStartup()");
        return -1;
    }
#endif

    struct addrinfo hints, *res, *res0;
    int ret;
    SOCKET sock;

    memset(&hints, 0, sizeof(hints));
    hints.ai_family   = PF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = 0;
    hints.ai_flags    = AI_ADDRCONFIG;

    ret = getaddrinfo(host, port, &hints, &res0);
    if (ret) {
        print_log(LOG_ERROR, __func__, gai_strerror(ret));
        return -1;
    }
    sock = INVALID_SOCKET;
    for (res = res0; res; res = res->ai_next) {
        sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (sock >= 0) {
            ret = connect(sock, res->ai_addr, res->ai_addrlen);
            if (ret == -1) {
                perror("connect");
                closesocket(sock);
                sock = INVALID_SOCKET;
            }
            else
                break; // success
        }
    }
    freeaddrinfo(res0);
    if (sock == INVALID_SOCKET) {
        perror("socket");
        return -1;
    }

    //int const value_one = 1;
    //ret = setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *)&value_one, sizeof(value_one));
    //if (ret < 0)
    //    fprintf(stderr, "rtl_tcp TCP_NODELAY failed\n");

    struct rtl_tcp_info info;
    ret = recv(sock, (char *)&info, sizeof (info), 0);
    if (ret != 12) {
        print_logf(LOG_ERROR, __func__, "Bad rtl_tcp header (%d)", ret);
        return -1;
    }
    if (strncmp(info.magic, "RTL0", 4)) {
        info.tuner_number = 0; // terminate magic
        print_logf(LOG_ERROR, __func__, "Bad rtl_tcp header magic \"%s\"", info.magic);
        return -1;
    }

    unsigned tuner_number = ntohl(info.tuner_number);
    //int tuner_gain_count  = ntohl(info.tuner_gain_count);

    char const *tuner_names[] = { "Unknown", "E4000", "FC0012", "FC0013", "FC2580", "R820T", "R828D" };
    char const *tuner_name = tuner_number > sizeof (tuner_names) ? "Invalid" : tuner_names[tuner_number];

    print_logf(LOG_CRITICAL, "SDR", "rtl_tcp connected to %s:%s (Tuner: %s)", host, port, tuner_name);

    sdr_dev_t *dev = calloc(1, sizeof(sdr_dev_t));
    if (!dev) {
        WARN_CALLOC("rtltcp_open()");
        return -1; // NOTE: returns error on alloc failure.
    }
#ifdef THREADS
    pthread_mutex_init(&dev->lock, NULL);
#endif

    dev->rtl_tcp = sock;
    dev->sample_size = sizeof(uint8_t) * 2; // CU8
    dev->sample_signed = 0;

    *out_dev = dev;
    return 0;
}

static int rtltcp_close(SOCKET sock)
{
    int ret = shutdown(sock, SHUT_RDWR);
    if (ret == -1) {
        perror("shutdown");
        return -1;
    }

    ret = closesocket(sock);
    if (ret == -1) {
        perror("close");
        return -1;
    }

    return 0;
}

static int rtltcp_read_loop(sdr_dev_t *dev, sdr_event_cb_t cb, void *ctx, uint32_t buf_num, uint32_t buf_len)
{
    size_t buffer_size = (size_t)buf_num * buf_len;
    if (dev->buffer_size != buffer_size) {
        free(dev->buffer);
        dev->buffer = malloc(buffer_size);
        if (!dev->buffer) {
            WARN_MALLOC("rtltcp_read_loop()");
            return -1; // NOTE: returns error on alloc failure.
        }
        dev->buffer_size = buffer_size;
        dev->buffer_pos = 0;
    }

    dev->running = 1;
    do {
        if (dev->buffer_pos + buf_len > buffer_size)
            dev->buffer_pos = 0;
        uint8_t *buffer = &dev->buffer[dev->buffer_pos];
        dev->buffer_pos += buf_len;

        unsigned n_read = 0;
        int r;

        do {
            r = recv(dev->rtl_tcp, &buffer[n_read], buf_len - n_read, MSG_WAITALL);
            if (r <= 0)
                break;
            n_read += r;
            //fprintf(stderr, "readStream ret=%d (of %u)\n", r, n_read);
        } while (n_read < buf_len);
        //fprintf(stderr, "readStream ret=%d (read %u)\n", r, n_read);

        if (r < 0) {
            print_logf(LOG_WARNING, __func__, "sync read failed. %d", r);
        }
        if (n_read == 0) {
            perror("rtl_tcp");
            dev->running = 0;
        }

#ifdef THREADS
        pthread_mutex_lock(&dev->lock);
#endif
        uint32_t sample_rate      = dev->sample_rate;
        uint32_t center_frequency = dev->center_frequency;
#ifdef THREADS
        pthread_mutex_unlock(&dev->lock);
#endif
        sdr_event_t ev = {
                .ev               = SDR_EV_DATA,
                .sample_rate      = sample_rate,
                .center_frequency = center_frequency,
                .buf              = buffer,
                .len              = n_read,
        };
#ifdef THREADS
        pthread_mutex_lock(&dev->lock);
        int exit_acquire = dev->exit_acquire;
        pthread_mutex_unlock(&dev->lock);
        if (exit_acquire) {
            break; // do not deliver any more events
        }
#endif
        if (n_read > 0) // prevent a crash in callback
            cb(&ev, ctx);

    } while (dev->running);

    return 0;
}

#pragma pack(push, 1)
struct command {
    unsigned char cmd;
    unsigned int param;
};
#pragma pack(pop)

// rtl_tcp API
#define RTLTCP_SET_FREQ 0x01
#define RTLTCP_SET_SAMPLE_RATE 0x02
#define RTLTCP_SET_GAIN_MODE 0x03
#define RTLTCP_SET_GAIN 0x04
#define RTLTCP_SET_FREQ_CORRECTION 0x05
#define RTLTCP_SET_IF_TUNER_GAIN 0x06
#define RTLTCP_SET_TEST_MODE 0x07
#define RTLTCP_SET_AGC_MODE 0x08
#define RTLTCP_SET_DIRECT_SAMPLING 0x09
#define RTLTCP_SET_OFFSET_TUNING 0x0a
#define RTLTCP_SET_RTL_XTAL 0x0b
#define RTLTCP_SET_TUNER_XTAL 0x0c
#define RTLTCP_SET_TUNER_GAIN_BY_ID 0x0d
#define RTLTCP_SET_BIAS_TEE 0x0e

static int rtltcp_command(sdr_dev_t *dev, char cmd, int param)
{
    struct command command;
    command.cmd   = cmd;
    command.param = htonl(param);

    return sizeof(command) == send(dev->rtl_tcp, (const char*) &command, sizeof(command), 0) ? 0 : -1;
}

/* RTL-SDR helpers */

#ifdef RTLSDR

static int sdr_open_rtl(sdr_dev_t **out_dev, char const *dev_query, int verbose)
{
    uint32_t device_count = rtlsdr_get_device_count();
    if (!device_count) {
        print_log(LOG_CRITICAL, "SDR", "No supported devices found.");
        return -1;
    }

    if (verbose)
        print_logf(LOG_NOTICE, "SDR", "Found %u device(s)", device_count);

    int dev_index = 0;
    // select rtlsdr device by serial (-d :<serial>)
    if (dev_query && *dev_query == ':') {
        dev_index = rtlsdr_get_index_by_serial(&dev_query[1]);
        if (dev_index < 0) {
            if (verbose)
                print_logf(LOG_ERROR, "SDR", "Could not find device with serial '%s' (err %d)",
                        &dev_query[1], dev_index);
            return -1;
        }
    }

    // select rtlsdr device by number (-d <n>)
    else if (dev_query) {
        dev_index = atoi(dev_query);
        // check if 0 is a parsing error?
        if (dev_index < 0) {
            // select first available rtlsdr device
            dev_index = 0;
            dev_query = NULL;
        }
    }

    char vendor[256] = "n/a", product[256] = "n/a", serial[256] = "n/a";
    int r = -1;
    sdr_dev_t *dev = calloc(1, sizeof(sdr_dev_t));
    if (!dev) {
        WARN_CALLOC("sdr_open_rtl()");
        return -1; // NOTE: returns error on alloc failure.
    }
#ifdef THREADS
    pthread_mutex_init(&dev->lock, NULL);
#endif

    for (uint32_t i = dev_query ? dev_index : 0;
            //cast quiets -Wsign-compare; if dev_index were < 0, would have returned -1 above
            i < (dev_query ? (unsigned)dev_index + 1 : device_count);
            i++) {
        rtlsdr_get_device_usb_strings(i, vendor, product, serial);

        if (verbose)
            print_logf(LOG_NOTICE, "SDR", "trying device %u: %s, %s, SN: %s",
                    i, vendor, product, serial);

        r = rtlsdr_open(&dev->rtlsdr_dev, i);
        if (r < 0) {
            if (verbose)
                print_logf(LOG_ERROR, __func__, "Failed to open rtlsdr device #%u.", i);
        }
        else {
            if (verbose)
                print_logf(LOG_CRITICAL, "SDR", "Using device %u: %s, %s, SN: %s, \"%s\"",
                        i, vendor, product, serial, rtlsdr_get_device_name(i));
            dev->sample_size = sizeof(uint8_t) * 2; // CU8
            dev->sample_signed = 0;

            size_t info_len = 41 + strlen(vendor) + strlen(product) + strlen(serial);
            dev->dev_info = malloc(info_len);
            if (!dev->dev_info)
                FATAL_MALLOC("sdr_open_rtl");
            snprintf(dev->dev_info, info_len, "{\"vendor\":\"%s\", \"product\":\"%s\", \"serial\":\"%s\"}",
                    vendor, product, serial);
            break;
        }
    }
    if (r < 0) {
        free(dev);
        if (verbose)
            print_log(LOG_ERROR, __func__, "Unable to open a device");
    }
    else {
        *out_dev = dev;
    }
    return r;
}

static int rtlsdr_find_tuner_gain(sdr_dev_t *dev, int centigain, int verbose)
{
    /* Get allowed gains */
    int gains_count = rtlsdr_get_tuner_gains(dev->rtlsdr_dev, NULL);
    if (gains_count < 0) {
        if (verbose)
            print_log(LOG_WARNING, __func__, "Unable to get exact gains");
        return centigain;
    }
    if (gains_count < 1) {
        if (verbose)
            print_log(LOG_WARNING, __func__, "No exact gains");
        return centigain;
    }
    if (gains_count > 29) {
        print_log(LOG_ERROR, __func__, "Unexpected gain count, notify maintainers please!");
        return centigain;
    }
    // We known the maximum nunmber of gains is 29.
    // Let's not waste an alloc
    int gains[29] = {0};
    rtlsdr_get_tuner_gains(dev->rtlsdr_dev, gains);

    /* Find allowed gain */
    for (int i = 0; i < gains_count; ++i) {
        if (centigain <= gains[i]) {
            centigain = gains[i];
            break;
        }
    }
    if (centigain > gains[gains_count - 1]) {
        centigain = gains[gains_count - 1];
    }

    return centigain;
}

static void rtlsdr_read_cb(unsigned char *iq_buf, uint32_t len, void *ctx)
{
    sdr_dev_t *dev = ctx;

    //fprintf(stderr, "rtlsdr_read_cb enter...\n");
#ifdef THREADS
    pthread_mutex_lock(&dev->lock);
    int exit_acquire = dev->exit_acquire;
    pthread_mutex_unlock(&dev->lock);
    if (exit_acquire) {
        // we get one more call after rtlsdr_cancel_async(),
        // it then takes a full second until rtlsdr_read_async() ends.
        //fprintf(stderr, "rtlsdr_read_cb stopping...\n");
        return; // do not deliver any more events
    }
#endif

    if (dev->buffer_pos + len > dev->buffer_size)
        dev->buffer_pos = 0;
    uint8_t *buffer = &dev->buffer[dev->buffer_pos];
    dev->buffer_pos += len;

    // NOTE: we need to copy the buffer, it might go away on cancel_async
    memcpy(buffer, iq_buf, len);

#ifdef THREADS
    pthread_mutex_lock(&dev->lock);
#endif
    uint32_t sample_rate      = dev->sample_rate;
    uint32_t center_frequency = dev->center_frequency;
#ifdef THREADS
    pthread_mutex_unlock(&dev->lock);
#endif
    sdr_event_t ev = {
            .ev               = SDR_EV_DATA,
            .sample_rate      = sample_rate,
            .center_frequency = center_frequency,
            .buf              = buffer,
            .len              = len,
    };
    //fprintf(stderr, "rtlsdr_read_cb cb...\n");
    if (len > 0) // prevent a crash in callback
        dev->rtlsdr_cb(&ev, dev->rtlsdr_cb_ctx);
    //fprintf(stderr, "rtlsdr_read_cb cb done.\n");
    // NOTE: we actually need to copy the buffer to prevent it going away on cancel_async
}

static int rtlsdr_read_loop(sdr_dev_t *dev, sdr_event_cb_t cb, void *ctx, uint32_t buf_num, uint32_t buf_len)
{
    size_t buffer_size = (size_t)buf_num * buf_len;
    if (dev->buffer_size != buffer_size) {
        free(dev->buffer);
        dev->buffer = malloc(buffer_size);
        if (!dev->buffer) {
            WARN_MALLOC("rtlsdr_read_loop()");
            return -1; // NOTE: returns error on alloc failure.
        }
        dev->buffer_size = buffer_size;
        dev->buffer_pos = 0;
    }

    int r = 0;

    dev->rtlsdr_cb = cb;
    dev->rtlsdr_cb_ctx = ctx;

    dev->running = 1;

        r = rtlsdr_read_async(dev->rtlsdr_dev, rtlsdr_read_cb, dev, buf_num, buf_len);
        // rtlsdr_read_async() returns possible error codes from:
        //     if (!dev) return -1;
        //     if (RTLSDR_INACTIVE != dev->async_status) return -2;
        //     r = libusb_submit_transfer(dev->xfer[i]);
        //     r = libusb_handle_events_timeout_completed(dev->ctx, &tv,
        //     r = libusb_cancel_transfer(dev->xfer[i]);
        // We can safely assume it's an libusb error.
        if (r < 0) {
#ifdef LIBUSB1
            print_logf(LOG_ERROR, __func__, "%s: %s! "
                            "Check your RTL-SDR dongle, USB cables, and power supply.",
                    libusb_error_name(r), libusb_strerror(r));
#else
            print_logf(LOG_ERROR, __func__, "LIBUSB_ERROR: %d! "
                            "Check your RTL-SDR dongle, USB cables, and power supply.",
                    r);
#endif
            dev->running = 0;
        }
    print_log(LOG_DEBUG, __func__, "rtlsdr_read_async done");

    return r;
}

#endif

/* SoapySDR helpers */

#ifdef SOAPYSDR

static int soapysdr_set_bandwidth(SoapySDRDevice *dev, uint32_t bandwidth)
{
    int r;
    r = (int)SoapySDRDevice_setBandwidth(dev, SOAPY_SDR_RX, 0, (double)bandwidth);
    uint32_t applied_bw = 0;
    if (r != 0) {
        print_log(LOG_WARNING, "SDR", "Failed to set bandwidth.");
    }
    else if (bandwidth > 0) {
        applied_bw = (uint32_t)SoapySDRDevice_getBandwidth(dev, SOAPY_SDR_RX, 0);
        if (applied_bw)
            print_logf(LOG_NOTICE, "SDR", "Bandwidth parameter %u Hz resulted in %u Hz.", bandwidth, applied_bw);
        else
            print_logf(LOG_NOTICE, "SDR", "Set bandwidth parameter %u Hz.", bandwidth);
    }
    else {
        print_logf(LOG_NOTICE, "SDR", "Bandwidth set to automatic resulted in %u Hz.", applied_bw);
    }
    return r;
}

static int soapysdr_direct_sampling(SoapySDRDevice *dev, int on)
{
    int r = 0;
    char const *value;
    if (on == 0)
        value = "0";
    else if (on == 1)
        value = "1";
    else if (on == 2)
        value = "2";
    else
        return -1;
    SoapySDRDevice_writeSetting(dev, "direct_samp", value);
    char *set_value = SoapySDRDevice_readSetting(dev, "direct_samp");

    if (set_value == NULL) {
        print_log(LOG_ERROR, __func__, "Failed to set direct sampling moden");
        return r;
    }
    int set_num = atoi(set_value);
    if (set_num == 0) {
        print_log(LOG_CRITICAL, "SDR", "Direct sampling mode disabled.");}
    else if (set_num == 1) {
        print_log(LOG_CRITICAL, "SDR", "Enabled direct sampling mode, input 1/I.");}
    else if (set_num == 2) {
        print_log(LOG_CRITICAL, "SDR", "Enabled direct sampling mode, input 2/Q.");}
    else if (set_num == 3) {
        print_log(LOG_CRITICAL, "SDR", "Enabled no-mod direct sampling mode.");}
    SoapySDR_free(set_value);
    return r;
}

static int soapysdr_offset_tuning(SoapySDRDevice *dev)
{
    int r = 0;
    SoapySDRDevice_writeSetting(dev, "offset_tune", "true");
    char *set_value = SoapySDRDevice_readSetting(dev, "offset_tune");

    if (strcmp(set_value, "true") != 0) {
        /* TODO: detection of failure modes
        if (r == -2)
            print_log(LOG_WARNING, __func__, "Failed to set offset tuning: tuner doesn't support offset tuning!");
        else if (r == -3)
            print_log(LOG_WARNING, __func__, "Failed to set offset tuning: direct sampling not combinable with offset tuning!");
        else
        */
            print_log(LOG_WARNING, __func__, "Failed to set offset tuning.");
    }
    else {
        print_log(LOG_CRITICAL, "SDR", "Offset tuning mode enabled.");
    }
    SoapySDR_free(set_value);
    return r;
}

static int soapysdr_auto_gain(SoapySDRDevice *dev, int verbose)
{
    int r = 0;

    r = SoapySDRDevice_hasGainMode(dev, SOAPY_SDR_RX, 0);
    if (r) {
        r = SoapySDRDevice_setGainMode(dev, SOAPY_SDR_RX, 0, 1);
        if (r != 0) {
            print_log(LOG_WARNING, __func__, "Failed to enable automatic gain.");
        }
        else {
            if (verbose)
                print_log(LOG_CRITICAL, "SDR", "Tuner set to automatic gain.");
        }
    }

    // Per-driver hacks TODO: clean this up
    char *driver = SoapySDRDevice_getDriverKey(dev);
    if (strcmp(driver, "HackRF") == 0) {
        // HackRF has three gains LNA, VGA, and AMP, setting total distributes amongst, 116.0 dB seems to work well,
        // even though it logs HACKRF_ERROR_INVALID_PARAM? https://github.com/rxseger/rx_tools/issues/9
        // Total gain is distributed amongst all gains, 116 = 37,65,1; the LNA is OK (<40) but VGA is out of range (65 > 62)
        // TODO: generic means to set all gains, of any SDR? string parsing LNA=#,VGA=#,AMP=#?
        r = SoapySDRDevice_setGainElement(dev, SOAPY_SDR_RX, 0, "LNA", 40.); // max 40
        if (r != 0) {
            print_log(LOG_WARNING, __func__, "Failed to set LNA tuner gain.");
        }
        r = SoapySDRDevice_setGainElement(dev, SOAPY_SDR_RX, 0, "VGA", 20.); // max 65
        if (r != 0) {
            print_log(LOG_WARNING, __func__, "Failed to set VGA tuner gain.");
        }
        r = SoapySDRDevice_setGainElement(dev, SOAPY_SDR_RX, 0, "AMP", 0.); // on or off
        if (r != 0) {
            print_log(LOG_WARNING, __func__, "Failed to set AMP tuner gain.");
        }

    }
    SoapySDR_free(driver);
    // otherwise leave unset, hopefully the driver has good defaults

    return r;
}

static int soapysdr_gain_str_set(SoapySDRDevice *dev, char const *gain_str, int verbose)
{
    if (!gain_str || !*gain_str || strlen(gain_str) >= GAIN_STR_MAX_SIZE)
        return -1;

    int r = 0;

    // Disable automatic gain
    r = SoapySDRDevice_hasGainMode(dev, SOAPY_SDR_RX, 0);
    if (r) {
        r = SoapySDRDevice_setGainMode(dev, SOAPY_SDR_RX, 0, 0);
        if (r != 0) {
            print_log(LOG_WARNING, __func__, "Failed to disable automatic gain.");
        }
        else {
            if (verbose)
                print_log(LOG_NOTICE, "SDR", "Tuner set to manual gain.");
        }
    }

    if (strchr(gain_str, '=')) {
        char gain_cpy[GAIN_STR_MAX_SIZE];
        snprintf(gain_cpy, sizeof(gain_cpy), "%s", gain_str);
        char *gain_p = gain_cpy;
        // Set each gain individually (more control)
        char *name;
        char *value;
        while (getkwargs(&gain_p, &name, &value)) {
            double num = atof(value);
            if (verbose)
                print_logf(LOG_NOTICE, "SDR", "Setting gain element %s: %f dB", name, num);
            r = SoapySDRDevice_setGainElement(dev, SOAPY_SDR_RX, 0, name, num);
            if (r != 0) {
                print_logf(LOG_WARNING, __func__, "setGainElement(%s, %f) failed: %d", name, num, r);
            }
        }
    }
    else {
        // Set overall gain and let SoapySDR distribute amongst components
        double value = atof(gain_str);
        r = SoapySDRDevice_setGain(dev, SOAPY_SDR_RX, 0, value);
        if (r != 0) {
            print_log(LOG_WARNING, __func__, "Failed to set tuner gain.");
        }
        else {
            if (verbose)
                print_logf(LOG_NOTICE, __func__, "Tuner gain set to %0.2f dB.", value);
        }
        // read back and print each individual gain element
        if (verbose) {
            size_t len = 0;
            char **gains = SoapySDRDevice_listGains(dev, SOAPY_SDR_RX, 0, &len);
            fprintf(stderr, "Gain elements: ");
            for (size_t i = 0; i < len; ++i) {
                double gain = SoapySDRDevice_getGain(dev, SOAPY_SDR_RX, 0);
                fprintf(stderr, "%s=%g ", gains[i], gain);
            }
            fprintf(stderr, "\n");
            SoapySDRStrings_clear(&gains, len);
        }
    }

    return r;
}

static void soapysdr_show_device_info(SoapySDRDevice *dev)
{
    size_t len = 0, i = 0;
    char *hwkey;
    SoapySDRKwargs args;
    char **antennas;
    char **gains;
    char **frequencies;
    SoapySDRRange *frequencyRanges;
    SoapySDRRange *rates;
    SoapySDRRange *bandwidths;
    double fullScale;
    char **stream_formats;
    char *native_stream_format;

    int direction = SOAPY_SDR_RX;
    size_t channel = 0;

    hwkey = SoapySDRDevice_getHardwareKey(dev);
    fprintf(stderr, "Using device %s: ", hwkey);
    SoapySDR_free(hwkey);

    args = SoapySDRDevice_getHardwareInfo(dev);
    for (i = 0; i < args.size; ++i) {
        fprintf(stderr, "%s=%s ", args.keys[i], args.vals[i]);
    }
    fprintf(stderr, "\n");
    SoapySDRKwargs_clear(&args);

    antennas = SoapySDRDevice_listAntennas(dev, direction, channel, &len);
    fprintf(stderr, "Found %zu antenna(s): ", len);
    for (i = 0; i < len; ++i) {
        fprintf(stderr, "%s ", antennas[i]);
    }
    fprintf(stderr, "\n");
    SoapySDRStrings_clear(&antennas, len);

    gains = SoapySDRDevice_listGains(dev, direction, channel, &len);
    fprintf(stderr, "Found %zu gain(s): ", len);
    for (i = 0; i < len; ++i) {
        SoapySDRRange gainRange = SoapySDRDevice_getGainRange(dev, direction, channel);
        fprintf(stderr, "%s %.0f - %.0f (step %.0f) ", gains[i], gainRange.minimum, gainRange.maximum, gainRange.step);
    }
    fprintf(stderr, "\n");
    SoapySDRStrings_clear(&gains, len);

    frequencies = SoapySDRDevice_listFrequencies(dev, direction, channel, &len);
    fprintf(stderr, "Found %zu frequencies: ", len);
    for (i = 0; i < len; ++i) {
        fprintf(stderr, "%s ", frequencies[i]);
    }
    fprintf(stderr, "\n");
    SoapySDRStrings_clear(&frequencies, len);

    frequencyRanges = SoapySDRDevice_getFrequencyRange(dev, direction, channel, &len);
    fprintf(stderr, "Found %zu frequency range(s): ", len);
    for (i = 0; i < len; ++i) {
        fprintf(stderr, "%.0f - %.0f (step %.0f) ", frequencyRanges[i].minimum, frequencyRanges[i].maximum, frequencyRanges[i].step);
    }
    fprintf(stderr, "\n");
    SoapySDR_free(frequencyRanges);

    rates = SoapySDRDevice_getSampleRateRange(dev, direction, channel, &len);
    fprintf(stderr, "Found %zu sample rate range(s): ", len);
    for (i = 0; i < len; ++i) {
        if (rates[i].minimum == rates[i].maximum)
            fprintf(stderr, "%.0f ", rates[i].minimum);
        else
            fprintf(stderr, "%.0f - %.0f (step %.0f) ", rates[i].minimum, rates[i].maximum, rates[i].step);
    }
    fprintf(stderr, "\n");
    SoapySDR_free(rates);

    bandwidths = SoapySDRDevice_getBandwidthRange(dev, direction, channel, &len);
    fprintf(stderr, "Found %zu bandwidth range(s): ", len);
    for (i = 0; i < len; ++i) {
        fprintf(stderr, "%.0f - %.0f (step %.0f) ", bandwidths[i].minimum, bandwidths[i].maximum, bandwidths[i].step);
    }
    fprintf(stderr, "\n");
    SoapySDR_free(bandwidths);

    double bandwidth = SoapySDRDevice_getBandwidth(dev, direction, channel);
    fprintf(stderr, "Found current bandwidth %.0f\n", bandwidth);

    stream_formats = SoapySDRDevice_getStreamFormats(dev, direction, channel, &len);
    fprintf(stderr, "Found %zu stream format(s): ", len);
    for (i = 0; i < len; ++i) {
        fprintf(stderr, "%s ", stream_formats[i]);
    }
    fprintf(stderr, "\n");
    SoapySDRStrings_clear(&stream_formats, len);

    native_stream_format = SoapySDRDevice_getNativeStreamFormat(dev, direction, channel, &fullScale);
    fprintf(stderr, "Found native stream format: %s (full scale: %.1f)\n", native_stream_format, fullScale);
    SoapySDR_free(native_stream_format);
}

static int sdr_open_soapy(sdr_dev_t **out_dev, char const *dev_query, int verbose)
{
    if (verbose)
        SoapySDR_setLogLevel(SOAPY_SDR_DEBUG);

    sdr_dev_t *dev = calloc(1, sizeof(sdr_dev_t));
    if (!dev) {
        WARN_CALLOC("sdr_open_soapy()");
        return -1; // NOTE: returns error on alloc failure.
    }
#ifdef THREADS
    pthread_mutex_init(&dev->lock, NULL);
#endif

    dev->soapy_dev = SoapySDRDevice_makeStrArgs(dev_query);
    if (!dev->soapy_dev) {
        if (verbose)
            print_logf(LOG_ERROR, __func__, "Failed to open sdr device matching '%s'.", dev_query);
        free(dev);
        return -1;
    }

    if (verbose)
        soapysdr_show_device_info(dev->soapy_dev);

    // select a stream format, in preference order: native CU8, CS8, CS16, forced CS16
    // stream_formats = SoapySDRDevice_getStreamFormats(dev->soapy_dev, SOAPY_SDR_RX, 0, &len);
    char *native_format = SoapySDRDevice_getNativeStreamFormat(dev->soapy_dev, SOAPY_SDR_RX, 0, &dev->fullScale);
    char const *selected_format;
    if (!strcmp(SOAPY_SDR_CU8, native_format)) {
        // actually not supported by SoapySDR
        selected_format = SOAPY_SDR_CU8;
        dev->sample_size = sizeof(uint8_t); // CU8
        dev->sample_signed = 0;
    }
//    else if (!strcmp(SOAPY_SDR_CS8, native_format)) {
//        // TODO: CS8 needs conversion to CU8
//        // e.g. RTL-SDR (8 bit), scale is 128.0
//        selected_format = SOAPY_SDR_CS8;
//        dev->sample_size = sizeof(int8_t) * 2; // CS8
//        dev->sample_signed = 1;
//    }
    else if (!strcmp(SOAPY_SDR_CS16, native_format)) {
        // e.g. LimeSDR-mini (12 bit), native scale is 2048.0
        // e.g. SDRplay RSP1A (14 bit), native scale is 32767.0
        selected_format = SOAPY_SDR_CS16;
        dev->sample_size = sizeof(int16_t) * 2; // CS16
        dev->sample_signed = 1;
    }
    else {
        // force CS16
        selected_format = SOAPY_SDR_CS16;
        dev->sample_size = sizeof(int16_t) * 2; // CS16
        dev->sample_signed = 1;
        dev->fullScale = 32768.0; // assume max for SOAPY_SDR_CS16
    }
    SoapySDR_free(native_format);

    SoapySDRKwargs args = SoapySDRDevice_getHardwareInfo(dev->soapy_dev);
    size_t info_len     = 2;
    for (size_t i = 0; i < args.size; ++i) {
        info_len += strlen(args.keys[i]) + strlen(args.vals[i]) + 6;
    }
    char *p = dev->dev_info = malloc(info_len);
    if (!dev->dev_info)
        FATAL_MALLOC("sdr_open_soapy");
    for (size_t i = 0; i < args.size; ++i) {
        p += sprintf(p, "%s\"%s\":\"%s\"", i ? "," : "{", args.keys[i], args.vals[i]);
    }
    sprintf(p, "}");
    SoapySDRKwargs_clear(&args);

    SoapySDRKwargs stream_args = {0};
    int r;
#if SOAPY_SDR_API_VERSION >= 0x00080000
    // API version 0.8
#undef SoapySDRDevice_setupStream
    dev->soapy_stream = SoapySDRDevice_setupStream(dev->soapy_dev, SOAPY_SDR_RX, selected_format, NULL, 0, &stream_args);
    r = dev->soapy_stream == NULL;
#else
    // API version 0.7
    r = SoapySDRDevice_setupStream(dev->soapy_dev, &dev->soapy_stream, SOAPY_SDR_RX, selected_format, NULL, 0, &stream_args);
#endif
    if (r != 0) {
        if (verbose)
            print_log(LOG_ERROR, __func__, "Failed to setup sdr device");
        free(dev);
        return -3;
    }

    *out_dev = dev;
    return 0;
}

// the buffer sizes can't be proven to be correct
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wanalyzer-allocation-size"

static int soapysdr_read_loop(sdr_dev_t *dev, sdr_event_cb_t cb, void *ctx, uint32_t buf_num, uint32_t buf_len)
{
    size_t buffer_size = (size_t)buf_num * buf_len;
    if (dev->buffer_size != buffer_size) {
        free(dev->buffer);
        dev->buffer = malloc(buffer_size);
        if (!dev->buffer) {
            WARN_MALLOC("soapysdr_read_loop()");
            return -1; // NOTE: returns error on alloc failure.
        }
        dev->buffer_size = buffer_size;
        dev->buffer_pos = 0;
    }

    size_t buf_elems = buf_len / dev->sample_size;

    dev->running = 1;
    do {
        if (dev->buffer_pos + buf_len > buffer_size)
            dev->buffer_pos = 0;
        int16_t *buffer = (void *)&dev->buffer[dev->buffer_pos];
        dev->buffer_pos += buf_len;

        void *buffs[]    = {buffer};
        int flags        = 0;
        long long timeNs = 0;
        long timeoutUs   = 1000000; // 1 second
        unsigned n_read  = 0, i;
        int r;

        do {
            buffs[0] = &buffer[n_read * 2];
            r  = SoapySDRDevice_readStream(dev->soapy_dev, dev->soapy_stream, buffs, buf_elems - n_read, &flags, &timeNs, timeoutUs);
            if (r < 0)
                break;
            n_read += r; // r is number of elements read, elements=complex pairs, so buffer length is twice
            //fprintf(stderr, "readStream ret=%d, flags=%d, timeNs=%lld (%zu - %u)\n", r, flags, timeNs, buf_elems, n_read);
        } while (n_read < buf_elems);
        //fprintf(stderr, "readStream ret=%u (%u), flags=%d, timeNs=%lld\n", n_read, buf_len, flags, timeNs);
        if (r < 0) {
            if (r == SOAPY_SDR_OVERFLOW) {
                fprintf(stderr, "O");
                fflush(stderr);
                continue;
            }
            print_logf(LOG_WARNING, __func__, "sync read failed. %d", r);
        }

        // convert to CS16 or CU8 if needed
        // if converting CS8 to CU8 -- vectorized with -O3
        //for (i = 0; i < n_read * 2; ++i)
        //    cu8buf[i] = (int8_t)cu8buf[i] + 128;

        // TODO: SoapyRemote doesn't scale properly when reading (local) CS16 from (remote) CS8
        // rescale cs16 buffer
        if (dev->fullScale >= 2047.0 && dev->fullScale <= 2048.0) {
            for (i = 0; i < n_read * 2; ++i)
                buffer[i] *= 16; // prevent left shift of negative value
        }
        else if (dev->fullScale < 32767.0) {
            int upscale = 32768 / dev->fullScale;
            for (i = 0; i < n_read * 2; ++i)
                buffer[i] *= upscale;
        }

#ifdef THREADS
        pthread_mutex_lock(&dev->lock);
#endif
        uint32_t sample_rate      = dev->sample_rate;
        uint32_t center_frequency = dev->center_frequency;
#ifdef THREADS
        pthread_mutex_unlock(&dev->lock);
#endif
        sdr_event_t ev = {
                .ev               = SDR_EV_DATA,
                .sample_rate      = sample_rate,
                .center_frequency = center_frequency,
                .buf              = buffer,
                .len              = n_read * dev->sample_size,
        };
#ifdef THREADS
        pthread_mutex_lock(&dev->lock);
        int exit_acquire = dev->exit_acquire;
        pthread_mutex_unlock(&dev->lock);
        if (exit_acquire) {
            break; // do not deliver any more events
        }
#endif
        if (n_read > 0) // prevent a crash in callback
            cb(&ev, ctx);

    } while (dev->running);

    return 0;
}

#pragma GCC diagnostic pop

#endif

/* Public API */

int sdr_open(sdr_dev_t **out_dev, char const *dev_query, int verbose)
{
    if (dev_query && !strncmp(dev_query, "rtl_tcp", 7))
        return rtltcp_open(out_dev, dev_query, verbose);

#if !defined(RTLSDR) && !defined(SOAPYSDR)
    if (verbose)
        print_log(LOG_ERROR, __func__, "No input drivers (RTL-SDR or SoapySDR) compiled in.");
    return -1;
#endif

    /* Open RTLSDR by default or if index or serial given, if available */
    if (!dev_query || *dev_query == ':' || (*dev_query >= '0' && *dev_query <= '9')) {
#ifdef RTLSDR
        return sdr_open_rtl(out_dev, dev_query, verbose);
#else
        print_log(LOG_ERROR, __func__, "No input driver for RTL-SDR compiled in.");
        return -1;
#endif
    }

#ifdef SOAPYSDR
    UNUSED(soapysdr_set_bandwidth);
    UNUSED(soapysdr_direct_sampling);
    UNUSED(soapysdr_offset_tuning);

    /* Open SoapySDR otherwise, if available */
    return sdr_open_soapy(out_dev, dev_query, verbose);
#endif
    print_log(LOG_ERROR, __func__, "No input driver for SoapySDR compiled in.");

    return -1;
}

int sdr_close(sdr_dev_t *dev)
{
    if (!dev)
        return -1;

    int ret = sdr_stop(dev);

    if (dev->rtl_tcp)
        ret = rtltcp_close(dev->rtl_tcp);

#ifdef SOAPYSDR
    if (dev->soapy_dev)
        ret = SoapySDRDevice_unmake(dev->soapy_dev);
#endif

#ifdef RTLSDR
    if (dev->rtlsdr_dev)
        ret = rtlsdr_close(dev->rtlsdr_dev);
#endif

#ifdef THREADS
    pthread_mutex_destroy(&dev->lock);
#endif

    free(dev->dev_info);
    free(dev->buffer);
    free(dev);
    return ret;
}

char const *sdr_get_dev_info(sdr_dev_t *dev)
{
    if (!dev)
        return NULL;

    return dev->dev_info;
}

int sdr_get_sample_size(sdr_dev_t *dev)
{
    if (!dev)
        return 0;

    return dev->sample_size;
}

int sdr_get_sample_signed(sdr_dev_t *dev)
{
    if (!dev)
        return 0;

    return dev->sample_signed;
}

int sdr_set_center_freq(sdr_dev_t *dev, uint32_t freq, int verbose)
{
    if (!dev)
        return -1;

#ifdef THREADS
    if (pthread_equal(dev->thread, pthread_self())) {
        fprintf(stderr, "%s: must not be called from acquire callback!\n", __func__);
        return -1;
    }
#endif

    int r = -1;

    if (dev->rtl_tcp) {
        dev->rtl_tcp_freq = freq;
        r = rtltcp_command(dev, RTLTCP_SET_FREQ, freq);
    }

#ifdef SOAPYSDR
    SoapySDRKwargs args = {0};
    if (dev->soapy_dev) {
        r = SoapySDRDevice_setFrequency(dev->soapy_dev, SOAPY_SDR_RX, 0, (double)freq, &args);
    }
#endif

#ifdef RTLSDR
    if (dev->rtlsdr_dev) {
        r = rtlsdr_set_center_freq(dev->rtlsdr_dev, freq);
        print_logf(LOG_DEBUG, "SDR", "rtlsdr_set_center_freq %u = %d", freq, r);
    }
#endif

    if (verbose) {
        if (r < 0)
            print_log(LOG_WARNING, __func__, "Failed to set center freq.");
        else
            print_logf(LOG_NOTICE, "SDR", "Tuned to %s.", nice_freq(sdr_get_center_freq(dev)));
    }

#ifdef THREADS
    pthread_mutex_lock(&dev->lock);
#endif
    dev->center_frequency = freq;
#ifdef THREADS
    pthread_mutex_unlock(&dev->lock);
#endif

    return r;
}

uint32_t sdr_get_center_freq(sdr_dev_t *dev)
{
    if (!dev)
        return 0;

    if (dev->rtl_tcp)
        return dev->rtl_tcp_freq;

#ifdef SOAPYSDR
    if (dev->soapy_dev)
        return (uint32_t)SoapySDRDevice_getFrequency(dev->soapy_dev, SOAPY_SDR_RX, 0);
#endif

#ifdef RTLSDR
    if (dev->rtlsdr_dev)
        return rtlsdr_get_center_freq(dev->rtlsdr_dev);
#endif

    return 0;
}

int sdr_set_freq_correction(sdr_dev_t *dev, int ppm, int verbose)
{
    if (!dev)
        return -1;

#ifdef THREADS
    if (pthread_equal(dev->thread, pthread_self())) {
        fprintf(stderr, "%s: must not be called from acquire callback!\n", __func__);
        return -1;
    }
#endif

    int r = -1;

    if (dev->rtl_tcp)
        r = rtltcp_command(dev, RTLTCP_SET_FREQ_CORRECTION, ppm);

#ifdef SOAPYSDR
    if (dev->soapy_dev)
        r = SoapySDRDevice_setFrequencyComponent(dev->soapy_dev, SOAPY_SDR_RX, 0, "CORR", (double)ppm, NULL);
#endif

#ifdef RTLSDR
    if (dev->rtlsdr_dev) {
        r = rtlsdr_set_freq_correction(dev->rtlsdr_dev, ppm);
        if (r == -2)
            r = 0; // -2 is not an error code
    }
#endif

    if (verbose) {
        if (r < 0)
            print_log(LOG_WARNING, __func__, "Failed to set frequency correction.");
        else
            print_logf(LOG_NOTICE, "SDR", "Frequency correction set to %d ppm.", ppm);
    }
    return r;
}

int sdr_set_auto_gain(sdr_dev_t *dev, int verbose)
{
    if (!dev)
        return -1;

#ifdef THREADS
    if (pthread_equal(dev->thread, pthread_self())) {
        fprintf(stderr, "%s: must not be called from acquire callback!\n", __func__);
        return -1;
    }
#endif

    int r = -1;

    if (dev->rtl_tcp)
        r = rtltcp_command(dev, RTLTCP_SET_GAIN_MODE, 0);

#ifdef SOAPYSDR
    if (dev->soapy_dev)
        r = soapysdr_auto_gain(dev->soapy_dev, verbose);
#endif

#ifdef RTLSDR
    if (dev->rtlsdr_dev)
        r = rtlsdr_set_tuner_gain_mode(dev->rtlsdr_dev, 0);
#endif

    if (verbose) {
        if (r < 0)
            print_log(LOG_WARNING, __func__, "Failed to enable automatic gain.");
        else
            print_log(LOG_NOTICE, "SDR", "Tuner gain set to Auto.");
    }
    return r;
}

int sdr_set_tuner_gain(sdr_dev_t *dev, char const *gain_str, int verbose)
{
    if (!dev)
        return -1;

#ifdef THREADS
    if (pthread_equal(dev->thread, pthread_self())) {
        fprintf(stderr, "%s: must not be called from acquire callback!\n", __func__);
        return -1;
    }
#endif

    int r = -1;

    if (!gain_str || !*gain_str) {
        /* Enable automatic gain */
        return sdr_set_auto_gain(dev, verbose);
    }

#ifdef SOAPYSDR
    /* Enable manual gain */
    if (dev->soapy_dev)
        return soapysdr_gain_str_set(dev->soapy_dev, gain_str, verbose);
#endif

    int gain = (int)(atof(gain_str) * 10); /* tenths of a dB */
    if (gain == 0) {
        /* Enable automatic gain */
        return sdr_set_auto_gain(dev, verbose);
    }

    if (dev->rtl_tcp) {
        return rtltcp_command(dev, RTLTCP_SET_GAIN_MODE, 1)
                || rtltcp_command(dev, RTLTCP_SET_GAIN, gain);
    }

#ifdef RTLSDR
    /* Enable manual gain */
    r = rtlsdr_set_tuner_gain_mode(dev->rtlsdr_dev, 1);
    if (verbose)
        if (r < 0)
            print_log(LOG_WARNING, __func__, "Failed to enable manual gain.");

    /* Set the tuner gain */
    gain = rtlsdr_find_tuner_gain(dev, gain, verbose);

    /* Fix for FitiPower FC0012: set gain to minimum before desired value */
    if (rtlsdr_get_tuner_type(dev->rtlsdr_dev) == RTLSDR_TUNER_FC0012) {
        int minGain = -99;
        minGain = rtlsdr_find_tuner_gain(dev, minGain, verbose);

        r = rtlsdr_set_tuner_gain(dev->rtlsdr_dev, minGain);
        if (verbose) {
            if (r < 0)
                print_log(LOG_WARNING, __func__, "Failed to set initial gain.");
            else
                print_logf(LOG_NOTICE, "SDR", "Set initial gain for FC0012 to %f dB.", minGain / 10.0);
        }
    }

    r = rtlsdr_set_tuner_gain(dev->rtlsdr_dev, gain);
    if (verbose) {
        if (r < 0)
            print_log(LOG_WARNING, __func__, "Failed to set tuner gain.");
        else
            print_logf(LOG_NOTICE, "SDR", "Tuner gain set to %f dB.", gain / 10.0);
    }
#endif

    return r;
}

int sdr_set_antenna(sdr_dev_t *dev, char const *antenna_str, int verbose)
{
    if (!dev)
        return -1;

    POSSIBLY_UNUSED(verbose);
    int r = -1;

    if (!antenna_str)
        return 0;

#ifdef SOAPYSDR
    if (dev->soapy_dev) {
        r = SoapySDRDevice_setAntenna(dev->soapy_dev, SOAPY_SDR_RX, 0, antenna_str);

        if (verbose) {
            if (r < 0)
                print_log(LOG_WARNING, __func__, "Failed to set antenna.");

            // report the antenna that is actually used
            char *antenna = SoapySDRDevice_getAntenna(dev->soapy_dev, SOAPY_SDR_RX, 0);
            print_logf(LOG_NOTICE, "SDR", "Antenna set to '%s'.", antenna);
            free(antenna);
        }
        return r;
    }
#endif

  // currently only SoapySDR supports devices with multiple antennas
  print_log(LOG_WARNING, __func__, "Antenna selection only available for SoapySDR devices");

  return r;
}

int sdr_set_sample_rate(sdr_dev_t *dev, uint32_t rate, int verbose)
{
    if (!dev)
        return -1;

#ifdef THREADS
    if (pthread_equal(dev->thread, pthread_self())) {
        fprintf(stderr, "%s: must not be called from acquire callback!\n", __func__);
        return -1;
    }
#endif

    int r = -1;

    if (dev->rtl_tcp) {
        dev->rtl_tcp_rate = rate;
        r = rtltcp_command(dev, RTLTCP_SET_SAMPLE_RATE, rate);
    }

#ifdef SOAPYSDR
    if (dev->soapy_dev)
        r = SoapySDRDevice_setSampleRate(dev->soapy_dev, SOAPY_SDR_RX, 0, (double)rate);
#endif

#ifdef RTLSDR
    if (dev->rtlsdr_dev)
        r = rtlsdr_set_sample_rate(dev->rtlsdr_dev, rate);
#endif

    if (verbose) {
        if (r < 0)
            print_log(LOG_WARNING, __func__, "Failed to set sample rate.");
        else
            print_logf(LOG_NOTICE, "SDR", "Sample rate set to %u S/s.", sdr_get_sample_rate(dev)); // Unfortunately, doesn't return real rate
    }

#ifdef THREADS
    pthread_mutex_lock(&dev->lock);
#endif
    dev->sample_rate = rate;
#ifdef THREADS
    pthread_mutex_unlock(&dev->lock);
#endif

    return r;
}

uint32_t sdr_get_sample_rate(sdr_dev_t *dev)
{
    if (!dev)
        return 0;

    if (dev->rtl_tcp)
        return dev->rtl_tcp_rate;

#ifdef SOAPYSDR
    if (dev->soapy_dev)
        return (uint32_t)SoapySDRDevice_getSampleRate(dev->soapy_dev, SOAPY_SDR_RX, 0);
#endif

#ifdef RTLSDR
    if (dev->rtlsdr_dev)
        return rtlsdr_get_sample_rate(dev->rtlsdr_dev);
#endif

    return 0;
}

int sdr_apply_settings(sdr_dev_t *dev, char const *sdr_settings, int verbose)
{
    if (!dev)
        return -1;

    POSSIBLY_UNUSED(verbose);
    int r = 0;

    if (!sdr_settings || !*sdr_settings)
        return 0;

    if (dev->rtl_tcp) {
        while (sdr_settings && *sdr_settings) {
            char const *val = NULL;
            // This mirrors the settings of SoapyRTLSDR
            if (kwargs_match(sdr_settings, "direct_samp", &val)) {
                int direct_sampling = atoiv(val, 1);
                r = rtltcp_command(dev, RTLTCP_SET_DIRECT_SAMPLING, direct_sampling);
            }
            else if (kwargs_match(sdr_settings, "offset_tune", &val)) {
                int offset_tuning = atobv(val, 1);
                r = rtltcp_command(dev, RTLTCP_SET_OFFSET_TUNING, offset_tuning);
            }
            else if (kwargs_match(sdr_settings, "digital_agc", &val)) {
                int digital_agc = atobv(val, 1);
                r = rtltcp_command(dev, RTLTCP_SET_AGC_MODE, digital_agc);
            }
            else if (kwargs_match(sdr_settings, "biastee", &val)) {
                int biastee = atobv(val, 1);
                r = rtltcp_command(dev, RTLTCP_SET_BIAS_TEE, biastee);
            }
            else {
                print_logf(LOG_ERROR, __func__, "Unknown rtl_tcp setting: %s", sdr_settings);
                return -1;
            }
            sdr_settings = kwargs_skip(sdr_settings);
        }
        return r;
    }

#ifdef SOAPYSDR
    if (dev->soapy_dev) {
        SoapySDRKwargs settings = SoapySDRKwargs_fromString(sdr_settings);
        for (size_t i = 0; i < settings.size; ++i) {
            const char *key   = settings.keys[i];
            const char *value = settings.vals[i];
            if (!key) {
                continue;
            }
            if (verbose) {
                print_logf(LOG_NOTICE, "SDR", "Setting %s to %s", key, value);
            }
            if (!strcmp(key, "antenna")) {
                if (SoapySDRDevice_setAntenna(dev->soapy_dev, SOAPY_SDR_RX, 0, value) != 0) {
                    r = -1;
                    print_logf(LOG_WARNING, __func__, "Antenna setting failed: %s", SoapySDRDevice_lastError());
                }
            }
            else if (!strcmp(key, "bandwidth")) {
                uint32_t f_value = atouint32_metric(value, "-t bandwidth= ");
                if (SoapySDRDevice_setBandwidth(dev->soapy_dev, SOAPY_SDR_RX, 0, (double)f_value) != 0) {
                    r = -1;
                    print_logf(LOG_WARNING, __func__, "Bandwidth setting failed: %s", SoapySDRDevice_lastError());
                }
            }
            else {
                if (SoapySDRDevice_writeSetting(dev->soapy_dev, key, value) != 0) {
                    r = -1;
                    print_logf(LOG_WARNING, __func__, "sdr setting failed: %s", SoapySDRDevice_lastError());
                }
            }
        }
        SoapySDRKwargs_clear(&settings);
        return r;
    }
#endif

#ifdef RTLSDR
    if (dev->rtlsdr_dev) {
        while (sdr_settings && *sdr_settings) {
            char const *val = NULL;
            // This mirrors the settings of SoapyRTLSDR
            if (kwargs_match(sdr_settings, "direct_samp", &val)) {
                int direct_sampling = atoiv(val, 1);
                r = rtlsdr_set_direct_sampling(dev->rtlsdr_dev, direct_sampling);
            }
            else if (kwargs_match(sdr_settings, "offset_tune", &val)) {
                int offset_tuning = atobv(val, 1);
                r = rtlsdr_set_offset_tuning(dev->rtlsdr_dev, offset_tuning);
            }
            else if (kwargs_match(sdr_settings, "digital_agc", &val)) {
                int digital_agc = atobv(val, 1);
                r = rtlsdr_set_agc_mode(dev->rtlsdr_dev, digital_agc);
            }
            else if (kwargs_match(sdr_settings, "biastee", &val)) {
#if defined(__linux__) && (defined(__GNUC__) || defined(__clang__))
                // check weak link for Linux with older rtlsdr
                if (!rtlsdr_set_bias_tee) {
                    print_log(LOG_ERROR, __func__, "This librtlsdr version does not support biastee setting");
                    return -1;
                }
#endif
                int biastee = atobv(val, 1);
                r = rtlsdr_set_bias_tee(dev->rtlsdr_dev, biastee);
            }
            else {
                print_logf(LOG_ERROR, __func__, "Unknown RTLSDR setting: %s", sdr_settings);
                return -1;
            }
            sdr_settings = kwargs_skip(sdr_settings);
        }
        return r;
    }
#endif

    print_log(LOG_WARNING, __func__, "sdr settings not available."); // no open device

    return -1;
}

int sdr_activate(sdr_dev_t *dev)
{
    if (!dev)
        return -1;

#ifdef SOAPYSDR
    if (dev->soapy_dev) {
        if (SoapySDRDevice_activateStream(dev->soapy_dev, dev->soapy_stream, 0, 0, 0) != 0) {
            print_log(LOG_ERROR, __func__, "Failed to activate stream");
            exit(1);
        }
    }
#endif

    return 0;
}

int sdr_deactivate(sdr_dev_t *dev)
{
    if (!dev)
        return -1;

#ifdef SOAPYSDR
    if (dev->soapy_dev) {
        SoapySDRDevice_deactivateStream(dev->soapy_dev, dev->soapy_stream, 0, 0);
        SoapySDRDevice_closeStream(dev->soapy_dev, dev->soapy_stream);
    }
#endif

    return 0;
}

int sdr_reset(sdr_dev_t *dev, int verbose)
{
    if (!dev)
        return -1;

    int r = 0;

#ifdef RTLSDR
    if (dev->rtlsdr_dev)
        r = rtlsdr_reset_buffer(dev->rtlsdr_dev);
#endif

    if (verbose) {
        if (r < 0)
            print_log(LOG_WARNING, __func__, "Failed to reset buffers.");
    }
    return r;
}

int sdr_start_sync(sdr_dev_t *dev, sdr_event_cb_t cb, void *ctx, uint32_t buf_num, uint32_t buf_len)
{
    if (!dev)
        return -1;

    if (buf_num == 0)
        buf_num = SDR_DEFAULT_BUF_NUMBER;
    if (buf_len == 0)
        buf_len = SDR_DEFAULT_BUF_LENGTH;

    if (dev->rtl_tcp)
        return rtltcp_read_loop(dev, cb, ctx, buf_num, buf_len);

#ifdef SOAPYSDR
    if (dev->soapy_dev)
        return soapysdr_read_loop(dev, cb, ctx, buf_num, buf_len);
#endif

#ifdef RTLSDR
    if (dev->rtlsdr_dev)
        return rtlsdr_read_loop(dev, cb, ctx, buf_num, buf_len);
#endif

    return -1;
}

int sdr_stop_sync(sdr_dev_t *dev)
{
    if (!dev)
        return -1;

    if (dev->rtl_tcp) {
        dev->running = 0;
        return 0;
    }

#ifdef SOAPYSDR
    if (dev->soapy_dev) {
        dev->running = 0;
        return 0;
    }
#endif

#ifdef RTLSDR
    if (dev->rtlsdr_dev) {
        dev->running = 0;
        return rtlsdr_cancel_async(dev->rtlsdr_dev);
    }
#endif

    return -1;
}

#ifdef SOAPYSDR
static void soapysdr_log_handler(const SoapySDRLogLevel level, const char *message)
{
    // Our log levels are compatible with SoapySDR.
    print_log((log_level_t)level, "SoapySDR", message);
}
#endif

void sdr_redirect_logging(void)
{
#ifdef SOAPYSDR
    SoapySDR_registerLogHandler(soapysdr_log_handler);
#endif
}

/* threading */

#ifdef THREADS
static THREAD_RETURN THREAD_CALL acquire_thread(void *arg)
{
    sdr_dev_t *dev = arg;
    print_log(LOG_DEBUG, __func__, "acquire_thread enter...");

    int r = sdr_start_sync(dev, dev->async_cb, dev->async_ctx, dev->buf_num, dev->buf_len);
    // if (cfg->verbosity > 1)
    print_log(LOG_DEBUG, __func__, "acquire_thread async stop...");

    if (r < 0) {
        print_logf(LOG_ERROR, "SDR", "async read failed (%i).", r);
    }

//    sdr_event_t ev = {
//            .ev  = SDR_EV_QUIT,
//    };
//    dev->async_cb(&ev, dev->async_ctx);

    print_log(LOG_DEBUG, __func__, "acquire_thread done...");
    return (THREAD_RETURN)(intptr_t)r;
}

int sdr_start(sdr_dev_t *dev, sdr_event_cb_t async_cb, void *async_ctx, uint32_t buf_num, uint32_t buf_len)
{
    if (!dev)
        return -1;

    dev->async_cb = async_cb;
    dev->async_ctx = async_ctx;
    dev->buf_num = buf_num;
    dev->buf_len = buf_len;

#ifndef _WIN32
    // Block all signals from the worker thread
    sigset_t sigset;
    sigset_t oldset;
    sigfillset(&sigset);
    pthread_sigmask(SIG_SETMASK, &sigset, &oldset);
#endif
    int r = pthread_create(&dev->thread, NULL, acquire_thread, dev);
#ifndef _WIN32
    pthread_sigmask(SIG_SETMASK, &oldset, NULL);
#endif
    if (r) {
        fprintf(stderr, "%s: error in pthread_create, rc: %d\n", __func__, r);
    }
    return r;
}

int sdr_stop(sdr_dev_t *dev)
{
    if (!dev)
        return -1;

    if (pthread_equal(dev->thread, pthread_self())) {
        fprintf(stderr, "%s: must not be called from acquire callback!\n", __func__);
        return -1;
    }

    print_log(LOG_DEBUG, __func__, "EXITING...");
    pthread_mutex_lock(&dev->lock);
    if (dev->exit_acquire) {
        pthread_mutex_unlock(&dev->lock);
        print_log(LOG_DEBUG, __func__, "Already exiting.");
        return 0;
    }
    dev->exit_acquire = 1; // for rtl_tcp and SoapySDR
    sdr_stop_sync(dev); // for rtlsdr
    pthread_mutex_unlock(&dev->lock);

    print_log(LOG_DEBUG, __func__, "JOINING...");
    int r = pthread_join(dev->thread, NULL);
    if (r) {
        fprintf(stderr, "%s: error in pthread_join, rc: %d\n", __func__, r);
    }

    print_log(LOG_DEBUG, __func__, "EXITED.");
    return r;
}
#else
int sdr_start(sdr_dev_t *dev, sdr_event_cb_t cb, void *ctx, uint32_t buf_num, uint32_t buf_len)
{
    UNUSED(dev);
    return -1;
}
int sdr_stop(sdr_dev_t *dev)
{
    UNUSED(dev);
    return -1;
}
#endif
