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

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

   The code included in this file is provided under the terms of the ISC license
   http://www.isc.org/downloads/software-support-policy/isc-license. Permission
   To use, copy, modify, and/or distribute this software for any purpose with or
   without fee is hereby granted provided that the above copyright notice and
   this permission notice appear in all copies.

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

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

/*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

 name:             AudioLatencyDemo
 version:          1.0.0
 vendor:           JUCE
 website:          http://juce.com
 description:      Tests the audio latency of a device.

 dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats,
                   juce_audio_processors, juce_audio_utils, juce_core,
                   juce_data_structures, juce_events, juce_graphics,
                   juce_gui_basics, juce_gui_extra
 exporters:        xcode_mac, vs2022, linux_make, androidstudio, xcode_iphone

 moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

 type:             Component
 mainClass:        AudioLatencyDemo

 useLocalCopy:     1

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once

#include "../Assets/DemoUtilities.h"
#include "../Assets/AudioLiveScrollingDisplay.h"

//==============================================================================
class LatencyTester  : public AudioIODeviceCallback,
                       private Timer
{
public:
    LatencyTester (TextEditor& editorBox)
        : resultsBox (editorBox)
    {}

    //==============================================================================
    void beginTest()
    {
        resultsBox.moveCaretToEnd();
        resultsBox.insertTextAtCaret (newLine + newLine + "Starting test..." + newLine);
        resultsBox.moveCaretToEnd();

        startTimer (50);

        const ScopedLock sl (lock);
        createTestSound();
        recordedSound.clear();
        playingSampleNum = recordedSampleNum = 0;
        testIsRunning = true;
    }

    void timerCallback() override
    {
        if (testIsRunning && recordedSampleNum >= recordedSound.getNumSamples())
        {
            testIsRunning = false;
            stopTimer();

            // Test has finished, so calculate the result..
            auto latencySamples = calculateLatencySamples();

            resultsBox.moveCaretToEnd();
            resultsBox.insertTextAtCaret (getMessageDescribingResult (latencySamples));
            resultsBox.moveCaretToEnd();
        }
    }

    String getMessageDescribingResult (int latencySamples)
    {
        String message;

        if (latencySamples >= 0)
        {
            message << newLine
                    << "Results:" << newLine
                    << latencySamples << " samples (" << String (latencySamples * 1000.0 / sampleRate, 1)
                    << " milliseconds)" << newLine
                    << "The audio device reports an input latency of "
                    << deviceInputLatency << " samples, output latency of "
                    << deviceOutputLatency << " samples." << newLine
                    << "So the corrected latency = "
                    << (latencySamples - deviceInputLatency - deviceOutputLatency)
                    << " samples (" << String ((latencySamples - deviceInputLatency - deviceOutputLatency) * 1000.0 / sampleRate, 2)
                    << " milliseconds)";
        }
        else
        {
            message << newLine
                    << "Couldn't detect the test signal!!" << newLine
                    << "Make sure there's no background noise that might be confusing it..";
        }

        return message;
    }

    //==============================================================================
    void audioDeviceAboutToStart (AudioIODevice* device) override
    {
        testIsRunning = false;
        playingSampleNum = recordedSampleNum = 0;

        sampleRate          = device->getCurrentSampleRate();
        deviceInputLatency  = device->getInputLatencyInSamples();
        deviceOutputLatency = device->getOutputLatencyInSamples();

        recordedSound.setSize (1, (int) (0.9 * sampleRate));
        recordedSound.clear();
    }

    void audioDeviceStopped() override {}

    void audioDeviceIOCallback (const float** inputChannelData, int numInputChannels,
                                float** outputChannelData, int numOutputChannels, int numSamples) override
    {
        const ScopedLock sl (lock);

        if (testIsRunning)
        {
            auto* recordingBuffer = recordedSound.getWritePointer (0);
            auto* playBuffer = testSound.getReadPointer (0);

            for (int i = 0; i < numSamples; ++i)
            {
                if (recordedSampleNum < recordedSound.getNumSamples())
                {
                    auto inputSamp = 0.0f;

                    for (auto j = numInputChannels; --j >= 0;)
                        if (inputChannelData[j] != nullptr)
                            inputSamp += inputChannelData[j][i];

                    recordingBuffer[recordedSampleNum] = inputSamp;
                }

                ++recordedSampleNum;

                auto outputSamp = (playingSampleNum < testSound.getNumSamples()) ? playBuffer[playingSampleNum] : 0.0f;

                for (auto j = numOutputChannels; --j >= 0;)
                    if (outputChannelData[j] != nullptr)
                        outputChannelData[j][i] = outputSamp;

                ++playingSampleNum;
            }
        }
        else
        {
            // We need to clear the output buffers, in case they're full of junk..
            for (int i = 0; i < numOutputChannels; ++i)
                if (outputChannelData[i] != nullptr)
                    zeromem (outputChannelData[i], (size_t) numSamples * sizeof (float));
        }
    }

private:
    TextEditor& resultsBox;
    AudioBuffer<float> testSound, recordedSound;
    Array<int> spikePositions;
    CriticalSection lock;

    int playingSampleNum  = 0;
    int recordedSampleNum = -1;
    double sampleRate     = 0.0;
    bool testIsRunning    = false;
    int deviceInputLatency, deviceOutputLatency;

    //==============================================================================
    // create a test sound which consists of a series of randomly-spaced audio spikes..
    void createTestSound()
    {
        auto length = ((int) sampleRate) / 4;
        testSound.setSize (1, length);
        testSound.clear();

        Random rand;

        for (int i = 0; i < length; ++i)
            testSound.setSample (0, i, (rand.nextFloat() - rand.nextFloat() + rand.nextFloat() - rand.nextFloat()) * 0.06f);

        spikePositions.clear();

        int spikePos   = 0;
        int spikeDelta = 50;

        while (spikePos < length - 1)
        {
            spikePositions.add (spikePos);

            testSound.setSample (0, spikePos,      0.99f);
            testSound.setSample (0, spikePos + 1, -0.99f);

            spikePos += spikeDelta;
            spikeDelta += spikeDelta / 6 + rand.nextInt (5);
        }
    }

    // Searches a buffer for a set of spikes that matches those in the test sound
    int findOffsetOfSpikes (const AudioBuffer<float>& buffer) const
    {
        auto minSpikeLevel = 5.0f;
        auto smooth = 0.975;
        auto* s = buffer.getReadPointer (0);
        int spikeDriftAllowed = 5;

        Array<int> spikesFound;
        spikesFound.ensureStorageAllocated (100);
        auto runningAverage = 0.0;
        int lastSpike = 0;

        for (int i = 0; i < buffer.getNumSamples() - 10; ++i)
        {
            auto samp = std::abs (s[i]);

            if (samp > runningAverage * minSpikeLevel && i > lastSpike + 20)
            {
                lastSpike = i;
                spikesFound.add (i);
            }

            runningAverage = runningAverage * smooth + (1.0 - smooth) * samp;
        }

        int bestMatch = -1;
        auto bestNumMatches = spikePositions.size() / 3; // the minimum number of matches required

        if (spikesFound.size() < bestNumMatches)
            return -1;

        for (int offsetToTest = 0; offsetToTest < buffer.getNumSamples() - 2048; ++offsetToTest)
        {
            int numMatchesHere = 0;
            int foundIndex     = 0;

            for (int refIndex = 0; refIndex < spikePositions.size(); ++refIndex)
            {
                auto referenceSpike = spikePositions.getUnchecked (refIndex) + offsetToTest;
                int spike = 0;

                while ((spike = spikesFound.getUnchecked (foundIndex)) < referenceSpike - spikeDriftAllowed
                         && foundIndex < spikesFound.size() - 1)
                    ++foundIndex;

                if (spike >= referenceSpike - spikeDriftAllowed && spike <= referenceSpike + spikeDriftAllowed)
                    ++numMatchesHere;
            }

            if (numMatchesHere > bestNumMatches)
            {
                bestNumMatches = numMatchesHere;
                bestMatch = offsetToTest;

                if (numMatchesHere == spikePositions.size())
                    break;
            }
        }

        return bestMatch;
    }

    int calculateLatencySamples() const
    {
        // Detect the sound in both our test sound and the recording of it, and measure the difference
        // in their start times..
        auto referenceStart = findOffsetOfSpikes (testSound);
        jassert (referenceStart >= 0);

        auto recordedStart = findOffsetOfSpikes (recordedSound);

        return (recordedStart < 0) ? -1
                                   : (recordedStart - referenceStart);
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LatencyTester)
};

//==============================================================================
class AudioLatencyDemo  : public Component
{
public:
    AudioLatencyDemo()
    {
        setOpaque (true);

        liveAudioScroller.reset (new LiveScrollingAudioDisplay());
        addAndMakeVisible (liveAudioScroller.get());

        addAndMakeVisible (resultsBox);
        resultsBox.setMultiLine (true);
        resultsBox.setReturnKeyStartsNewLine (true);
        resultsBox.setReadOnly (true);
        resultsBox.setScrollbarsShown (true);
        resultsBox.setCaretVisible (false);
        resultsBox.setPopupMenuEnabled (true);

        resultsBox.setColour (TextEditor::outlineColourId, Colour (0x1c000000));
        resultsBox.setColour (TextEditor::shadowColourId,  Colour (0x16000000));

        resultsBox.setText ("Running this test measures the round-trip latency between the audio output and input "
                            "devices you\'ve got selected.\n\n"
                            "It\'ll play a sound, then try to measure the time at which the sound arrives "
                            "back at the audio input. Obviously for this to work you need to have your "
                            "microphone somewhere near your speakers...");

        addAndMakeVisible (startTestButton);
        startTestButton.onClick = [this] { startTest(); };

       #ifndef JUCE_DEMO_RUNNER
        RuntimePermissions::request (RuntimePermissions::recordAudio,
                                     [this] (bool granted)
                                     {
                                         int numInputChannels = granted ? 2 : 0;
                                         audioDeviceManager.initialise (numInputChannels, 2, nullptr, true, {}, nullptr);
                                     });
       #endif

        audioDeviceManager.addAudioCallback (liveAudioScroller.get());

        setSize (500, 500);
    }

    ~AudioLatencyDemo() override
    {
        audioDeviceManager.removeAudioCallback (liveAudioScroller.get());
        audioDeviceManager.removeAudioCallback (latencyTester    .get());
        latencyTester    .reset();
        liveAudioScroller.reset();
    }

    void startTest()
    {
        if (latencyTester.get() == nullptr)
        {
            latencyTester.reset (new LatencyTester (resultsBox));
            audioDeviceManager.addAudioCallback (latencyTester.get());
        }

        latencyTester->beginTest();
    }

    void paint (Graphics& g) override
    {
        g.fillAll (findColour (ResizableWindow::backgroundColourId));
    }

    void resized() override
    {
        auto b = getLocalBounds().reduced (5);

        if (liveAudioScroller.get() != nullptr)
        {
            liveAudioScroller->setBounds (b.removeFromTop (b.getHeight() / 5));
            b.removeFromTop (10);
        }

        startTestButton.setBounds (b.removeFromBottom (b.getHeight() / 10));
        b.removeFromBottom (10);

        resultsBox.setBounds (b);
    }

private:
    // if this PIP is running inside the demo runner, we'll use the shared device manager instead
   #ifndef JUCE_DEMO_RUNNER
    AudioDeviceManager audioDeviceManager;
   #else
    AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (1, 2) };
   #endif

    std::unique_ptr<LatencyTester> latencyTester;
    std::unique_ptr<LiveScrollingAudioDisplay> liveAudioScroller;

    TextButton startTestButton  { "Test Latency" };
    TextEditor resultsBox;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioLatencyDemo)
};
