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

    obsluhaGranuli.h
    Created: 28 Jan 2023 7:12:21pm
    Author:  Tomáš Pospíšil

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

/*
* This file contains all functions for grain creation and amplitude envelopes. It
* handles the procceses of grains like when the user wants to hear the grain or reset
* it. This file also includes the function for appending the grains to the certain
* channel and the function for removing all grains from channel. 
*/

#pragma once

#include "kanal.h"
#include "prehravac.h"


class handleGrain : public juce::Slider,
                    public juce::Slider::Listener,
                    public audioPlayer
{
public:

    /*
    * Constructor of this class. It sets the Channel objects to initial state.
    */
    handleGrain(){
        ch1.grainList = nullptr;
        ch1.ret = "Kanal 1";
        ch2.grainList = nullptr;
        ch2.ret = "Kanal 2";
        ch3.grainList = nullptr;
        ch3.ret = "Kanal 3";
        ch4.grainList = nullptr;
        ch4.ret = "Kanal 4";
        ch5.grainList = nullptr;
        ch5.ret = "Kanal 5";
        ch6.grainList = nullptr;
        ch6.ret = "Kanal 6";
        ch7.grainList = nullptr;
        ch7.ret = "Kanal 7";
        ch8.grainList = nullptr;
        ch8.ret = "Kanal 8";
    }

    /*
    * Variables of this class. Most important are Sliders for setting the parameters.
    * There are also ToggleButtons and TextButtons, which force the function callings.
    */
    Channel ch1;
    Channel ch2;
    Channel ch3;
    Channel ch4;
    Channel ch5;
    Channel ch6;
    Channel ch7;
    Channel ch8;

    juce::GroupComponent group3;

    juce::Label duration;
    juce::Label envelope;
    juce::Label density;
    juce::Label timeOffset;
    juce::Label volume;

    juce::Slider delka;
    juce::Slider obalka;
    juce::Slider pocet;
    juce::Slider offset;
    juce::Slider hlasitost;

    juce::ToggleButton kanal1;
    juce::ToggleButton kanal2;
    juce::ToggleButton kanal3;
    juce::ToggleButton kanal4;
    juce::ToggleButton kanal5;
    juce::ToggleButton kanal6;
    juce::ToggleButton kanal7;
    juce::ToggleButton kanal8;

    juce::TextButton playGrain;
    juce::TextButton makeGrain;
    juce::TextButton resetGrain;

    juce::GroupComponent group2;

    /*
    * Destructor of this class. It releases the memory.
    */
    ~handleGrain() {
        releaseResources();
    }
   
    /*
    * Function, that releases memory of the transportSource variable.
    */
    void releaseResources() override
    {
        transportSource.releaseResources();
    }


    /*
    * This function is called, when the plugin is closing. It removes all the grains
    * on certain channel from memory. 
    * 
    * @param head       This parameter needs to be the first grain in the linked list. 
    */
    void deleteChannelGrainList(Grain** head) {
        Grain* current = *head;
        while (current != nullptr) {
            Grain* next = current->next;
            delete current;
            current = next;
        }
        *head = nullptr;
    }


    /*
    * This function looks to what channel is the grain assigned. If the relevant channel
    * is toggled, the grain is appended at the end of the linked list of this channel.
    */
    void createGrain() {
        if (kanal1.getToggleState() == true) {
            append(&ch1.grainList);                             
        }
        if (kanal2.getToggleState() == true) {
            append(&ch2.grainList);
        }
        if (kanal3.getToggleState() == true) {
            append(&ch3.grainList);
        }
        if (kanal4.getToggleState() == true) {
            append(&ch4.grainList);
        }
        if (kanal5.getToggleState() == true) {
            append(&ch5.grainList);
        }
        if (kanal6.getToggleState() == true) {
            append(&ch6.grainList);
        }
        if (kanal7.getToggleState() == true) {
            append(&ch7.grainList);
        }
        if (kanal8.getToggleState() == true) {
            append(&ch8.grainList);
        }
    }

    /*
    * When user clicks the Play Grain button, this functions is called. Data are copied
    * to the grainBuffer, the amplitude envelope is applied on them and also the volume
    * of the grain is set. Function setAudioChannels then invokes the reproduction.
    */
    void grainPlayButtonClicked() {
        fillBuffer(&grainBuffer);
        adsrSettings(grainBuffer);
        float gain = 1.0f / 100 * hlasitost.getValue();
        grainBuffer.applyGain(gain);
        
        position = 0;
        bufferPlaying = true;
        setAudioChannels(0, 8);
    }

    /*
    * This function resets all the sliders and all the ToggleButtons in  
    * this class to its initial state. 
    */
    void resetButtonClicked() {
        delka.setValue(0, juce::dontSendNotification);
        obalka.setValue(0, juce::dontSendNotification);
        pocet.setValue(0, juce::dontSendNotification);
        offset.setValue(0, juce::dontSendNotification);
        hlasitost.setValue(0, juce::dontSendNotification);
        kanal1.setToggleState(false, juce::dontSendNotification);
        kanal2.setToggleState(false, juce::dontSendNotification);
        kanal3.setToggleState(false, juce::dontSendNotification);
        kanal4.setToggleState(false, juce::dontSendNotification);
        kanal5.setToggleState(false, juce::dontSendNotification);
        kanal6.setToggleState(false, juce::dontSendNotification);
        kanal7.setToggleState(false, juce::dontSendNotification);
        kanal8.setToggleState(false, juce::dontSendNotification);
    }

    /*
    * This function sets the density slider values based on the grain length
    * values. With longer length there is smaller density.
    * 
    * @param slider     This parameter is the slider, which has been mooved.
    */
    void sliderValueChanged(juce::Slider* slider) override{
        if (slider == &delka) {
            if ((int)delka.getValue() == 1) {
                pocet.setRange(1, 100.0, 1);
            }
            else {
                int max = 1000 / (int)delka.getValue();
                pocet.setRange(1, max, 1);
            }
        }
    }


    /*
    * This function copies the sound samples from the source to a buffer passed to this
    * function. It calculates the number samples, which has to be copied based on the 
    * length of grain. It takes the position of the cursor in the thumbnail and from 
    * this position the number of samples is calculated. The samplerate is 48 000 Hz.
    * 
    * @param buffer     AudioSampleBuffer expected to be filled with samples.
    */
    juce::AudioSampleBuffer fillBuffer(juce::AudioSampleBuffer* buffer) {
        auto tSPosition = transportSource.getCurrentPosition();
        auto stratingSample = tSPosition * 48000;
        auto endingSample = (tSPosition + (delka.getValue() / 1000)) * 48000;
        auto numSamples = endingSample - stratingSample;

        buffer->setSize(fileBuffer.getNumChannels(), (int)numSamples);
        for (int ch = 0; ch < buffer->getNumChannels(); ch++) {
            buffer->copyFrom(ch, 0, fileBuffer, ch, stratingSample, (int)numSamples);
        }
        return *buffer;
    }

    /*
    * This function adds zero samples at the end of the buffer passed to this function.
    * It adds the silence between two grains in the grainstream. It calculets the number
    * of zero samples based on the grain length and density of these grains. 
    * 
    * @param buffer     AudioSampleBuffer expected to be filled with samples of zeroes.
    */
    void addZeroesToBuffer(juce::AudioSampleBuffer& buffer) {
        int newSize = 1000/pocet.getValue()*48;
        int currentNumSamples = buffer.getNumSamples();
        int numSamplesToAdd = newSize - buffer.getNumSamples();
        buffer.setSize(buffer.getNumChannels(), newSize, true, true, false);
        buffer.clear(currentNumSamples, numSamplesToAdd);
    }


    /*
    * This function apppends the Grain object at the end of linked list, which head is
    * being expected to be passed as a function parameter. This function allocates the 
    * Grain object in memory, then it cycles through the linked list to its end and setts
    * the end pointer to this object. 
    * 
    * @param head       Expected begining of the linked list on certain channel.
    */
    void append(Grain** head) {
        juce::AudioSampleBuffer buffer, offsetBuffer;
        buffer = fillBuffer(&buffer);
        adsrSettings(buffer);
        addZeroesToBuffer(buffer);

        offsetBuffer.setSize(buffer.getNumChannels(), ((int)offset.getValue() * 48000));
        offsetBuffer.clear(0, ((int)offset.getValue() * 48000));
        auto* granule = new Grain((int)delka.getValue(), (int)obalka.getValue(), (int)pocet.getValue(),
                                  (int)offset.getValue(),(int)hlasitost.getValue(), buffer,offsetBuffer);

        Grain* last = *head;
        if (*head == NULL) {
            *head = granule;
            return;
        }
        while (last->next != NULL) {
            last = last->next;
        }
        last->next = granule;
        return;
    }


    /*
    * This function makes the amplitude envelope to the grain content. It takes the
    * buffer with samples and based on the obalka slider value it decides, which envelope
    * to apply on the samples. 
    * 
    * @param buffer     AudioSampleBuffer filled with audio samples prepared to be changed by envelope.
    */
    void adsrSettings(juce::AudioBuffer<float>& buffer) {
        auto numSamples = buffer.getNumSamples();
        if (obalka.getValue() == 1) {
            for (int ch = 0; ch < buffer.getNumChannels(); ch++) {
                buffer.applyGainRamp(ch, 0, (0.5*numSamples), 0, 1);
                buffer.applyGainRamp(ch, (0.5 * numSamples), (0.5 * numSamples), 1, 0);
            }
        }
        else if (obalka.getValue() == 2) {
            int nulatri = 0.3 * numSamples;
            int nulasedm = 0.7 * numSamples;
            if ((numSamples % 10) == 0) {
                for (int ch = 0; ch < buffer.getNumChannels(); ch++) {
                    buffer.applyGainRamp(ch, 0, nulatri, 0, 1);
                    buffer.applyGainRamp(ch, nulasedm, nulatri, 1, 0);
                }
            }
            else {
                for (int ch = 0; ch < buffer.getNumChannels(); ch++) {
                    buffer.applyGainRamp(ch, 0, nulatri, 0, 1);
                    buffer.applyGainRamp(ch, nulasedm, nulatri + 1, 1, 0);
                }
            }
        }
        else {
            const double peakTime = (delka.getValue()/1000) / 2.0;
            const double sigma = (delka.getValue() / 1000) / 12.0;
            applyEnvelopeToBuffer(buffer, peakTime, sigma);
        }
    }


    /*
    * This function returns the mathematical expression of Gaussian function. 
    */
    double gaussianEnvelope(double t, double tpeak, double sigma)
    {
        double exponent = (0.5) * (-pow((t - tpeak) / sigma, 2));
        return exp(exponent);
    }


    /*
    * This function applies Gaussian envelope to the buffer passed to this function as
    * a parameter. Gaussian envelope is multiplied with buffers samples. 
    * 
    * @param buffer     AudioSampleBuffer with samples ready for application of the envelope.
    * @param tpeak      Time, when the envelope has maximal amplitude (half of the grain length).
    * @param sigma      Value, which defines the width of the bell function (Gaussian).
    */
    void applyEnvelopeToBuffer(juce::AudioBuffer<float>& buffer, double tpeak, double sigma)
    {
        for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
        {
            float* channelData = buffer.getWritePointer(channel);
            for (double sample = 0; sample < buffer.getNumSamples(); ++sample)
            {
                double t = sample / 48000;
                double envelope = gaussianEnvelope(t, tpeak, sigma);
                channelData[(int)sample] *= envelope;
            }
        }
    }

    /*
    * This function enables all the objects in GUI in this group of objects.
    */
    void enableIfFileLoaded() {
        delka.setEnabled(true);
        obalka.setEnabled(true);
        pocet.setEnabled(true);
        offset.setEnabled(true);
        hlasitost.setEnabled(true);
        kanal1.setEnabled(true);
        kanal2.setEnabled(true);
        kanal3.setEnabled(true);
        kanal4.setEnabled(true);
        kanal5.setEnabled(true);
        kanal6.setEnabled(true);
        kanal7.setEnabled(true);
        kanal8.setEnabled(true);
        playGrain.setEnabled(true);
        makeGrain.setEnabled(true);
        resetGrain.setEnabled(true);
    }


    /*
    * GUI calls this function in the begining, when the audio source is not loaded.
    */
    void disableIfNoFileLoaded() {
        delka.setEnabled(false);
        obalka.setEnabled(false);
        pocet.setEnabled(false);
        offset.setEnabled(false);
        hlasitost.setEnabled(false);
        kanal1.setEnabled(false);
        kanal2.setEnabled(false);
        kanal3.setEnabled(false);
        kanal4.setEnabled(false);
        kanal5.setEnabled(false);
        kanal6.setEnabled(false);
        kanal7.setEnabled(false);
        kanal8.setEnabled(false);
        playGrain.setEnabled(false);
        makeGrain.setEnabled(false);
        resetGrain.setEnabled(false);
    }


    /*
    * GUI calls this function if the audio source file is not supported.
    */
    void disableIfFileNotSupported() {
        labelInit();
        disableIfNoFileLoaded();
        play.setEnabled(false);
        stop.setEnabled(false);
    }





    

};
