/*
 * DISTRHO Cardinal Plugin
 * Copyright (C) 2021-2024 Filipe Coelho <falktx@falktx.com>
 *
 * 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; either version 3 of
 * the License, or 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 for more details.
 *
 * For a full copy of the GNU General Public License see the LICENSE file.
 */

#include "plugin.hpp"
#include "plugincontext.hpp"
#include "ModuleWidgets.hpp"
#include "Widgets.hpp"
#include "engine/TerminalModule.hpp"

// -----------------------------------------------------------------------------------------------------------

USE_NAMESPACE_DISTRHO;

template<int numIO>
struct HostAudio : TerminalModule {
    CardinalPluginContext* const pcontext;
    const int numParams;
    const int numInputs;
    const int numOutputs;
    bool bypassed = false;
    bool in1connected = false;
    bool in2connected = false;
    uint32_t dataFrame = 0;
    uint32_t lastProcessCounter = 0;

    // for rack core audio module compatibility
    dsp::RCFilter dcFilters[numIO];
    bool dcFilterEnabled = (numIO == 2);

    HostAudio()
        : pcontext(static_cast<CardinalPluginContext*>(APP)),
          numParams(numIO == 2 ? 1 : 0),
          numInputs(pcontext->variant == kCardinalVariantMain ? numIO : 2),
          numOutputs(pcontext->variant == kCardinalVariantSynth ? 0 : pcontext->variant == kCardinalVariantMain ? numIO : 2)
    {
        if (pcontext == nullptr)
            throw rack::Exception("Plugin context is null");

        config(numParams, numIO, numIO, 0);

        if (numParams != 0)
            configParam(0, 0.f, 2.f, 1.f, "Level", " dB", -10, 40);

        const float sampleTime = pcontext->engine->getSampleTime();
        for (int i=0; i<numIO; ++i)
            dcFilters[i].setCutoffFreq(10.f * sampleTime);
    }

    void onReset() override
    {
        dcFilterEnabled = (numIO == 2);
    }

    void onSampleRateChange(const SampleRateChangeEvent& e) override
    {
        for (int i=0; i<numIO; ++i)
            dcFilters[i].setCutoffFreq(10.f * e.sampleTime);
    }

    void processTerminalInput(const ProcessArgs&) override
    {
        const uint32_t bufferSize = pcontext->bufferSize;
        const uint32_t processCounter = pcontext->processCounter;

        // only checked on input
        if (lastProcessCounter != processCounter)
        {
            bypassed = isBypassed();
            dataFrame = 0;
            lastProcessCounter = processCounter;

            if (numIO == 2)
            {
                in1connected = inputs[0].isConnected();
                in2connected = inputs[1].isConnected();
            }
        }

        // only incremented on output
        const uint32_t k = dataFrame;
        DISTRHO_SAFE_ASSERT_INT2_RETURN(k < bufferSize, k, bufferSize,);

        // from host into cardinal, shows as output plug
        if (bypassed)
        {
            for (int i=0; i<numOutputs; ++i)
                outputs[i].setVoltage(0.0f);
        }
        else if (const float* const* const dataIns = pcontext->dataIns)
        {
            for (int i=0; i<numOutputs; ++i)
                outputs[i].setVoltage(dataIns[i][k] * 10.0f);
        }
    }

    json_t* dataToJson() override
    {
        json_t* const rootJ = json_object();
        DISTRHO_SAFE_ASSERT_RETURN(rootJ != nullptr, nullptr);

        json_object_set_new(rootJ, "dcFilter", json_boolean(dcFilterEnabled));
        return rootJ;
    }

    void dataFromJson(json_t* const rootJ) override
    {
        json_t* const dcFilterJ = json_object_get(rootJ, "dcFilter");
        DISTRHO_SAFE_ASSERT_RETURN(dcFilterJ != nullptr,);

        dcFilterEnabled = json_boolean_value(dcFilterJ);
    }
};

struct HostAudio2 : HostAudio<2> {
#ifndef HEADLESS
    // for stereo meter
    uint32_t internalDataFrame = 0;
    float internalDataBufferL[128] = {};
    float internalDataBufferR[128] = {};
    volatile bool resetMeters = true;
    float gainMeterL = 0.0f;
    float gainMeterR = 0.0f;
#endif

#ifndef HEADLESS
    void onReset() override
    {
        HostAudio<2>::onReset();
        resetMeters = true;
    }

