///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Edouard Griffiths, F4EXB <f4exb06@gmail.com>               //
//                                                                               //
// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon          //
// reformatted and adapted to Qt and SDRangel context and FT4 scheme             //
//                                                                               //
// 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 as version 3 of the License, or                  //
// (at your option) any later version.                                           //
//                                                                               //
// This program is distributed in the hope that it will be useful,               //
// but WITHOUT ANY WARRANTY; without even the implied warranty of                //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the                  //
// GNU General Public License V3 for more details.                               //
//                                                                               //
// You should have received a copy of the GNU General Public License             //
// along with this program. If not, see <http://www.gnu.org/licenses/>.          //
///////////////////////////////////////////////////////////////////////////////////

#ifndef ft4_h
#define ft4_h

#include <vector>

#include <QObject>

#include "ft8.h"
#include "export.h"

class QThread;

namespace FT8 {

// FT4 characteristics:
// 576-point FFT at 12000 samples/second
// 23.4 Hz spacing, 0.048 seconds/symbol
// encode chain:
//   77 bits
//   append 14 bits CRC (for 91 bits)
//   LDPC(174,91) yields 174 bits
//   that's 87 2-bit FSK-4 symbols
//   gray code each 2 bits
//   insert four 4-symbol Costas sync arrays
//     at positions: 0-3, 33-36, 66-69, 99-102
//   thus: 103 FSK-4 symbols
//   add 2 ramp symbols at start and end to make 105 symbols
// total transmission time is 5.04 seconds

// The FT4 worker
class FT8_API FT4 : public QObject
{
    Q_OBJECT
public:
    FT4(
        const std::vector<float> &samples,
        float min_hz,
        float max_hz,
        int start,
        float rate,
        const int hints1[],
        const int hints2[],
        double deadline,
        double final_deadline,
        CallbackInterface *cb,
        const std::vector<cdecode>& prevdecs,
        FFTEngine *fftEngine
    );
    ~FT4() override = default;
    // Number of passes
    void set_npasses(int npasses) { npasses_ = npasses; }
    // Start the worker
    void start_work();
    // strength of costas block of signal with tone 0 at bi0,
    // and symbol zero at si0.
    float one_coarse_strength(const FFTEngine::ffts_t &bins, int bi0, int si0) const;
    // return symbol length in samples at the given rate.
    // insist on integer symbol lengths so that we can
    // use whole FFT bins.
    int blocksize(float rate) const;
    //
    // look for potential signals by searching FFT bins for Costas symbol
    // blocks. returns a vector of candidate positions.
    //
    std::vector<Strength> coarse(const FFTEngine::ffts_t &bins, int si0, int si1) const;

