/* -*- c++ -*- */
/*
 * Copyright (C) 2011,2013,2016-2017 Free Software Foundation, Inc.
 *
 * This file is part of GNU Radio
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "clock_tracking_loop.h"
#include <gnuradio/math.h>
#include <stdexcept>

namespace gr {
namespace digital {

clock_tracking_loop::clock_tracking_loop(float loop_bw,
                                         float max_period,
                                         float min_period,
                                         float nominal_period,
                                         float damping,
                                         float ted_gain)
    : d_avg_period(nominal_period),
      d_max_avg_period(max_period),
      d_min_avg_period(min_period),
      d_nom_avg_period(nominal_period),
      d_inst_period(nominal_period),
      d_phase(0.0f),
      d_zeta(damping),
      d_omega_n_norm(loop_bw),
      d_ted_gain(ted_gain),
      d_alpha(0.0f),
      d_beta(0.0f),
      d_prev_avg_period(nominal_period),
      d_prev_inst_period(nominal_period),
      d_prev_phase(0.0f)
{
    set_max_avg_period(max_period);
    set_min_avg_period(min_period);
    set_nom_avg_period(nominal_period);

    set_avg_period(d_nom_avg_period);
    set_inst_period(d_nom_avg_period);

    if (d_zeta < 0.0f)
        throw std::out_of_range("clock_tracking_loop: damping factor must be > 0.0");

    if (d_omega_n_norm < 0.0f)
        throw std::out_of_range(
            "clock_tracking_loop: loop bandwidth must be greater than 0.0");

    if (d_ted_gain <= 0.0f)
        throw std::out_of_range(
            "clock_tracking_loop: expected ted gain must be greater than 0.0");

    update_gains();
}

clock_tracking_loop::~clock_tracking_loop() {}

void clock_tracking_loop::update_gains()
{
    float omega_n_T, omega_d_T, zeta_omega_n_T, cosx_omega_d_T;
    float k0, k1, sinh_zeta_omega_n_T;
    float alpha, beta;

    omega_n_T = d_omega_n_norm;
    zeta_omega_n_T = d_zeta * omega_n_T;
    k0 = 2.0f / d_ted_gain;
    k1 = expf(-zeta_omega_n_T);
    sinh_zeta_omega_n_T = sinhf(zeta_omega_n_T);

    if (d_zeta > 1.0f) { // Over-damped (or critically-damped too)

        omega_d_T = omega_n_T * sqrtf(d_zeta * d_zeta - 1.0f);
        cosx_omega_d_T = coshf(omega_d_T);
        // cosh ---------^^^^

    } else if (d_zeta == 1.0f) { // Critically-damped

        omega_d_T = 0.0f;
        cosx_omega_d_T = 1.0f;
        // cosh(omega_d_T) & cos(omega_d_T) are both 1 for omega_d_T == 0

    } else { // Under-damped (or critically-damped too)

        omega_d_T = omega_n_T * sqrtf(1.0 - d_zeta * d_zeta);
        cosx_omega_d_T = cosf(omega_d_T);
        // cos ----------^^^
    }

    alpha = k0 * k1 * sinh_zeta_omega_n_T;
    beta = k0 * (1 - k1 * (sinh_zeta_omega_n_T + cosx_omega_d_T));

    set_alpha(alpha);
    set_beta(beta);
}

/*******************************************************************
 * SET FUNCTIONS
 *******************************************************************/

void clock_tracking_loop::set_loop_bandwidth(float bw)
{
    if (bw < 0.0f)
        throw std::out_of_range(
            "clock_tracking_loop: loop bandwidth must be greater than 0.0");

    d_omega_n_norm = bw;
    update_gains();
}

void clock_tracking_loop::set_damping_factor(float df)
{
    if (df < 0.0f)
        throw std::out_of_range("clock_tracking_loop: damping factor must be > 0.0");

    d_zeta = df;
    update_gains();
}

void clock_tracking_loop::set_ted_gain(float ted_gain)
{
    if (ted_gain <= 0.0f)
        throw std::out_of_range("clock_tracking_loop: expected ted gain must be > 0.0");

    d_ted_gain = ted_gain;
    update_gains();
}

void clock_tracking_loop::set_alpha(float alpha) { d_alpha = alpha; }

void clock_tracking_loop::set_beta(float beta) { d_beta = beta; }

void clock_tracking_loop::set_avg_period(float period)
{
    d_avg_period = period;
    d_prev_avg_period = period;
}

void clock_tracking_loop::set_inst_period(float period)
{
    d_inst_period = period;
    d_prev_inst_period = period;
}

void clock_tracking_loop::set_phase(float phase)
{
    // This previous phase is likely inconsistent with the tracking,
    // but if the caller is setting the phase, the odds of
    // revert_loop() being called are slim.
    d_prev_phase = phase;

    d_phase = phase;
}

void clock_tracking_loop::set_max_avg_period(float period) { d_max_avg_period = period; }

void clock_tracking_loop::set_min_avg_period(float period) { d_min_avg_period = period; }

void clock_tracking_loop::set_nom_avg_period(float period)
{
    if (period < d_min_avg_period || period > d_max_avg_period) {
        d_nom_avg_period = (d_max_avg_period + d_min_avg_period) / 2.0f;
    } else {
        d_nom_avg_period = period;
    }
}

/*******************************************************************
 * GET FUNCTIONS
 *******************************************************************/

float clock_tracking_loop::get_loop_bandwidth() const { return d_omega_n_norm; }

float clock_tracking_loop::get_damping_factor() const { return d_zeta; }

float clock_tracking_loop::get_ted_gain() const { return d_ted_gain; }

float clock_tracking_loop::get_alpha() const { return d_alpha; }

float clock_tracking_loop::get_beta() const { return d_beta; }

float clock_tracking_loop::get_avg_period() const { return d_avg_period; }

float clock_tracking_loop::get_inst_period() const { return d_inst_period; }

float clock_tracking_loop::get_phase() const { return d_phase; }

float clock_tracking_loop::get_max_avg_period() const { return d_max_avg_period; }

float clock_tracking_loop::get_min_avg_period() const { return d_min_avg_period; }

float clock_tracking_loop::get_nom_avg_period() const { return d_nom_avg_period; }

} /* namespace digital */
} /* namespace gr */