// Class method used to Start a Real-Time acquisition through Plux::Source configuration:
    // samplingRate -> Desired sampling rate that will be used during the data acquisition stage.
    //                 The used units are in Hz (samples/s)
    // sourcesArray -> List of Sources that define which channels are active and its internal configurations,
    //				   namely the resolution and frequency divisor.
    public void StartAcquisitionBySourcesUnity(int samplingRate, PluxSource[] sourcesArray)
    {
        // Reboot BufferedSamples object.
        BufferAcqSamples bufferedSamples = LazyObject.Value;

        lock (bufferedSamples)
        {
            bufferedSamples.reinitialise();
        }

        if (!bufferedSamples.getUncaughtExceptionState())
        {
            // >>> Garbage collector memory management.
            GCHandle pinnedArray = GCHandle.Alloc(sourcesArray, GCHandleType.Pinned);
            // >>> Convert to a memory address.
            IntPtr ptr = pinnedArray.AddrOfPinnedObject();
            // >>> Call correspondent .dll method to start the real-time acquisition.
            StartAcquisitionBySources(samplingRate, ptr, sourcesArray.Length);
            // >>> Releasing memory.
            pinnedArray.Free();

            // Start Communication Loop.
            StartLoopUnity();
        }
        else
        {
            throw new Exception("Unable to start a real-time acquisition. It is probable that the connection between the computer and the PLUX device was broke");
        }

        // Update global flag.
        AcquisitionStopped = false;
    }
    // Auxiliary function that manages the scanning process.
    // domains -> Array of strings that defines which domains will be used while searching for PLUX devices
    //            [Valid Options: "BTH" -> classic Bluetooth; "BLE" -> Bluetooth Low Energy; "USB" -> Through USB connection cable]
    private void ScanPluxDevs(List <string> domains)
    {
        try
        {
            // Search for BLE and BTH devices.
            List <string> listDevices  = new List <string>();
            List <String> devicesFound = PluxDevsFound.Value;

            // Clear previous content of the device list.
            devicesFound.Clear();
            for (int domainNbr = 0; domainNbr < domains.Count; domainNbr++)
            {
                // List of available Devices.
                GetDetectableDevices(domains[domainNbr]);
            }

            // Send data (list of devices found) to the MAIN THREAD.
            UnityThreadHelper.Dispatcher.Dispatch(() => ScanResultsCallback(devicesFound));
        }
        catch (ExecutionEngineException exc)
        {
            Debug.Log("Exception found while scanning: \n" + exc.Message + "\n" + exc.StackTrace);
            BufferAcqSamples bufferedSamples = LazyObject.Value;
            lock (bufferedSamples)
            {
                bufferedSamples.actUncaughtException();
            }
        }
    }
    /**
     * Auxiliary method used to reboot the buffer responsible for storing the packages of collected data.
     */
    public void RebootDataBuffer()
    {
        // Clear the buffer containing the packages of collected data.
        // Lock is an essential step to ensure that variables shared by the same thread will not be accessed at the same time.
        BufferAcqSamples bufferedSamples = LazyObject.Value;

        lock (bufferedSamples)
        {
            bufferedSamples.reboot();
        }
    }
    // Callback Handler (function invoked during signal acquisition, being essential to ensure the
    // communication between our C++ API and the Unity project.
    bool CallbackHandler(int nSeq, int[] data, int dataLength)
    {
        // Lock is an essential step to ensure that variables shared by the same thread will not be accessed at the same time.
        BufferAcqSamples bufferedSamples = LazyObject.Value;

        lock (bufferedSamples)
        {
            // Storage of the received data samples.
            // Samples are organized in a sequential way, so if channels 1 and 4 are active it means that
            // data[0] will contain the sample value of channel 1 while data[1] is the sample collected on channel 4.
            bufferedSamples.addSamples(nSeq, data);
        }
        return(true);
    }
    // Method used to check if any unhandled exception was raised until the moment.
    public bool IsExceptionInBuffer()
    {
        // Lock is an essential step to ensure that variables shared by the same thread will not be accessed at the same time.
        BufferAcqSamples bufferedSamples = LazyObject.Value;

        lock (bufferedSamples)
        {
            if (bufferedSamples.getUncaughtExceptionState())
            {
                bufferedSamples.deactUncaughtException();
                throw new ExternalException("An exception with unknown origin was raised, but it is not fatal. It is probable that the device connection was lost...");
            }

            return(false);
        }
    }
    // Getter method used to request a new package of data.
    // channelNbr -> Number of the channel under analysis.
    // activeChannelsMask -> List containing set of active channels.
    // rebootMemory -> When true the stored data inside BufferedSamples object is re-initialized.
    public int[] GetPackageOfData(int channelNbr, List <int> activeChannelsMask, bool rebootMemory)
    {
        // Lock is an essential step to ensure that variables shared by the same thread will not be accessed at the same time.
        BufferAcqSamples bufferedSamples = LazyObject.Value;

        lock (bufferedSamples)
        {
            if (!bufferedSamples.getUncaughtExceptionState())
            {
                // Identification of the current channel index.
                int auxCounter = activeChannelsMask.IndexOf(channelNbr);

                // Initialisation of local variables.
                int[][] fullData = bufferedSamples.getPackages(rebootMemory);

                // Selection of data inside the desired channel.
                if (fullData != null && fullData[0] != null)
                {
                    int[] dataInChannel = new int[fullData.Length];
                    for (int i = 0; i < dataInChannel.Length; i++)
                    {
                        try
                        {
                            dataInChannel[i] = fullData[i][auxCounter];
                        }
                        catch (Exception exc)
                        {
                            bufferedSamples.reboot();
                            DoubleCheckLock = null;
                            return(new int[0]);
                        }
                    }

                    return(dataInChannel);
                }

                return(new int[0]);
            }
            else
            {
                bufferedSamples.deactUncaughtException();
                throw new ExternalException("An exception with unknown origin was raised, but it is not fatal. It is probable that the device connection was lost...");
            }
        }
    }
    // Getter method used to request a new package of data.
    // rebootMemory -> When true the stored data inside BufferedSamples object is re-initialized.
    public int[][] GetPackageOfData(bool rebootMemory)
    {
        // Lock is an essential step to ensure that variables shared by the same thread will not be accessed at the same time.
        BufferAcqSamples bufferedSamples = LazyObject.Value;

        lock (bufferedSamples)
        {
            if (!bufferedSamples.getUncaughtExceptionState())
            {
                return(bufferedSamples.getPackages(rebootMemory));
            }
            else
            {
                bufferedSamples.deactUncaughtException();
                throw new ExternalException("An exception with unknown origin was raised, but it is not fatal. It is probable that the device connection was lost...");
            }
        }
    }
    // Class method used to Start a Real-Time acquisition:
    // samplingRate -> Desired sampling rate that will be used during the data acquisition stage.
    //                 The used units are in Hz (samples/s)
    // listChannels -> A list where there are specified the active channels. Each entry contains a port number of an active channel.
    // resolution -> Analog-to-Digital Converter (ADC) resolution. This parameter defines how precise are the digital sampled values when
    //               compared with the ideal real case scenario.
    // callbackFunction -> Pointer to the callback function that will be used to send/communicate the data acquired by PLUX devices, i.e., this callback
    //                     function will be invoked during the data acquisition process and through his inputs the acquired data will become accessible.
    //                     So, for receiving data on a third-party application it will only be necessary to redefine this callbackFunction.
    public void StartAcquisitionUnity(int samplingRate, List <int> listChannels, int resolution)
    {
        // Conversion of List of active channels to a string format.
        for (int i = 0; i < 11; i++)
        {
            if (listChannels.Contains(i + 1))
            {
                ActiveChannelsStr += "1";
            }
            else
            {
                ActiveChannelsStr += "0";
            }
        }

        // Reboot BufferedSamples object.
        BufferAcqSamples bufferedSamples = LazyObject.Value;

        lock (bufferedSamples)
        {
            bufferedSamples.reinitialise();
        }

        if (!bufferedSamples.getUncaughtExceptionState())
        {
            // Start of acquisition.
            StartAcquisition(samplingRate, ActiveChannelsStr, resolution);

            // Start Communication Loop.
            StartLoopUnity();
        }
        else
        {
            throw new Exception("Unable to start a real-time acquisition. It is probable that the connection between the computer and the PLUX device was broke");
        }

        // Update global flag.
        AcquisitionStopped = false;
    }
    // Factory for our Multi-Thread lazy object.
    static BufferAcqSamples InitBufferedSamplesObject()
    {
        BufferAcqSamples lazyComponent = new BufferAcqSamples();

        return(lazyComponent);
    }
    // Callback function responsible for receiving the acquired data samples from the communication loop started by StartLoopUnity().
    private bool DllCommunicationHandler(int exceptionCode, string exceptionDescription, int nSeq, IntPtr data, int dataInSize)
    {
        lock (callbackPointer)
        {
            try
            {
                // DEBUG
                //Console.WriteLine("Monitoring: " + nSeq + "|" + data + "|" + dataInSize);

                // Check if no exception were raised.
                if (exceptionCode == 0)
                {
                    try
                    {
                        // Convert our data pointer to an array format.
                        int[] dataArray = new int[dataInSize];
                        Marshal.Copy(data, dataArray, 0, dataInSize);

                        callbackPointer.GetCallbackRef()(nSeq, dataArray, dataInSize);
                    }
                    catch (OutOfMemoryException exception)
                    {
                        BufferAcqSamples bufferedSamples = LazyObject.Value;
                        lock (bufferedSamples)
                        {
                            bufferedSamples.actUncaughtException();
                            Debug.Log("Executing preventive approaches to deal with a potential OutOfMemoryException:\n" + exception.StackTrace);
                        }
                    }
                }
                else if (exceptionCode == 777) // Receiving devices found during scan.
                {
                    // Store list of found devices in a global variable shared between threads.
                    Debug.Log("Receiving Device: " + exceptionDescription);
                    List <String> devicesFound = PluxDevsFound.Value;
                    devicesFound.Add(exceptionDescription);
                }
                else
                {
                    Debug.Log("A new C++ exception was found...");
                    // Check if the current exception could be an uncaught one.
                    BufferAcqSamples bufferedSamples = LazyObject.Value;
                    lock (bufferedSamples)
                    {
                        // Activate flag in BufferedSamples object stating that an uncaught exception exists.
                        bufferedSamples.actUncaughtException();
                    }

                    throw new Exception(exceptionCode.ToString() + " | " + exceptionDescription);
                }
            }
            catch (OutOfMemoryException exception)
            {
                BufferAcqSamples bufferedSamples = LazyObject.Value;
                lock (bufferedSamples)
                {
                    bufferedSamples.actUncaughtException();
                    Debug.Log("Executing preventive approaches to deal with a potential OutOfMemoryException:\n" + exception.StackTrace);
                }
            }

            return(true);
        }
    }