/*
 * 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_ENVELOPE       ]
 *       +---------------------+
 *       |                     |
 * IN00--| SIGNAL       SIGNAL |--OUT00
 * IN01--| TRIGGER     TRIGGER |--OUT01
 *       |                     |
 *       +---------------------+
 */

#ifndef NodeEnvelope_hpp
#define NodeEnvelope_hpp

#include <vector>

#include "NodeKernel.hpp"

namespace klang {
    class EnvelopeStage {
    public:
        EnvelopeStage(float pDurationMillis, float pValue) : duration_ms(pDurationMillis), value(pValue) {}
        EnvelopeStage() : duration_ms(0.0), value(0.0) {}
        float duration_ms;
        float value;
    };

    class NodeEnvelope : public Node {
    public:
        static const CHANNEL_ID CH_IN_TRIGGER = 1;
        static const CHANNEL_ID NUM_CH_IN     = 2;

        static const CHANNEL_ID CH_OUT_TRIGGER = 1;
        static const CHANNEL_ID NUM_CH_OUT     = 2;

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

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

        void update(CHANNEL_ID pChannel, float* pAudioBlock) {
            if (is_not_updated()) {
                if (mConnection_CH_IN_SIGNAL != nullptr) {
                    mConnection_CH_IN_SIGNAL->update(pAudioBlock);
                }
                mBlock_TRIGGER = AudioBlockPool::NO_ID;
                if (mConnection_CH_IN_TRIGGER != nullptr) {
                    mBlock_TRIGGER = AudioBlockPool::instance().request();
                    mConnection_CH_IN_TRIGGER->update(mBlock_TRIGGER);
                    float* mBlockData_TRIGGER = AudioBlockPool::instance().data(mBlock_TRIGGER);
                    for (uint16_t i = 0; i < KLANG_SAMPLES_PER_AUDIO_BLOCK; i++) {
                        const float mCurrentSample = mBlockData_TRIGGER[i];
                        mBlockData_TRIGGER[i]      = evaluateEdge(mPreviousSample, mCurrentSample);
                        mPreviousSample            = mCurrentSample;
                    }
                }
                flag_updated();
            }

            if (pChannel == CH_OUT_SIGNAL) {
                const bool   mHasTriggerSignal  = (mBlock_TRIGGER != AudioBlockPool::NO_ID);
                float* mBlockData_TRIGGER = nullptr;
                if (mHasTriggerSignal) {
                    mBlockData_TRIGGER = AudioBlockPool::instance().data(mBlock_TRIGGER);
                }
                for (uint16_t i = 0; i < KLANG_SAMPLES_PER_AUDIO_BLOCK; i++) {
                    if (mHasTriggerSignal) {
                        const float mTriggerState = mBlockData_TRIGGER[i];
                        if (mTriggerState == RAMP_RISING_EDGE) {
                            start();
                        } else if (mTriggerState == RAMP_FALLING_EDGE) {
                            stop();
                        }
                    }
                    pAudioBlock[i] = kernel(pAudioBlock[i]);
                }
            } else if (pChannel == CH_OUT_TRIGGER) {
                const bool mHasTriggerSignal = (mBlock_TRIGGER != AudioBlockPool::NO_ID);
                if (mHasTriggerSignal) {
                    float* mBlockData_TRIGGER = AudioBlockPool::instance().data(mBlock_TRIGGER);
                    KLANG_COPY_AUDIO_BUFFER(pAudioBlock, mBlockData_TRIGGER);
                } else {
                    KLANG_FILL_AUDIO_BUFFER(pAudioBlock, 0.0);
                }
            }
        }

        std::vector<EnvelopeStage>& stages() {
            return mEnvelopeStages;
        }

        void add_stage_ms(float pDuration, float pValue) {
            mEnvelopeStages.push_back(EnvelopeStage(pDuration, pValue));
        }

        void add_stage(float pDuration, float pValue) {
            add_stage_ms(pDuration * M_TIME_SCALE, pValue);
        }

        void clear_stages() {
            mEnvelopeStages.clear();
        }

        void start() {
            mEnvelopeDone = false;
            mValue        = mStartValue;
            prepareNextStage(0);
        }

        void stop() {
            mEnvelopeDone = true;
        }

        void set_time_scale(float pTimeScale) {
            mTimeScale = pTimeScale;
        }

        float get_time_scale() {
            return mTimeScale;
        }

        void set_value_scale(float pValueScale) {
            mValueScale = pValueScale;
        }

