//
// Copyright 2020 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: GPL-3.0-or-later
//

#include <uhd/exception.hpp>
#include <uhd/utils/log.hpp>
#include <uhdlib/usrp/dboard/zbx/zbx_lo_ctrl.hpp>
#include <uhdlib/utils/narrow.hpp>
#include <chrono>
#include <thread>

namespace uhd { namespace usrp { namespace zbx {

zbx_lo_ctrl::zbx_lo_ctrl(zbx_lo_t lo,
    lmx2572_iface::write_fn_t&& poke16,
    lmx2572_iface::read_fn_t&& peek16,
    lmx2572_iface::sleep_fn_t&& sleep,
    const double default_frequency,
    const double db_prc_rate,
    const bool testing_mode_enabled)
    : _log_id(ZBX_LO_LOG_ID.at(lo))
    , _freq(default_frequency)
    , _db_prc_rate(db_prc_rate)
    , _testing_mode_enabled(testing_mode_enabled)
{
    _lmx = lmx2572_iface::make(std::move(poke16), std::move(peek16), std::move(sleep));
    UHD_ASSERT_THROW(_lmx);
    UHD_LOG_TRACE(_log_id, "LO initialized...");
    _lmx->reset();

    set_lo_port_enabled(true);
    // In ZBX, we always run the LOs in sync mode. It is theoretically possible
    // to not do so, but we gain nothing by doing that.
    _lmx->set_sync_mode(true);
    set_lo_freq(LMX2572_DEFAULT_FREQ);
    wait_for_lo_lock();
}

double zbx_lo_ctrl::set_lo_freq(const double freq)
{
    UHD_ASSERT_THROW(_lmx);
    UHD_LOG_TRACE(_log_id, "Setting LO frequency " << freq / 1e6 << " MHz");

    _freq = _lmx->set_frequency(freq, _db_prc_rate, false /*TODO: get_spur_dodging()*/);
    const uint8_t power = _find_lo_power(_freq);
    if (_lmx->get_output_enabled(lmx2572_iface::output_t::RF_OUTPUT_A)) {
        _lmx->set_output_power(lmx2572_iface::output_t::RF_OUTPUT_A, power);
    }
    if (_lmx->get_output_enabled(lmx2572_iface::output_t::RF_OUTPUT_B)) {
        _lmx->set_output_power(lmx2572_iface::output_t::RF_OUTPUT_B, power);
    }
    _lmx->commit();
    return _freq;
}

double zbx_lo_ctrl::get_lo_freq()
{
    return _freq;
}

void zbx_lo_ctrl::wait_for_lo_lock()
{
    UHD_LOG_TRACE(_log_id, "Waiting for LO lock,  " << ZBX_LO_LOCK_TIMEOUT_MS << " ms");
    const auto timeout = std::chrono::steady_clock::now()
                         + std::chrono::milliseconds(ZBX_LO_LOCK_TIMEOUT_MS);
    while (std::chrono::steady_clock::now() < timeout && !get_lock_status()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
    if (!get_lock_status()) {
        // If we can't lock our LO, this could be a lot of possible issues
        throw uhd::runtime_error(_log_id + " has failed to lock!");
    }
}

bool zbx_lo_ctrl::get_lock_status()
{
    return _lmx->get_lock_status();
}

void zbx_lo_ctrl::set_lo_port_enabled(bool enable)
{
    UHD_LOG_TRACE(_log_id,
        "Enabling LO " << (_testing_mode_enabled ? "test" : "output") << " port");

    // We want to set the output port regardless of test mode being enabled
    _lmx->set_output_enable(_get_output_port(false), enable);

    if (_testing_mode_enabled && enable) {
        // If testing mode is enabled, also set the test port
        _lmx->set_output_enable(_get_output_port(true), true);
    } else {
        // If testing mode is disabled, test port should be disabled
        _lmx->set_output_enable(_get_output_port(true), false);
        _lmx->set_mux_input(
            _get_output_port(true), lmx2572_iface::mux_in_t::HIGH_IMPEDANCE);
    }

    _lmx->set_enabled(enable);
    _lmx->commit();
}

bool zbx_lo_ctrl::get_lo_port_enabled()
{
    return _lmx->get_enabled();
}

void zbx_lo_ctrl::set_lo_test_mode_enabled(bool enable)
{
    _testing_mode_enabled = enable;
    set_lo_port_enabled(get_lo_port_enabled());
}

bool zbx_lo_ctrl::get_lo_test_mode_enabled()
{
    return _testing_mode_enabled;
}

zbx_lo_t zbx_lo_ctrl::lo_string_to_enum(
    const uhd::direction_t trx, const size_t channel, const std::string name)
{
    if (trx == TX_DIRECTION) {
        if (channel == 0) {
            if (name == ZBX_LO1) {
                return zbx_lo_t::TX0_LO1;
            } else if (name == ZBX_LO2) {
                return zbx_lo_t::TX0_LO2;
            }
        } else if (channel == 1) {
            if (name == ZBX_LO1) {
                return zbx_lo_t::TX1_LO1;
            } else if (name == ZBX_LO2) {
                return zbx_lo_t::TX1_LO2;
            }
        }
    } else {
        if (channel == 0) {
            if (name == ZBX_LO1) {
                return zbx_lo_t::RX0_LO1;
            } else if (name == ZBX_LO2) {
                return zbx_lo_t::RX0_LO2;
            }
        } else if (channel == 1) {
            if (name == ZBX_LO1) {
                return zbx_lo_t::RX1_LO1;
            } else if (name == ZBX_LO2) {
                return zbx_lo_t::RX1_LO2;
            }
        }
    }
    UHD_THROW_INVALID_CODE_PATH();
}

lmx2572_iface::output_t zbx_lo_ctrl::_get_output_port(bool testing_mode)
{
    // Note: The LO output ports here are dependent to the LO and zbx hardware
    // configuration, in no particular order (zbx radio configuration output vs.
    // test port output)
    if (!testing_mode) {
        // Rev B has all LO outputs on Port A
        return lmx2572_iface::output_t::RF_OUTPUT_A;
    } else {
        return lmx2572_iface::output_t::RF_OUTPUT_B;
    }
}

uint8_t zbx_lo_ctrl::_find_lo_power(const double freq)
{
    if (freq < 3e9) {
        return 25;
    } else if (3e9 <= freq && freq < 4e9) {
        constexpr double slope         = 5.0;
        constexpr double segment_range = 1e9;
        constexpr double power_base    = 25.0;
        const double offset            = freq - 3e9;
        const uint8_t power            = uhd::narrow_cast<uint8_t>(
            std::lround(power_base + ((offset / segment_range) * slope)));
        return power;
    } else if (4e9 <= freq && freq < 5e9) {
        constexpr double slope         = 10.0;
        constexpr double segment_range = 1e9;
        constexpr double power_base    = 30.0;
        const double offset            = freq - 4e9;
        const uint8_t power            = uhd::narrow_cast<uint8_t>(
            std::lround(power_base + ((offset / segment_range) * slope)));
        return power;
    } else if (5e9 <= freq && freq < 6.4e9) {
        constexpr double slope         = 5 / 1.4;
        constexpr double segment_range = 1.4e9;
        constexpr double power_base    = 40.0;
        const double offset            = freq - 5e9;
        const uint8_t power            = uhd::narrow_cast<uint8_t>(
            std::lround(power_base + ((offset / segment_range) * slope)));
        return power;
    } else if (freq >= 6.4e9) {
        return 45;
    } else {
        UHD_THROW_INVALID_CODE_PATH();
    }
}
}}} // namespace uhd::usrp::zbx