    void onSampleRateChange(const SampleRateChangeEvent& e) override
    {
        HostAudio<2>::onSampleRateChange(e);
        resetMeters = true;
    }
#endif

    void processTerminalOutput(const ProcessArgs&) override
    {
        if (pcontext->bypassed || (!in1connected && !in2connected))
        {
#ifndef HEADLESS
            if (resetMeters)
            {
                internalDataFrame = 0;
                gainMeterL = gainMeterR = 0.0f;
                resetMeters = false;
            }
#endif
            return;
        }

        const uint32_t bufferSize = pcontext->bufferSize;

        // only incremented on output
        const uint32_t k = dataFrame++;
        DISTRHO_SAFE_ASSERT_INT2_RETURN(k < bufferSize, k, bufferSize,);

        if (bypassed)
            return;

        float** const dataOuts = pcontext->dataOuts;

        // gain (stereo variant only)
        const float gain = std::pow(params[0].getValue(), 2.f);

        // read stereo values
        float valueL, valueR;

        if (in1connected)
        {
            if (!std::isfinite(dataOuts[0][k]))
                __builtin_unreachable();

            valueL = inputs[0].getVoltageSum() * 0.1f;

            if (dcFilterEnabled)
            {
                dcFilters[0].process(valueL);
                valueL = dcFilters[0].highpass();
            }

            valueL = clamp(valueL * gain, -1.0f, 1.0f);
            dataOuts[0][k] += valueL;
        }
        else
        {
            valueL = 0.0f;
        }

        if (in2connected)
        {
            if (!std::isfinite(dataOuts[1][k]))
                __builtin_unreachable();

            valueR = inputs[1].getVoltageSum() * 0.1f;

            if (dcFilterEnabled)
            {
                dcFilters[1].process(valueR);
                valueR = dcFilters[1].highpass();
            }

            valueR = clamp(valueR * gain, -1.0f, 1.0f);
            dataOuts[1][k] += valueR;
        }
        else if (in1connected)
        {
            valueR = valueL;
            dataOuts[1][k] += valueL;
        }
#ifndef HEADLESS
        else
        {
            valueR = 0.0f;
        }

        if (pcontext->variant == kCardinalVariantMini)
        {
            const uint32_t j = internalDataFrame++;
            internalDataBufferL[j] = valueL;
            internalDataBufferR[j] = valueR;

            if (internalDataFrame == 4)
            {
                internalDataFrame = 0;

                if (resetMeters)
                    gainMeterL = gainMeterR = 0.0f;

                gainMeterL = std::max(gainMeterL, d_findMaxNormalizedFloat<4>(internalDataBufferL));

                if (in2connected)
                    gainMeterR = std::max(gainMeterR, d_findMaxNormalizedFloat<4>(internalDataBufferR));
                else
                    gainMeterR = gainMeterL;

                resetMeters = false;
            }
        }
        else
        {
            const uint32_t j = internalDataFrame++;
            internalDataBufferL[j] = valueL;
            internalDataBufferR[j] = valueR;

            if (internalDataFrame == 128)
            {
                internalDataFrame = 0;

                if (resetMeters)
                    gainMeterL = gainMeterR = 0.0f;

                gainMeterL = std::max(gainMeterL, d_findMaxNormalizedFloat128(internalDataBufferL));

                if (in2connected)
                    gainMeterR = std::max(gainMeterR, d_findMaxNormalizedFloat128(internalDataBufferR));
                else
                    gainMeterR = gainMeterL;

                resetMeters = false;
            }
        }
#endif
    }
};

struct HostAudio8 : HostAudio<8> {
    // no meters in this variant

    void processTerminalOutput(const ProcessArgs&) override
    {
        if (pcontext->bypassed)
            return;

        const uint32_t bufferSize = pcontext->bufferSize;

        // only incremented on output
        const uint32_t k = dataFrame++;
        DISTRHO_SAFE_ASSERT_INT2_RETURN(k < bufferSize, k, bufferSize,);

        if (bypassed)
            return;

        float** const dataOuts = pcontext->dataOuts;

        for (int i=0; i<numInputs; ++i)
        {
            float v = inputs[i].getVoltageSum() * 0.1f;

            if (dcFilterEnabled)
            {
                dcFilters[i].process(v);
                v = dcFilters[i].highpass();
            }

            dataOuts[i][k] += clamp(v, -1.0f, 1.0f);
        }
    }

};