        float get_value_scale() {
            return mValueScale;
        }

        void set_start_value(float pStartValue) {
            mStartValue = pStartValue;
        }

        float get_start_value() {
            return mStartValue;
        }

        void set_loop(bool pLoop) {
            fLoop = pLoop;
        }

        void set_command(KLANG_CMD_TYPE pCommand, KLANG_CMD_TYPE* pPayLoad) {
            switch (pCommand) {
                case KLANG_SET_TIME_SCALE_F32:
                    set_time_scale(KlangMath::FLOAT_32(pPayLoad));
                    break;
                case KLANG_SET_ADD_STAGE_F32_F32:
                    add_stage(KlangMath::FLOAT_32(pPayLoad, 0), KlangMath::FLOAT_32(pPayLoad, 4));
                    break;
                case KLANG_SET_CLEAR_STAGES:
                    clear_stages();
                    break;
                case KLANG_SET_START:
                    start();
                    break;
                case KLANG_SET_STOP:
                    stop();
                    break;
            }
        }

    private:
        Connection* mConnection_CH_IN_SIGNAL  = nullptr;
        Connection* mConnection_CH_IN_TRIGGER = nullptr;

        AUDIO_BLOCK_ID mBlock_TRIGGER = AudioBlockPool::NO_ID;

        static constexpr float RAMP_NO_EDGE                     = 0.0;
        static constexpr float RAMP_RISING_EDGE                 = 1.0;
        static constexpr float RAMP_FALLING_EDGE                = -1.0;
        static constexpr float       mThreshold                       = 0.0;  // @todo(could be made configurable)
        static constexpr float       M_TIME_SCALE                     = 1000;
        static constexpr float       KLANG_AUDIO_RATE_UINT16_FRAC_INV = M_TIME_SCALE / KLANG_AUDIO_RATE_UINT16;

        std::vector<EnvelopeStage> mEnvelopeStages;

        uint16_t    mEnvStage       = 0;
        float       mStartValue     = 0.0;
        float       mValue          = 0.0;
        float       mDelta          = 0.0;
        float       mTimeScale      = 1.0;
        float       mValueScale     = 1.0;
        float       mStageDuration  = 0.0;
        bool        mEnvelopeDone   = true;
        bool        fLoop           = false;
        float mPreviousSample = mThreshold;

        inline float kernel(const float s) {
            if (!mEnvelopeDone) {
                if (mEnvStage < mEnvelopeStages.size()) {
                    mValue += mTimeScale * mDelta;
                    mStageDuration += mTimeScale * KLANG_AUDIO_RATE_UINT16_FRAC_INV;
                    if (mStageDuration > mEnvelopeStages[mEnvStage].duration_ms) {
                        mEnvStage++;
                        if (mEnvStage < mEnvelopeStages.size()) {
                            prepareNextStage(mEnvStage);
                        } else {
                            if (fLoop) {
                                start();
                            } else {
                                stop();
                            }
                        }
                    }
                }
            }
            const bool        mHasInputSignal = (mConnection_CH_IN_SIGNAL != nullptr);
            const float a               = mHasInputSignal ? s * mValue : mValue;
            return a * mValueScale;
        }

        void prepareNextStage(uint16_t pEnvStage) {
            mEnvStage      = pEnvStage;
            mStageDuration = 0.0;
            if (mEnvelopeStages.size() > 0) {
                setDelta(mEnvStage);
            }
        }

        void setDelta(uint16_t pEnvStage) {
            const float mTotalDelta = mEnvelopeStages[pEnvStage].value - mValue;
            mDelta                  = compute_delta_fraction(mTotalDelta, mEnvelopeStages[mEnvStage].duration_ms);
        }

        float compute_delta_fraction(const float pDelta, const float pDuration) {
            if (pDuration > 0) {
                const float a = pDelta * KLANG_AUDIO_RATE_UINT16_FRAC_INV;
                return a / pDuration;
            } else {
                return pDelta;
            }
        }

        const float evaluateEdge(const float pPreviousSample, const float pCurrentSample) {
            if ((pPreviousSample <= mThreshold) && (pCurrentSample > mThreshold)) {
                return RAMP_RISING_EDGE;
            } else if ((pPreviousSample >= mThreshold) && (pCurrentSample < mThreshold)) {
                return RAMP_FALLING_EDGE;
            } else {
                return RAMP_NO_EDGE;
            }
        }
    };
}  // namespace klang

#endif /* NodeEnvelope_hpp */