#include <iostream>

#include "include/cgaborator.h"
#include "gaborator/gaborator.h"
#include <cmath>
#include <memory>

class Gaborator {

public:
    Gaborator(int block_size, double sampleRate, int bandsPerOctave, double minimumFrequency, double referenceFrequency, double maximumFrequency, int stepSize) :
    parameters(bandsPerOctave, minimumFrequency / sampleRate, referenceFrequency / sampleRate, 1.0, 1e-5),
    analyzer(parameters),
    coefs(analyzer),
    frequencyBinTimeStepSize(stepSize),
    sample_rate((int) sampleRate),
    blockSize(block_size)
    {

        //converts frequency (ff_max) in hertz to the number of bands above the min frequency
        //the ceil is used to end up at a full band
        int interesting_bands = ceil(bandsPerOctave * log(maximumFrequency/minimumFrequency)/log(2.0f));

        //since bands are ordered from high to low we are only interested in lower bands:
        //fs/2.0 is the nyquist frequency
        int total_bands = ceil(bandsPerOctave * log(sampleRate/2.0/minimumFrequency)/log(2.0f));

        latency = (int64_t) ceil(analyzer.analysis_support());
        min_band = total_bands - interesting_bands;
        t_in = 0;

        int max_band = analyzer.bandpass_bands_end();

        std::vector<float> bandcenterCache(max_band);

        for(int i = 0 ; i < max_band ; i++){
            if(i < min_band){
                bandcenterCache[i] = -1;
            }else{
                bandcenterCache[i] = analyzer.band_ff(i) * sample_rate;
            }

            if (bandcenterCache[i] > 0) {
                ++numberOfBandsCache;
                if (firstBandCache == -1) {
                    firstBandCache = i;
                }
            }
        }

        coefficientSize = (latency + 2*blockSize) / frequencyBinTimeStepSize;

        coefficients.resize(coefficientSize);
        for (int i = 0; i < coefficients.size(); ++i){
            coefficients[i] = std::make_unique<std::vector<float>>(numberOfBandsCache);
        }

        assert(t_in == 0);

    }


    void gabor_transform(float* audio_block, int audio_block_length, gaborator_transform_callback callback, uintptr_t callback_data) {
        resultCache.clear();

        if (audio_block == nullptr || audio_block_length == 0) { //finish
            finish(callback, callback_data);
        } else {
            analyze(audio_block, audio_block_length, callback, callback_data);
        }

        if(!resultCache.empty()){
            callback(callback_data, resultCache.data(), resultCache.size(), static_cast<int>(numberOfBandsCache));
        }
    }

    int64_t analysis_support() const {
        return latency;
    }

    int number_of_bands() const {
        return numberOfBandsCache;
    }

    ~Gaborator()= default;


private:
    void analyze(float* audio_block, int audio_block_length, gaborator_transform_callback callback, uintptr_t callback_data){
        analyzer.analyze(audio_block, t_in, t_in + audio_block_length, coefs);

        int64_t st0 = t_in - latency;
        int64_t st1 = t_in - latency + audio_block_length;

        gaborator::process([&](int band, int64_t audioSampleIndex, std::complex<float>& coef) {
            gabor_process_entry(band, int(audioSampleIndex), coef, callback, callback_data);
        }, min_band, INT_MAX, st0, st1, coefs);


        t_in += (int64_t) audio_block_length;

        int64_t t_out = t_in - latency;

        forget_before(analyzer, coefs, t_out - audio_block_length);
    }

    void finish(gaborator_transform_callback callback, uintptr_t callback_data){
        int64_t st0 = t_in - latency;
        int64_t st1 = t_in;

        //flush all till latency spot
        gaborator::process([&](int band, int64_t audioSampleIndex, std::complex<float>& coef) {
            gabor_process_entry(band, int(audioSampleIndex), coef, callback, callback_data);
        }, min_band, INT_MAX, st0, st1, coefs);

        //flush remaining
        for (int i = 1; i < coefficientSize; ++i) {
            int circularIndex = (mostRecentCoefficentIndex + i) % int(coefficientSize);

            auto& currentCoefficient = *coefficients[circularIndex];

            resultCache.insert(resultCache.end(), currentCoefficient.begin(), currentCoefficient.end());
            // fill the oldest with zeros
            std::fill(currentCoefficient.begin(), currentCoefficient.end(), 0);
        }
    }

    inline void gabor_process_entry(int band, int sampleIndex, std::complex<float>& coef, gaborator_transform_callback callback, uintptr_t callback_data) {
        int coefficientIndex = sampleIndex / frequencyBinTimeStepSize;
        int bandIndex = band - firstBandCache;

        int circularIndex = coefficientIndex % int(coefficientSize);

        auto& currentCoefficient = *coefficients[circularIndex];

        // The first results have a negative audio sample index
        // ignore these
        if (coefficientIndex > 0 && bandIndex < numberOfBandsCache) {

            // If a new index is reached, save the old (fixed) coefficients in the history
            // Fill the array with zeros to get the max
            if (coefficientIndex > mostRecentCoefficentIndex && coefficientIndex > coefficientSize) {
                // keep the new maximum
                mostRecentCoefficentIndex = coefficientIndex;
                // "copy" the oldest data to the history
                // the slice can be reused thanks to the oldest being filled with zeros just after
                resultCache.insert(resultCache.end(), currentCoefficient.begin(), currentCoefficient.end());
                // fill the oldest with zeros
                std::fill(currentCoefficient.begin(), currentCoefficient.end(), 0);
            }
            // due to reduction in precision (from audio sample accuracy to steps) multiple
            // magnitudes could be placed in the same stepIndex, bandIndex pair.
            // We take the maximum magnitudes value.
            currentCoefficient[bandIndex] = std::max(currentCoefficient[bandIndex], std::abs(coef));
        }

    }

private:


    std::vector<float> resultCache;
    std::vector<std::unique_ptr<std::vector<float>>> coefficients;
    int firstBandCache = -1;
    int numberOfBandsCache = 0;
    int mostRecentCoefficentIndex = 0;

    const int blockSize;
    const int frequencyBinTimeStepSize;
    int64_t t_in;
    int min_band;
    const int sample_rate;
    int64_t latency;
    int64_t coefficientSize;

private:
    gaborator::parameters parameters;
    gaborator::analyzer<float> analyzer;
    gaborator::coefs<float> coefs;
};

uintptr_t gaborator_initialize(int blockSize, double sampleRate, int bandsPerOctave, double minimumFrequency, double referenceFrequency, double maximumFrequency, int stepSize){
    return reinterpret_cast<uintptr_t>(new Gaborator(blockSize, sampleRate, bandsPerOctave, minimumFrequency, referenceFrequency,
                                                     maximumFrequency, stepSize));
}

long gaborator_analysis_support(uintptr_t ptr) {
    return reinterpret_cast<Gaborator*>(ptr)->analysis_support();
}

int gaborator_number_of_bands(uintptr_t ptr) {
    return reinterpret_cast<Gaborator*>(ptr)->number_of_bands();
}

void gaborator_transform(uintptr_t ptr, float* audio_block, int audio_block_length, gaborator_transform_callback callback, uintptr_t callback_data){
    reinterpret_cast<Gaborator*>(ptr)->gabor_transform(audio_block, audio_block_length, callback, callback_data);
}

void gaborator_release(uintptr_t ptr) {
    auto g = reinterpret_cast<Gaborator*>(ptr);
    delete g;
}