/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2022 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

#pragma once

#include <JuceHeader.h>
#include <mutex>

//==============================================================================
class MainContentComponent   : public AudioAppComponent,
                               private Timer
{
public:
    //==============================================================================
    MainContentComponent()
    {
        setSize (400, 400);
        setAudioChannels (0, 2);

        initGui();
        Desktop::getInstance().setScreenSaverEnabled (false);
        startTimer (1000);
    }

    ~MainContentComponent() override
    {
        shutdownAudio();
    }

    //==============================================================================
    void prepareToPlay (int bufferSize, double sampleRate) override
    {
        currentSampleRate = sampleRate;
        allocateBuffers (static_cast<size_t> (bufferSize));
        printHeader();
    }

    void releaseResources() override
    {
        a.clear();
        b.clear();
        c.clear();
        currentSampleRate = 0.0;
    }

    //==============================================================================
    void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
    {
        const double startTimeMs = getPreciseTimeMs();

        AudioBuffer<float>& outputAudio = *bufferToFill.buffer;
        std::size_t bufferSize = (std::size_t) outputAudio.getNumSamples();
        initialiseBuffers (bufferToFill, bufferSize);

        for (int ch = 0; ch < outputAudio.getNumChannels(); ++ch)
            crunchSomeNumbers (outputAudio.getWritePointer (ch), bufferSize, numLoopIterationsPerCallback);

        std::lock_guard<std::mutex> lock (metricMutex);

        double endTimeMs = getPreciseTimeMs();
        addCallbackMetrics (startTimeMs, endTimeMs);
    }

    //==============================================================================
    void addCallbackMetrics (double startTimeMs, double endTimeMs)
    {
        double runtimeMs = endTimeMs - startTimeMs;
        audioCallbackRuntimeMs.addValue (runtimeMs);

        if (runtimeMs > getPhysicalTimeLimitMs())
            numCallbacksOverPhysicalTimeLimit++;

        if (lastCallbackStartTimeMs > 0.0)
        {
            double gapMs = startTimeMs - lastCallbackStartTimeMs;
            audioCallbackGapMs.addValue (gapMs);

            if (gapMs > 1.5 * getPhysicalTimeLimitMs())
                numLateCallbacks++;
        }

        lastCallbackStartTimeMs = startTimeMs;
    }

    //==============================================================================
    void paint (Graphics& g) override
    {
        g.fillAll (Colours::black);
        g.setFont (Font (16.0f));
        g.setColour (Colours::white);
        g.drawText ("loop iterations / audio callback",
                    getLocalBounds().withY (loopIterationsSlider.getHeight()), Justification::centred, true);
    }

    //==============================================================================
    void resized() override
    {
        loopIterationsSlider.setBounds (getLocalBounds().withSizeKeepingCentre (proportionOfWidth (0.9f), 50));
    }

private:
    //==============================================================================
    void initGui()
    {
        loopIterationsSlider.setSliderStyle (Slider::LinearBar);
        loopIterationsSlider.setRange (0, 30000, 250);
        loopIterationsSlider.setValue (15000);
        loopIterationsSlider.setColour (Slider::thumbColourId, Colours::white);
        loopIterationsSlider.setColour (Slider::textBoxTextColourId, Colours::grey);
        updateNumLoopIterationsPerCallback();
        addAndMakeVisible (loopIterationsSlider);
    }

    //==============================================================================
    void allocateBuffers (std::size_t bufferSize)
    {
        a.resize (bufferSize);
        b.resize (bufferSize);
        c.resize (bufferSize);
    }

    //==============================================================================
    void initialiseBuffers (const AudioSourceChannelInfo& bufferToFill, std::size_t bufferSize)
    {
        if (bufferSize != a.size())
        {
            jassertfalse;
            Logger::writeToLog ("WARNING: Unexpected buffer size received."
                                "expected: " + String (a.size()) +
                                ", actual: " + String (bufferSize));

            if (bufferSize > a.size())
                Logger::writeToLog ("WARNING: Need to allocate larger buffers on audio thread!");

            allocateBuffers (bufferSize);
        }

        bufferToFill.clearActiveBufferRegion();
        std::fill (a.begin(), a.end(), 0.09f);
        std::fill (b.begin(), b.end(), 0.1f );
        std::fill (c.begin(), c.end(), 0.11f);
    }

    //==============================================================================
    void crunchSomeNumbers (float* outBuffer, std::size_t bufferSize, int numIterations) noexcept
    {
        jassert (a.size() == bufferSize && b.size() == bufferSize && c.size() == bufferSize);

        for (int i = 0; i < numIterations; ++i)
        {
            FloatVectorOperations::multiply (c.data(), a.data(), b.data(), (int) bufferSize);
            FloatVectorOperations::addWithMultiply (outBuffer, b.data(), c.data(), (int) bufferSize);
        }
    }

    //==============================================================================
    void timerCallback() override
    {
        printAndResetPerformanceMetrics();
    }

    //==============================================================================
    void printHeader() const
    {
        Logger::writeToLog ("buffer size = " + String (a.size()) + " samples");
        Logger::writeToLog ("sample rate = " + String (currentSampleRate) + " Hz");
        Logger::writeToLog ("physical time limit / callback = " + String (getPhysicalTimeLimitMs() )+ " ms");
        Logger::writeToLog ("");
        Logger::writeToLog ("         | callback exec time / physLimit   | callback time gap / physLimit    | callback counters        ");
        Logger::writeToLog ("numLoops | avg     min     max     stddev   | avg     min     max     stddev   | called  late    >limit   ");
        Logger::writeToLog ("-----    | -----   -----   -----   -----    | -----   -----   -----   -----    | ---     ---     ---      ");
    }

    //==============================================================================
    void printAndResetPerformanceMetrics()
    {
        std::unique_lock<std::mutex> lock (metricMutex);

        auto runtimeMetric = audioCallbackRuntimeMs;
        auto gapMetric = audioCallbackGapMs;
        auto late = numLateCallbacks;
        auto overLimit = numCallbacksOverPhysicalTimeLimit;

        resetPerformanceMetrics();
        updateNumLoopIterationsPerCallback();

        lock.unlock();

        Logger::writeToLog (String (numLoopIterationsPerCallback).paddedRight (' ', 8) + " | "
                            + getPercentFormattedMetricString (runtimeMetric) + " | "
                            + getPercentFormattedMetricString (gapMetric) + " | "
                            + String (runtimeMetric.getCount()).paddedRight (' ', 8)
                            + String (late).paddedRight (' ', 8)
                            + String (overLimit).paddedRight (' ', 8) + " | ");
    }

    //==============================================================================
    String getPercentFormattedMetricString (const StatisticsAccumulator<double> metric) const
    {
        auto physTimeLimit = getPhysicalTimeLimitMs();

        return (String (100.0 * metric.getAverage()  / physTimeLimit, 1) + "%").paddedRight (' ', 8)
             + (String (100.0 * metric.getMinValue() / physTimeLimit, 1) + "%").paddedRight (' ', 8)
             + (String (100.0 * metric.getMaxValue() / physTimeLimit, 1) + "%").paddedRight (' ', 8)
             + String (metric.getStandardDeviation(), 3).paddedRight (' ', 8);
    }

    //==============================================================================
    void resetPerformanceMetrics()
    {
        audioCallbackRuntimeMs.reset();
        audioCallbackGapMs.reset();
        numLateCallbacks = 0;
        numCallbacksOverPhysicalTimeLimit = 0;
    }

    //==============================================================================
    void updateNumLoopIterationsPerCallback()
    {
        numLoopIterationsPerCallback = (int) loopIterationsSlider.getValue();
    }

    //==============================================================================
    static double getPreciseTimeMs() noexcept
    {
        return 1000.0 * (double) Time::getHighResolutionTicks() / (double) Time::getHighResolutionTicksPerSecond();
    }

    //==============================================================================
    double getPhysicalTimeLimitMs() const noexcept
    {
        return 1000.0 * (double) a.size() / currentSampleRate;
    }

    //==============================================================================
    std::vector<float> a, b, c; // must always be of size == current bufferSize
    double currentSampleRate = 0.0;

    StatisticsAccumulator<double> audioCallbackRuntimeMs;
    StatisticsAccumulator<double> audioCallbackGapMs;
    double lastCallbackStartTimeMs = 0.0;
    int numLateCallbacks = 0;
    int numCallbacksOverPhysicalTimeLimit = 0;
    int numLoopIterationsPerCallback;

    Slider loopIterationsSlider;
    std::mutex metricMutex;

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};


// (This function is called by the app startup code to create our main component)
Component* createMainContentComponent()     { return new MainContentComponent(); }