    FT8Params& getParams() { return params; }
    //
    // given log likelihood for each bit, try LDPC and OSD decoders.
    // on success, puts corrected 174 bits into a174[].
    //
    static int decode(const float ll174[], int a174[], const FT8Params& params, int use_osd, std::string &comment);
    // encode a 77 bit message into a 174 bit payload
    // adds the 14 bit CRC to obtain 91 bits
    // apply (174, 91) generator mastrix to obtain the 83 parity bits
    // append the 83 bits to the 91 bits message e+ crc to obtain the 174 bit payload
    static void encode(int a174[], const int s77[]);

private:
    //
    // reduce the sample rate from arate to brate.
    // center hz0..hz1 in the new nyquist range.
    // but first filter to that range.
    // sets delta_hz to hz moved down.
    //
    std::vector<float> reduce_rate(
        const std::vector<float> &a,
        float hz0,
        float hz1,
        int arate,
        int brate,
        float &delta_hz
    );
    // The actual main process
    void go(int npasses);
    //
    // what's the strength of the Costas sync blocks of
    // the signal starting at hz and off?
    //
    float one_strength(const std::vector<float> &samples200, float hz, int off);
    //
    // given a complete known signal's symbols in syms,
    // how strong is it? used to look for the best
    // offset and frequency at which to subtract a
    // decoded signal.
    //
    float one_strength_known(
        const std::vector<float> &samples,
        float rate,
        const std::vector<int> &syms,
        float hz,
        int off
    );
    int search_time_fine(
        const std::vector<float> &samples200,
        int off0,
        int offN,
        float hz,
        int gran,
        float &str
    );
    int search_time_fine_known(
        const std::vector<std::complex<float>> &bins,
        float rate,
        const std::vector<int> &syms,
        int off0,
        int offN,
        float hz,
        int gran,
        float &str
    );
    //
    // search for costas blocks in an MxN time/frequency grid.
    // hz0 +/- hz_win in hz_inc increments. hz0 should be near 25.
    // off0 +/- off_win in off_inc incremenents.
    //
    std::vector<Strength> search_both(
        const std::vector<float> &samples200,
        float hz0,
        int hz_n,
        float hz_win,
        int off0,
        int off_n,
        int off_win
    );
    void search_both_known(
        const std::vector<float> &samples,
        float rate,
        const std::vector<int> &syms,
        float hz0,
        float off_secs0, // seconds
        float &hz_out,
        float &off_out
    );
    //
    // shift frequency by shifting the bins of one giant FFT.
    // so no problem with phase mismatch &c at block boundaries.
    // surprisingly fast at 200 samples/second.
    // shifts *down* by hz.
    //
    std::vector<float> fft_shift(
        const std::vector<float> &samples,
        int off,
        int len,
        float rate,
        float hz
    );
    //
    // shift down by hz.
    //
    std::vector<float> fft_shift_f(
        const std::vector<std::complex<float>> &bins,
        float rate,
        float hz
    );
    // shift the frequency by a fraction of 23.4,
    // to center hz on bin 2 (46.8 hz).
    std::vector<float> shift200(
        const std::vector<float> &samples200,
        int off,
        int len,
        float hz
    );
    // returns a mini-FFT of 103 4-tone symbols.
    FFTEngine::ffts_t extract(const std::vector<float> &samples200, float, int off);
    //
    // m103 is a 103x4 array of complex.
    //
    FFTEngine::ffts_t un_gray_code_c(const FFTEngine::ffts_t &m103) const;
    //
    // m103 is a 103x4 array of float.
    //
    std::vector<std::vector<float>> un_gray_code_r(const std::vector<std::vector<float>> &m103) const;
    //
    // normalize levels by windowed median.
    // this helps, but why?
    //
    std::vector<std::vector<float>> convert_to_snr(const std::vector<std::vector<float>> &m103) const;
    // normalize levels by windowed median.
    // this helps, but why?
    //
    std::vector<std::vector<std::complex<float>>> c_convert_to_snr(
        const std::vector<std::vector<std::complex<float>>> &m103
    ) const;
    //
    // statistics to decide soft probabilities,
    // to drive LDPC decoder.
    // distribution of strongest tones, and
    // distribution of noise.
    //
    static void make_stats(
        const std::vector<std::vector<float>> &m103,
        Stats &bests,
        Stats &all
    );
    // convert 103x4 complex FFT bins to magnitudes.
    //
    // exploits local phase coherence by decreasing magnitudes of bins
    // whose phase is far from the phases of nearby strongest tones.
    //
    // relies on each tone being reasonably well centered in its FFT bin
    // (in time and frequency) so that each tone completes an integer
    // number of cycles and thus preserves phase from one symbol to the
    // next.
    //
    std::vector<std::vector<float>> soft_c2m(const FFTEngine::ffts_t &c103) const;
    // c103 is 103x4 complex tones, before un-gray-coding.
    //
    void soft_decode(const FFTEngine::ffts_t &c103, float ll174[]) const;
    //
    // c103 is 103x4 complex tones, before un-gray-coding.
    //
    void c_soft_decode(const FFTEngine::ffts_t &c103x, float ll174[]) const;
    //
    // turn 103 symbol numbers into 174 bits.
    // strip out the three Costas sync blocks,
    // leaving 87 symbol numbers.
    // each represents three bits.
    // (all post-un-gray-code).
    // str is per-symbol strength; must be positive.
    // each returned element is < 0 for 1, > 0 for zero,
    // scaled by str.
    //
    std::vector<float> extract_bits(const std::vector<int> &syms, const std::vector<float>& str) const;
    // decode successive pairs of symbols. exploits the likelihood
    // that they have the same phase, by summing the complex
    // correlations for each possible pair and using the max.
    void soft_decode_pairs(
        const FFTEngine::ffts_t &m103x,
        float ll174[]
    ) const;
    void soft_decode_triples(
        const FFTEngine::ffts_t &m103x,
        float ll174[]
    ) const;
    //
    // bandpass filter some FFT bins.
    // smooth transition from stop-band to pass-band,
    // so that it's not a brick-wall filter, so that it
    // doesn't ring.
    //
    std::vector<std::complex<float>> fbandpass(
        const std::vector<std::complex<float>> &bins0,
        float bin_hz,
        float low_outer,  // start of transition
        float low_inner,  // start of flat area
        float high_inner, // end of flat area
        float high_outer  // end of transition
    ) const;
    //
    // move hz down to 25, filter+convert to 200 samples/second.
    //
    // like fft_shift(). one big FFT, move bins down and
    // zero out those outside the band, then IFFT,
    // then re-sample.
    //
    // XXX maybe merge w/ fft_shift() / shift200().
    //
    std::vector<float> down_v7(const std::vector<float> &samples, float hz);
    std::vector<float> down_v7_f(const std::vector<std::complex<float>> &bins, int len, float hz);
    //
    // putative start of signal is at hz and symbol si0.
    //
    // return 2 if it decodes to a brand-new message.
    // return 1 if it decodes but we've already seen it,
    //   perhaps in a different pass.
    // return 0 if we could not decode.
    //
    // XXX merge with one_iter().
    //
    int one_merge(const std::vector<std::complex<float>> &bins, int len, float hz, int off);
    // return 2 if it decodes to a brand-new message.
    // return 1 if it decodes but we've already seen it,
    //   perhaps in a different pass.
    // return 0 if we could not decode.
    int one_iter(const std::vector<float> &samples200, int best_off, float hz_for_cb);
    //
    // estimate SNR, yielding numbers vaguely similar to WSJT-X.
    // m103 is a 103x4 complex FFT output.
    //
    float guess_snr(const FFTEngine::ffts_t &m103) const;
    //
    // compare phases of successive symbols to guess whether
    // the starting offset is a little too high or low.
    // we expect each symbol to have the same phase.
    // an error in causes the phase to advance at a steady rate.
    // so if hz is wrong, we expect the phase to advance
    // or retard at a steady pace.
    // an error in offset causes each symbol to start at
    // a phase that depends on the symbol's frequency;
    // a particular offset error causes a phase error
    // that depends on frequency.
    // hz0 is actual FFT bin number of m103[...][0].
    //
    // the output adj_hz is relative to the FFT bin center;
    // a positive number means the real signal seems to be
    // a bit higher in frequency that the bin center.
    //
    // adj_off is the amount to change the offset, in samples.
    // should be subtracted from offset.
    //
    void fine(const FFTEngine::ffts_t &m103, int, float &adj_hz, float &adj_off) const;
    //
    // subtract a corrected decoded signal from nsamples_,
    // perhaps revealing a weaker signal underneath,
    // to be decoded in a subsequent pass.
    //
    // re103[] holds the error-corrected symbol numbers.
    //
    void subtract(
        const std::vector<int>& re103,
        float hz0,
        float hz1,
        float off_sec
    );
    //
    // decode, give to callback, and subtract.
    //
    // return 2 if it decodes to a brand-new message.
    // return 1 if it decodes but we've already seen it,
    //   perhaps in a different pass.
    // return 0 if we could not decode.
    //
    int try_decode(
        const std::vector<float> &samples200,
        const float ll174[174],
        float best_hz,
        int best_off_samples,
        float hz0_for_cb,
        float,
        int use_osd,
        const char *comment1,
        const FFTEngine::ffts_t &m103
    );
    //
    // given 174 bits corrected by LDPC, work
    // backwards to the symbols that must have
    // been sent.
    // used to help ensure that subtraction subtracts
    // at the right place.
    //
    std::vector<int> recode(const int a174[]) const;
    //
    // the signal is centered around 46.8 Hz in samples200.
    //
    // return 2 if it decodes to a brand-new message.
    // return 1 if it decodes but we've already seen it,
    //   perhaps in a different pass.
    // return 0 if we could not decode.
    //
    int one_iter1(
        const std::vector<float> &samples200x,
        int best_off,
        float best_hz,
        float hz0_for_cb,
        float hz1_for_cb
    );

signals:
    void finished();
private:
    FT8Params params;
    FFTEngine *fftEngine_;
    int npasses_;
    static const double apriori174[];

