/*
 * Klang – a node+text-based synthesizer library
 *
 * This file is part of the *wellen* library (https://github.com/dennisppaul/wellen).
 * Copyright (c) 2022 Dennis P Paul.
 *
 * This library 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, version 3.
 *
 * This library 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 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/>.
 */

/**
 *       [ NODE_FFT            ]
 *       +---------------------+
 *       |                     |
 * IN00--| SIGNAL       SIGNAL |--OUT00
 *       |                     |
 *       +---------------------+
 */

// @note(uses_cmsis_dsp_library)

#ifndef NodeFFT_hpp
#define NodeFFT_hpp

#ifdef KLST_USE_CMSIS_DSP
#include <CMSIS_DSP.h>

#ifndef KLANG_NODE_FFT_PRECOMPUTE_HAMMING
#define KLANG_NODE_FFT_PRECOMPUTE_HAMMING 1
#endif

#ifndef KLANG_NODE_FFT_BUFFER_SIZE
#define KLANG_NODE_FFT_BUFFER_SIZE 2048
#endif

#if (KLANG_SAMPLES_PER_AUDIO_BLOCK > 8192)
#warning "@NodeFFT / KLANG_SAMPLES_PER_AUDIO_BLOCK exceeds maximum buffer size of 8192"
#endif

#ifndef KLANG_NODE_FFT_BUFFER_EXPANSION
#if (KLANG_NODE_FFT_BUFFER_SIZE < KLANG_SAMPLES_PER_AUDIO_BLOCK)
#warning "@NodeFFT / KLANG_NODE_FFT_BUFFER_SIZE should not be smaller than KLANG_SAMPLES_PER_AUDIO_BLOCK"
#define KLANG_NODE_FFT_BUFFER_EXPANSION 1
#else
#define KLANG_NODE_FFT_BUFFER_EXPANSION (KLANG_NODE_FFT_BUFFER_SIZE / KLANG_SAMPLES_PER_AUDIO_BLOCK)
#endif
#endif

namespace klang {
    class NodeFFT : public Node {
    public:
        static const CHANNEL_ID NUM_CH_IN  = 1;
        static const CHANNEL_ID NUM_CH_OUT = 1;

        NodeFFT() {
#if KLANG_NODE_FFT_PRECOMPUTE_HAMMING
            _fill_hamming_buffer();
#endif
            arm_rfft_fast_init_f32(&fft, _KLANG_NODE_FFT_BUFFER_SIZE);
        }

        bool connect(Connection* pConnection, CHANNEL_ID pInChannel) {
            if (pInChannel == CH_IN_SIGNAL) {
                mConnection_CH_IN_SIGNAL = pConnection;
                return true;
            }
            return false;
        }

        bool disconnect(CHANNEL_ID pInChannel) {
            if (pInChannel == CH_IN_SIGNAL) {
                mConnection_CH_IN_SIGNAL = nullptr;
                return true;
            }
            return false;
        }

        void perform_analysis() {
            if (mPerfomHammingWindow) {
                window_with_hamming();
            }
            transform_to_frequency_domain();
        }

        void process_frame() {
            float mBuffer[KLANG_SAMPLES_PER_AUDIO_BLOCK];
            update(Node::CH_IN_SIGNAL, mBuffer);
        }

        void update(float* pAudioBlock) {
            std::copy_n(pAudioBlock, KLANG_SAMPLES_PER_AUDIO_BLOCK,
                        mInputBuffer + mSampleBufferPointer * KLANG_SAMPLES_PER_AUDIO_BLOCK);
            // for (uint16_t i = 0; i < KLANG_SAMPLES_PER_AUDIO_BLOCK; i++) {
            //     mInputBuffer[i + mSampleBufferPointer * KLANG_SAMPLES_PER_AUDIO_BLOCK] = pAudioBlock[i];
            // }
            mSampleBufferPointer++;
            if (mSampleBufferPointer >= KLANG_NODE_FFT_BUFFER_EXPANSION) {
                mSampleBufferPointer = 0;
                if (mPerfomAnalysisInline) {
                    perform_analysis();
                }
            }
        }

        void update(CHANNEL_ID pChannel, float* pAudioBlock) {
            if (is_not_updated()) {
                if (mConnection_CH_IN_SIGNAL != nullptr) {
                    mConnection_CH_IN_SIGNAL->update(pAudioBlock);
                }
                flag_updated();
                update(pAudioBlock);
            }
        }

        void set_command(KLANG_CMD_TYPE pCommand, KLANG_CMD_TYPE* pPayLoad) {}

        float get_frequency() {
            return mMaxIndex * mFrequencyResolution;
        }