#ifndef HEADLESS
// --------------------------------------------------------------------------------------------------------------------

template<int numIO>
struct HostAudioWidget : ModuleWidgetWith8HP {
    HostAudio<numIO>* const module;
    CardinalPluginContext* const pcontext;

    HostAudioWidget(HostAudio<numIO>* const m)
        : module(m),
          pcontext(static_cast<CardinalPluginContext*>(APP))
    {
        setModule(m);
        setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/HostAudio.svg")));

        createAndAddScrews();

        const uint8_t ioCount = pcontext->variant == kCardinalVariantMain ? 8 : 2;

        for (uint i=0; i<numIO; ++i)
        {
            createAndAddInput(i, i, i<ioCount);
            createAndAddOutput(i, i, i<ioCount);
        }
    }

    void appendContextMenu(Menu* const menu) override {
        menu->addChild(new MenuSeparator);
        menu->addChild(createBoolPtrMenuItem("DC blocker", "", &module->dcFilterEnabled));
    }
};

// --------------------------------------------------------------------------------------------------------------------

struct HostAudioNanoMeter : NanoMeter {
    HostAudio2* const module;

    HostAudioNanoMeter(HostAudio2* const m)
        : module(m)
    {
        hasGainKnob = true;
    }

    void updateMeters() override
    {
        if (module == nullptr || module->resetMeters)
            return;

        // Only fetch new values once DSP side is updated
        gainMeterL = module->gainMeterL;
        gainMeterR = module->gainMeterR;
        module->resetMeters = true;
    }
};

// --------------------------------------------------------------------------------------------------------------------

struct HostAudioWidget2 : HostAudioWidget<2> {
    typedef NanoKnob<> Knob;

    HostAudioWidget2(HostAudio2* const m)
        : HostAudioWidget<2>(m)
    {
        // FIXME
        const float middleX = box.size.x * 0.5f;
        addParam(createParamCentered<Knob>(Vec(middleX, 310.0f), m, 0));

        HostAudioNanoMeter* const meter = new HostAudioNanoMeter(m);
        meter->box.pos = Vec(middleX - padding + 2.75f, startY + padding * 2);
        meter->box.size = Vec(padding * 2.0f - 4.0f, 136.0f);
        addChild(meter);
    }

    void draw(const DrawArgs& args) override
    {
        drawBackground(args.vg);
        drawOutputJacksArea(args.vg, 2);
        setupTextLines(args.vg);

        drawTextLine(args.vg, 0, "Left/M");
        drawTextLine(args.vg, 1, "Right");

        ModuleWidgetWith8HP::draw(args);
    }
};

struct HostAudioWidget8 : HostAudioWidget<8> {
    HostAudioWidget8(HostAudio8* const m)
        : HostAudioWidget<8>(m) {}

    void draw(const DrawArgs& args) override
    {
        const uint8_t ioCount = pcontext->variant == kCardinalVariantMain ? 8 : 2;

        drawBackground(args.vg);
        drawOutputJacksArea(args.vg, ioCount);
        setupTextLines(args.vg);

        for (int i=0; i<ioCount; ++i)
        {
            char text[] = {'A','u','d','i','o',' ',static_cast<char>('0'+i+1),'\0'};
            drawTextLine(args.vg, i, text);
        }

        ModuleWidgetWith8HP::draw(args);
    }
};

#else
// --------------------------------------------------------------------------------------------------------------------

struct HostAudioWidget2 : ModuleWidget {
    HostAudioWidget2(HostAudio2* const module) {
        setModule(module);
        for (uint i=0; i<2; ++i) {
            addInput(createInput<PJ301MPort>({}, module, i));
            addOutput(createOutput<PJ301MPort>({}, module, i));
        }
    }
};
struct HostAudioWidget8 : ModuleWidget {
    HostAudioWidget8(HostAudio8* const module) {
        setModule(module);
        for (uint i=0; i<8; ++i) {
            addInput(createInput<PJ301MPort>({}, module, i));
            addOutput(createOutput<PJ301MPort>({}, module, i));
        }
    }
};

// --------------------------------------------------------------------------------------------------------------------
#endif

Model* modelHostAudio2 = createModel<HostAudio2, HostAudioWidget2>("HostAudio2");
Model* modelHostAudio8 = createModel<HostAudio8, HostAudioWidget8>("HostAudio8");

// --------------------------------------------------------------------------------------------------------------------