    float min_hz_;
    float max_hz_;
    std::vector<float> samples_;  // input to each pass
    std::vector<float> nsamples_; // subtract from here

    int start_;             // sample number of 0.5 seconds into samples[]
    float rate_;            // samples/second
    double deadline_;       // start time + budget
    double final_deadline_; // keep going this long if no decodes
    std::vector<int> hints1_;
    std::vector<int> hints2_;
    int pass_;
    float down_hz_;

    QMutex cb_mu_;
    CallbackInterface *cb_; // call-back interface

    QMutex hack_mu_;
    int hack_size_;
    int hack_off_;
    int hack_len_;
    float hack_0_;
    float hack_1_;
    const float *hack_data_;
    std::vector<std::complex<float>> hack_bins_;
    std::vector<cdecode> prevdecs_;
}; // class FT4


class FT8_API FT4Decoder : public QObject {
    Q_OBJECT
public:
    ~FT4Decoder();
    void entry(
        const float xsamples[],
        int nsamples,
        int start,
        float rate,
        float min_hz,
        float max_hz,
        const int hints1[],
        const int hints2[],
        double time_left,
        double total_time_left,
        CallbackInterface *cb,
        int,
        const struct cdecode*
    );
    void wait(double time_left); //!< wait for all threads to finish
    void forceQuit(); //!< force quit all threads
    FT8Params& getParams() { return params; }
private:
    FT8Params params;
    std::vector<QThread*> threads;
    std::vector<FFTEngine*> fftEngines;
}; // FT4Decoder


} // namespace FT8

#endif