        float get_frequency_gaussian_interpolation() {
            // from [Improving FFT Frequency Resolution](http://www.add.ece.ufl.edu/4511/references/ImprovingFFTResoltuion.pdf)
            if (mMaxIndex > 0 && mMaxIndex < (_KLANG_NODE_FFT_BUFFER_SIZE_HALF - 1)) {
                //calculate the intermittent bin on continuous spectrum using GI
                const float mInterBin = mMaxIndex + log(mPowerBuffer[mMaxIndex + 1] / mPowerBuffer[mMaxIndex - 1]) * 0.5 / log(mPowerBuffer[mMaxIndex] * mPowerBuffer[mMaxIndex] / (mPowerBuffer[mMaxIndex + 1] * mPowerBuffer[mMaxIndex - 1]));
                //calculate max input frequency
                return (mFrequencyResolution * mInterBin);
            } else {
                return mPowerBuffer[mMaxIndex];
            }
        }

        float* get_bands() {
            return mPowerBuffer;
        }

        const uint32_t get_number_of_bands() {
            return _KLANG_NODE_FFT_BUFFER_SIZE_HALF;
        }

        void enable_hamming_window(bool pEnableWindowHamming) {
            mPerfomHammingWindow = pEnableWindowHamming;
        }

        bool is_hamming_window_enabled() {
            return mPerfomHammingWindow;
        }

        void enable_inline_analysis(bool pPerfomAnalysisInline) {
            mPerfomAnalysisInline = pPerfomAnalysisInline;
        }

    private:
        static const uint32_t      _KLANG_NODE_FFT_BUFFER_SIZE      = (KLANG_SAMPLES_PER_AUDIO_BLOCK * KLANG_NODE_FFT_BUFFER_EXPANSION);
        static const uint32_t      _KLANG_NODE_FFT_BUFFER_SIZE_HALF = _KLANG_NODE_FFT_BUFFER_SIZE / 2;
        static constexpr float     mFrequencyResolution             = (float)KLANG_AUDIO_RATE / (float)_KLANG_NODE_FFT_BUFFER_SIZE;
        static const uint8_t       mIFFTFlag                        = 0;
        Connection*                mConnection_CH_IN_SIGNAL         = nullptr;
        float                      mInputBuffer[_KLANG_NODE_FFT_BUFFER_SIZE];
        float                      mOutputBuffer[_KLANG_NODE_FFT_BUFFER_SIZE];
        float                      mPowerBuffer[_KLANG_NODE_FFT_BUFFER_SIZE_HALF];
        float32_t                  mMaxValue             = 0.0;
        uint32_t                   mMaxIndex             = 0;
        bool                       mPerfomAnalysisInline = true;
        bool                       mPerfomHammingWindow  = true;
        uint8_t                    mSampleBufferPointer  = 0;
        arm_rfft_fast_instance_f32 fft;

        void transform_to_frequency_domain() {
            /* analyze signal */
            arm_rfft_fast_f32(&fft, mInputBuffer, mOutputBuffer, mIFFTFlag);
            arm_cmplx_mag_f32(mOutputBuffer, mPowerBuffer, _KLANG_NODE_FFT_BUFFER_SIZE_HALF);
            /* find dominant frequency */
            arm_max_f32(mPowerBuffer, _KLANG_NODE_FFT_BUFFER_SIZE_HALF, &mMaxValue, &mMaxIndex);
        }

#if KLANG_NODE_FFT_PRECOMPUTE_HAMMING
        float mHammingBuffer[_KLANG_NODE_FFT_BUFFER_SIZE];
        void  _fill_hamming_buffer() {
            for (uint16_t i = 0; i < _KLANG_NODE_FFT_BUFFER_SIZE; i++) {
                const float r     = TWO_PI * i / (_KLANG_NODE_FFT_BUFFER_SIZE - 1);
                const float c     = arm_cos_f32(r);
                mHammingBuffer[i] = 0.54 - 0.46 * c;
            }
        }
#endif

        void window_with_hamming() {
#if KLANG_NODE_FFT_PRECOMPUTE_HAMMING
            arm_mult_f32(mInputBuffer, mHammingBuffer, mInputBuffer, _KLANG_NODE_FFT_BUFFER_SIZE);
#else
            /* windows the data in samples with a Hamming window */
            for (uint16_t i = 0; i < _KLANG_NODE_FFT_BUFFER_SIZE; i++) {
                const float r = TWO_PI * i / (_KLANG_NODE_FFT_BUFFER_SIZE - 1);
                // const float c = klang_math_cos(r);
                const float c = arm_cos_f32(r);
                mInputBuffer[i] *= 0.54 - 0.46 * c;
            }
#endif
        }
    };

}  // namespace klang

#endif // KLST_USE_CMSIS_DSP

#endif /* NodeFFT_hpp */