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

    midiKomunikace.h
    Created: 26 Jan 2023 1:36:42pm
    Author:  Tomáš Pospíšil

   Tento kód byl sestaven na základě tutoriálů, dostupných na adrese:
   https://docs.juce.com/master/tutorial_handling_midi_events.html a
   https://docs.juce.com/master/tutorial_looping_audio_sample_buffer.html

   This code was built based on tutorials, which can be found on address:
   https://docs.juce.com/master/tutorial_handling_midi_events.html and
   https://docs.juce.com/master/tutorial_looping_audio_sample_buffer.html

  ==============================================================================

   Some of the code is part of the JUCE tutorials.
   Copyright (c) 2020 - 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.

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



/*
* This class contains the functions for handeling the MIDI events and reproduction
* itself. The reproduction process starts with MIDI message NoteOn and ends with 
* MIDI message NoteOff. When the key on the keyboard is pressed, the data from 8
* buffers are copied on the channels of output buffer, based on the reproduction
* algorithm chosen by the user.
*/

#include "vizualizace.h"

#pragma once

class midiComunication : public juce::MidiInputCallback,
                         public juce::MidiKeyboardStateListener,
                         public visualization
{
public:

    /*
    * Constructor of this class. It sets the size of buffers and sets the samples
    * to zeroes. 
    */
    midiComunication() : keyboardComponent(keyboardState, juce::MidiKeyboardComponent::horizontalKeyboard){
        buffer1.setSize(1, (121 * 48000));
        buffer2.setSize(1, (121 * 48000));
        buffer3.setSize(1, (121 * 48000));
        buffer4.setSize(1, (121 * 48000));
        buffer5.setSize(1, (121 * 48000));
        buffer6.setSize(1, (121 * 48000));
        buffer7.setSize(1, (121 * 48000));
        buffer8.setSize(1, (121 * 48000));
        
        buffer1.clear();
        buffer2.clear();
        buffer3.clear();
        buffer4.clear();
        buffer5.clear();
        buffer6.clear();
        buffer7.clear();
        buffer8.clear();
    }

    /*
    * Class variables order by function
    */

    //--------------------------MIDI---------------------------
    juce::Label midiInputListLabel;
    juce::ComboBox midiInputList;
    juce::AudioDeviceManager deviceManager;
    int lastInputIndex = 0;
    bool isAddingFromMidiInput = false;
    juce::MidiKeyboardState keyboardState;
    juce::MidiKeyboardComponent keyboardComponent;
    //---------------------------------------------------------

    //-----------------------BUFFERS---------------------------
    juce::AudioSampleBuffer buffer1;
    juce::AudioSampleBuffer buffer2;
    juce::AudioSampleBuffer buffer3;
    juce::AudioSampleBuffer buffer4;
    juce::AudioSampleBuffer buffer5;
    juce::AudioSampleBuffer buffer6;
    juce::AudioSampleBuffer buffer7;
    juce::AudioSampleBuffer buffer8;
    bool grainsPlaying = false;
    //---------------------------------------------------------

    //-------------------ADITIONAL STUFF-----------------------
    int currentChannel = 0;
    bool secDelay = true;

    int pole[8] = { 0,0,0,0,0,0,0,0 };
    int pole2[10] = {0,0,0,0,0,0,0,0,0,0};
    int pole3[18] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };
    //---------------------------------------------------------

    
    /*
    * Destructor of this class. Destructor calls the deleteChannelGrainList function,
    * which deletes all grains from certain channel. This function is calles on all
    * channels of audio system.
    */
    ~midiComunication() {
        deleteChannelGrainList(&ch1.grainList);
        deleteChannelGrainList(&ch2.grainList);
        deleteChannelGrainList(&ch3.grainList);
        deleteChannelGrainList(&ch4.grainList);
        deleteChannelGrainList(&ch5.grainList);
        deleteChannelGrainList(&ch6.grainList);
        deleteChannelGrainList(&ch7.grainList);
        deleteChannelGrainList(&ch8.grainList);
    }


    /*
    * This function creates list of available MIDI devices. When the user selects
    * divice from the list, it is set as an input device.
    */
    void midiInputListInitialize() {
        auto midiInputs = juce::MidiInput::getAvailableDevices();
        juce::StringArray midiInputNames;

        for (juce::MidiDeviceInfo input : midiInputs)
            midiInputNames.add(input.name);

        midiInputList.addItemList(midiInputNames, 1);
        midiInputList.onChange = [this] { setMidiInput(midiInputList.getSelectedItemIndex()); };

        for (juce::MidiDeviceInfo input : midiInputs)
        {
            if (other.isMidiInputDeviceEnabled(input.identifier))
            {
                setMidiInput(midiInputs.indexOf(input));
                break;
            }
        }

    }


    /*
    * This funtion sets the device given by the index in the list of devices as active
    * and prefered one. 
    * 
    * @param index      Index of the available device in the list of devices.
    */
    void setMidiInput(int index) {
        juce::Array<juce::MidiDeviceInfo> list = juce::MidiInput::getAvailableDevices();

        other.removeMidiInputDeviceCallback(list[lastInputIndex].identifier, this);

        juce::MidiDeviceInfo newInput = list[index];

        if (!other.isMidiInputDeviceEnabled(newInput.identifier))
            other.setMidiInputDeviceEnabled(newInput.identifier, true);
        other.addMidiInputDeviceCallback(newInput.identifier, this);
        midiInputList.setSelectedId(index + 1, juce::dontSendNotification);

        lastInputIndex = index;

        isAddingFromMidiInput = true;
    }


    /*
    * This function is called, when the key on prefered keyboard is pressed. If the message
    * send by the device is NoteOn or NoteaOff, then myNoteOn and mtNoteOff functions are 
    * called. Otherwise nothig happens, because function calls return.
    * 
    * @param source     MIDI device, which sends the message.
    * @param message    MIDI message sent by the prefered device set as an input device. 
    */
    void handleIncomingMidiMessage(juce::MidiInput* source, const juce::MidiMessage& message) override {
        if (message.isNoteOn()) {
            myNoteOn();
        }
        else if (message.isNoteOff()) {
            myNoteOff();
        }
        else
            return;
    }


    /*
    * This function mixes all the grains together. It makes the streams of grains and adds them
    * to a buffer passed as a function parameter. 
    * 
    * @param ch     Channel object with the head to the linked list as variable.
    * @param buf    Buffer expected to be filld with streams of grains.
    */
    void addToChannel(Channel ch, juce::AudioSampleBuffer& buf) {
        Grain* grain = (&ch)->grainList;
        if (grain == NULL)
            return;
        do {
            int pocetVsekunde = grain->pocet;
            int startSample = grain->offset * 48000;
            for (int i = 0; i < (121 - grain->offset); i++) {
                for (int j = 1; j <= pocetVsekunde; j++) {
                    for (int ch = 0; ch < 1; ch++) {
                        buf.addFrom(ch, startSample, grain->grinOwnBuffer, ch, 0, grain->grinOwnBuffer.getNumSamples(), (1.0f / 100 * grain->hlasitost));
                    }
                    startSample += grain->grinOwnBuffer.getNumSamples();
                }
            }
            grain = grain->next;
        } while (grain != nullptr);
    }


    /*
    * This function fills the array with parameters called in function
    */
    void fillArray(int arr[],int size, int j, int d, int t, int c, int p, int s, int se, int o, int de, int des,int je,int dv,int tr, int ct,
        int pa, int ses,int sed, int os) {
        arr[0] = j;
        arr[1] = d;
        arr[2] = t;
        arr[3] = c;
        arr[4] = p;
        arr[5] = s;
        arr[6] = se;
        arr[7] = o;
        if (size >= 10) {
            arr[8] = de;
            arr[9] = des;
        }
        if (size == 17) {
            arr[10] = je;
            arr[11] = dv;
            arr[12] = tr;
            arr[13] = ct;
            arr[14] = pa;
            arr[15] = ses;
            arr[16] = sed;
            arr[17] = os;
        }
    }


    /*
    * Function determines which button was toggled and fills the pole array with coresponding 
    * numbers.
    */
    void assignToArray() {
        if (kruh.getToggleState() == true) {
            fillArray(pole, 8, 0, 1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
        }
        else if (hodiny.getToggleState() == true) {
            fillArray(pole, 8, 1, 5, 3, 7, 1, 5, 3, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
        }
        else if (hvezda.getToggleState() == true) {
            fillArray(pole, 8, 0, 3, 6, 1, 4, 7, 2, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
        }
        else if (pulkruhL.getToggleState() == true) {
            fillArray(pole2, 10, 6, 7, 0, 1, 2, 2, 1, 0, 7, 6, 0, 0, 0, 0, 0, 0, 0, 0);
        }
        else if (pulkruhR.getToggleState() == true) {
            fillArray(pole2, 10, 2, 1, 0, 7, 6, 6, 7, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0);
        }
        else if (kruh2.getToggleState() == true) {
            fillArray(pole3, 17, 4, 3, 2, 1, 0, 7, 6, 5, 4, 4, 5, 6, 7, 0, 1, 2, 3, 4);
        }
    }


    /*
    * This function is called when the key on the keyboard is pressed. If the key is pressed
    * and no Grain is created, then the return is called. Otherwise the grains are added on the 
    * channels and the visualisation is started with reproduction. 
    */
    void myNoteOn() {
        if ((ch1.grainList == nullptr && ch2.grainList == nullptr && ch3.grainList == nullptr && ch4.grainList == nullptr
            && ch5.grainList == nullptr && ch6.grainList == nullptr && ch7.grainList == nullptr && ch8.grainList == nullptr) || !sourceSet) {
            return;
        }
        else {
            grainsPlaying = true;
            position = 0;
            assignToArray();

            addToChannel(ch1, buffer1);
            addToChannel(ch2, buffer2);
            addToChannel(ch3, buffer3);
            addToChannel(ch4, buffer4);
            addToChannel(ch5, buffer5);
            addToChannel(ch6, buffer6);
            addToChannel(ch7, buffer7);
            addToChannel(ch8, buffer8);

            if (other.getCurrentAudioDevice()->getActiveOutputChannels().countNumberOfSetBits() == 8)
                showPatternClicked(1000);
            setAudioChannels(0, 8);
        }
    }


    /*
    * If the key is released and there is no Grain object in memory the return is called. When there
    * are Grain objects in the memory all the buffers with grainstreams are cleared. The timer is set
    * to 1 milisecond so the visualization can end fast.
    */
    void myNoteOff() {
        if ((ch1.grainList == nullptr && ch2.grainList == nullptr && ch3.grainList == nullptr && ch4.grainList == nullptr
            && ch5.grainList == nullptr && ch6.grainList == nullptr && ch7.grainList == nullptr && ch8.grainList == nullptr) || !sourceSet) {
            return;
        }
        else {
            grainsPlaying = false;
            currentChannel = 0;

            buffer1.clear();
            buffer2.clear();
            buffer3.clear();
            buffer4.clear();
            buffer5.clear();
            buffer6.clear();
            buffer7.clear();
            buffer8.clear();

            secDelay = true;
            startTimer(1);
        }
    }

    /*
    * When the key is pressed on MidiKeyboardComponent this function is called. Inside this function
    * is called the function myNoteOn. 
    */
    void handleNoteOn(juce::MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override {
        myNoteOn();
    }


    /*
    * When the key is released on MidiKeyboardComponent this function is called. Inside this function
    * is called the function myNoteOff.
    */
    void handleNoteOff(juce::MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override {
        myNoteOff();
    }


    /*
    * This function is called for example by setAudioChannels function. Here are the samples copied to
    * the output buffer. The samples are copied based on the reproduction algorithm. There are multiple 
    * conditions - output buffer is filled with samples if the reproduction is expected or if the user
    * wants to play the grain before creation or if the user only wants to play the file.
    * 
    * @param bufferToFill       Output buffer, where the samples are copied to be reproduced.
    */
    void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) override {
        if (grainsPlaying) {

            if (bufferToFill.buffer->getNumChannels() != 8)
                return;

            int numInputChannels = buffer1.getNumChannels();
            int numOutputChannels = bufferToFill.buffer->getNumChannels();

            int outputSamplesRemaining = bufferToFill.numSamples;
            int outputSamplesOffset = bufferToFill.startSample;

            int channel;

            while (outputSamplesRemaining > 0)
            {
                int bufferSamplesRemaining = buffer1.getNumSamples() - position;
                int samplesThisTime = juce::jmin(outputSamplesRemaining, bufferSamplesRemaining);

                if (secDelay) {
                    outputSamplesRemaining -= samplesThisTime;
                    outputSamplesOffset += samplesThisTime;
                    position += samplesThisTime;
                    if (position == (1 * 48000)) {
                        secDelay = false;
                        position = 0;
                    }
                }
                else {
                    if (kruh.getToggleState() == true || hodiny.getToggleState() == true || hvezda.getToggleState() == true)
                        channel = pole[currentChannel];
                    else if (pulkruhL.getToggleState() == true || pulkruhR.getToggleState() == true)
                        channel = pole2[currentChannel];
                    else if (kruh2.getToggleState() == true)
                        channel = pole3[currentChannel];
                    else
                        return;

                    switch (channel)
                    {
                    case 0:
                        bufferToFill.buffer->copyFrom(channel,outputSamplesOffset,buffer1,0,position,samplesThisTime);
                        break;
                    case 1:
                        bufferToFill.buffer->copyFrom(channel, outputSamplesOffset, buffer2, 0, position, samplesThisTime);
                        break;
                    case 2:
                        bufferToFill.buffer->copyFrom(channel, outputSamplesOffset, buffer3, 0, position, samplesThisTime);
                        break;
                    case 3:
                        bufferToFill.buffer->copyFrom(channel, outputSamplesOffset, buffer4, 0, position, samplesThisTime);
                        break;
                    case 4:
                        bufferToFill.buffer->copyFrom(channel, outputSamplesOffset, buffer5, 0, position, samplesThisTime);
                        break;
                    case 5:
                        bufferToFill.buffer->copyFrom(channel, outputSamplesOffset, buffer6, 0, position, samplesThisTime);
                        break;
                    case 6:
                        bufferToFill.buffer->copyFrom(channel, outputSamplesOffset, buffer7, 0, position, samplesThisTime);
                        break;
                    case 7:
                        bufferToFill.buffer->copyFrom(channel, outputSamplesOffset, buffer8, 0, position, samplesThisTime);
                        break;
                    default:
                        bufferToFill.buffer->copyFrom(channel, outputSamplesOffset, buffer1, 1, position, samplesThisTime);
                        break;
                    }

                    outputSamplesRemaining -= samplesThisTime;
                    outputSamplesOffset += samplesThisTime;
                    position += samplesThisTime;

                    if (position % (1 * 48000) == 0) {
                        if (kruh.getToggleState() == true || hodiny.getToggleState() == true || hvezda.getToggleState() == true) {
                            currentChannel = (currentChannel + 1) % 8;
                            if (currentChannel == 7) {
                                showPatternClicked(1000);
                            }
                        }
                        else if (pulkruhL.getToggleState() == true || pulkruhR.getToggleState() == true) {
                            currentChannel = (currentChannel + 1) % 10;
                            if (currentChannel == 9) {
                                showPatternClicked(1000);
                            }
                        }
                        else if (kruh2.getToggleState() == true) {
                            currentChannel = (currentChannel + 1) % 18;
                            if (currentChannel == 17) {
                                showPatternClicked(1000);
                            }
                        }
                    }
                }

                if (position == buffer1.getNumSamples()) {
                    position -= 48000;
                }
            }
            return;
        }
        if (bufferPlaying == true) {
            auto numInputChannels = grainBuffer.getNumChannels();
            auto numOutputChannels = bufferToFill.buffer->getNumChannels();

            auto outputSamplesRemaining = bufferToFill.numSamples;                                  
            auto outputSamplesOffset = bufferToFill.startSample;

            while (outputSamplesRemaining > 0)
            {

                auto bufferSamplesRemaining = grainBuffer.getNumSamples() - position;                
                auto samplesThisTime = juce::jmin(outputSamplesRemaining, bufferSamplesRemaining); 

                for (auto channel = 0; channel < numOutputChannels; ++channel)
                {
                    bufferToFill.buffer->copyFrom(channel,outputSamplesOffset,grainBuffer,channel%numInputChannels,position,samplesThisTime);                                
                }

                outputSamplesRemaining -= samplesThisTime;                                          
                outputSamplesOffset += samplesThisTime;                                             
                position += samplesThisTime;                                                       

                if (position == grainBuffer.getNumSamples())
                {
                    bufferPlaying = false;
                    break;
                }
            }
            return;
        }
        if (readerSource.get() == nullptr)
        {
            bufferToFill.clearActiveBufferRegion();
            return;
        }
        transportSource.getNextAudioBlock(bufferToFill);
    }

};
