using Emotiv;
using System;
using System.Collections.Generic;
using System.Threading;

namespace MotorSkill
{
    class EEGRawDataFrame
    {
        // member data
        public long timestamp { get; set; }
        public double raw_cq { get; set; } // quality of contacts(1-4 range as valid value), multiplexed as single double;
        public double af3 { get; set; } // uV voltage for AF3 channel;
        public double f7 { get; set; } // uV voltage for F7 channel;
        public double f3 { get; set; } // uV voltage for F3 channel; 
        public double fc5 { get; set; } // uV voltage for FC5 channel;
        public double t7 { get; set; } // uV voltage for T7 channel;
        public double p7 { get; set; } // uV voltage for P7 channel;
        public double o1 { get; set; } // uV voltage for 01 channel;
        public double o2 { get; set; } // uV voltage for 02 channel;
        public double p8 { get; set; } // uV voltage for P8 channel;
        public double t8 { get; set; } // uV voltage for T8 channel;
        public double fc6 { get; set; } // uV voltage for FC6 channel;
        public double f4 { get; set; } // uV voltage for F4 channel;
        public double f8 { get; set; } // uV voltage for F8 channel;
        public double af4 { get; set; } // uV voltage for AF4 channel;
        public double gyrox { get; set; } // giroscope value for Ox axis;
        public double gyroy { get; set; } // giroscope value for Oy axis;

        // constructor
        public EEGRawDataFrame(long timestamp, double raw_cq, double af3, double f7, double f3, double fc5, double t7, double p7, double o1, double o2, double p8, double t8, double fc6, double f4, double f8, double af4, double gyrox, double gyroy)
        {
            this.timestamp = timestamp;
            this.raw_cq = raw_cq;
            this.af3 = af3;
            this.f7 = f7;
            this.f3 = f3;
            this.fc5 = fc5;
            this.t7 = t7;
            this.p7 = p7;
            this.o1 = o1;
            this.o2 = o2;
            this.p8 = p8;
            this.t8 = t8;
            this.fc6 = fc6;
            this.f4 = f4;
            this.f8 = f8;
            this.af4 = af4;
            this.gyrox = gyrox;
            this.gyroy = gyroy;
        }
    }

    class EEGEmoDataFrame
    {
        // member data
        public long timestamp { get; set; }
        public double longTermExcitementScore { get; set; }
        public double shortTermExcitementScore { get; set; }
        public double meditationScore { get; set; }
        public double frustrationScore { get; set; }
        public double boredomScore { get; set; }
        public double valenceScore { get; set; }

        // constructor
        public EEGEmoDataFrame(long timestamp, double longTermExcitementScore, double shortTermExcitementScore, double meditationScore, double frustrationScore, double boredomScore, double valenceScore)
        {
            this.timestamp = timestamp;
            this.longTermExcitementScore = longTermExcitementScore;
            this.shortTermExcitementScore = shortTermExcitementScore;
            this.meditationScore = meditationScore;
            this.frustrationScore = frustrationScore;
            this.boredomScore = boredomScore;
            this.valenceScore = valenceScore;
        }
    }

    /// <summary>
    /// <param>This classe uses the SDK set as reference "DotNetEmotivSDK-vs2010.dll" (version 8.4.0). For best
    /// result should have a local copy.</param>
    /// <remarks>The following DLLs *MUST* be placed in "../bin/debug/" folder, along with the binary file:
    /// "edk.dll", "edk_utils.dll", "glut32.dll".
    /// </remarks>
    /// </summary>
    class EEGDataStream
    {
        // Access to the EDK via the EmoEngine framework. 
        EmoEngine engine;
        // Access to processed EDK values - EmoStates.
        EmoState es;
        // Sets the buffer size for EmoEngine (in seconds).
        int EE_BUFF_SIZE = 1;
        // Sets the sampling interval (for the EmoEngine) in miliseconds.
        int EE_SAMPLE_MS = 100;
        // Used for uniquely identifying a user's headset (more testing required). First user is no. 0.
        int userID = -1;
        // Stores the status of the engine. Initialised with FALSE (EmoEngine unavailable). Set to TRUE if
        // the engine is primed and working.
        bool status = false;

        private int continousNullDataPacks_Counter = 0;
        private int continousInvalidUser_Counter = 0;

        EmoStateUpdatedEventArgs esu;

        // The callback method.
        private Action<EEGRawDataFrame> callbackRaw;
        private Action<EEGEmoDataFrame> callbackEmo;
        private Thread thread;

        /// <summary>
        /// The constructor sends the callback function to the object at instantiation time.
        /// </summary>
        public EEGDataStream(Action<EEGRawDataFrame> callbackRaw, Action<EEGEmoDataFrame> callbackEmo)
        {
            this.callbackRaw = callbackRaw;
            this.callbackEmo = callbackEmo;
        }

        public int GetContinousInvalidUserCounter()
        {
            return continousInvalidUser_Counter;
        }

        public int GetContinousNullDataPacksCounter()
        {
            return continousNullDataPacks_Counter;
        }

        /// <summary>
        /// The THREADJOB method is executed in the Action Thread. EEG data is pooled from this (main loop). 
        /// Extracted data is retrieved via the callback method and processed in the main application thread.
        /// <remarks>WARNING: Max recomended sample interval is 1 second. If the data is not sampled fast 
        /// enough, the buffer will overflow with side effects! The headset generate 128 samples per second
        /// (as default value). NOTE: the observed sampling speed sometimes DECREASES in the equipment- 
        /// hardware lock on headset (it is transitory though...)?</remarks>
        /// </summary>
        private void ThreadJob()
        {
            while (true)
            {
                // Handle Emo events wainting in queue (unloading it!).
                engine.ProcessEvents();

                // If the user has not yet connected abort this timeslotted procedure; proceed only for
                // connected user!
                if ((int)userID != -1)
                {
                    continousInvalidUser_Counter = 0;

                    /// Get raw EEG data. The data is represented as voltages captured from the headset 
                    /// (there is a primary filtration level in hardware). Data is expressed as doubles,
                    /// being values in the uV (micro-Volt range). There is also service data from the headset
                    /// - gyro data, timestamps, markers, sync signals, etc.
                    Dictionary<EdkDll.EE_DataChannel_t, double[]> data = engine.GetData((uint)userID);
                    // If data is null we sampled too fast; skip the time slot and wait for next event.
                    if (data != null)
                    {
                        continousNullDataPacks_Counter = 0;
                        int _bufferSize = data[EdkDll.EE_DataChannel_t.TIMESTAMP].Length;

                        long currSysTimestamp = ExperimentConfig.getMsDateTime();
                        long lastEegTimestamp = (long)(data[EdkDll.EE_DataChannel_t.TIMESTAMP][_bufferSize - 1] * 1000);

                        for (int i = 0; i < _bufferSize; i++)
                        {
                            EEGRawDataFrame eegRawDataFrame = new EEGRawDataFrame(
                                currSysTimestamp - (lastEegTimestamp - (long)(data[EdkDll.EE_DataChannel_t.TIMESTAMP][i] * 1000)),
                                data[EdkDll.EE_DataChannel_t.RAW_CQ][i],
                                data[EdkDll.EE_DataChannel_t.AF3][i],
                                data[EdkDll.EE_DataChannel_t.F7][i],
                                data[EdkDll.EE_DataChannel_t.F3][i],
                                data[EdkDll.EE_DataChannel_t.FC5][i],
                                data[EdkDll.EE_DataChannel_t.T7][i],
                                data[EdkDll.EE_DataChannel_t.P7][i],
                                data[EdkDll.EE_DataChannel_t.O1][i],
                                data[EdkDll.EE_DataChannel_t.O2][i],
                                data[EdkDll.EE_DataChannel_t.P8][i],
                                data[EdkDll.EE_DataChannel_t.T8][i],
                                data[EdkDll.EE_DataChannel_t.FC6][i],
                                data[EdkDll.EE_DataChannel_t.F4][i],
                                data[EdkDll.EE_DataChannel_t.F8][i],
                                data[EdkDll.EE_DataChannel_t.AF4][i],
                                data[EdkDll.EE_DataChannel_t.GYROX][i],
                                data[EdkDll.EE_DataChannel_t.GYROY][i]);
                            callbackRaw(eegRawDataFrame);
                        }
                    }
                    else {
                        continousNullDataPacks_Counter++;
                    }

                    if (esu != null)
                    {
                        es = esu.emoState;
                        EEGEmoDataFrame eegEmoDataFrame = new EEGEmoDataFrame(
                            ExperimentConfig.getMsDateTime(),
                            es.AffectivGetExcitementLongTermScore(),
                            es.AffectivGetExcitementShortTermScore(),
                            es.AffectivGetMeditationScore(),
                            es.AffectivGetFrustrationScore(),
                            es.AffectivGetEngagementBoredomScore(),
                            es.AffectivGetValenceScore());

                        callbackEmo(eegEmoDataFrame);
                    }
                }
                else {
                    continousInvalidUser_Counter++;
                }

                // Sleepy time!
                Thread.Sleep(EE_SAMPLE_MS);
            }
        }

        /// <summary>
        /// <para>This method initializes the EmoEngine data stream. EDK resources will be checked; if the check
        /// fails, an exception will be thrown and the global STATUS variable will be set to FALSE.</para>
        /// </summary>
        public void Open()
        {
            // Trying to initialise the EDK environment. Failure will prevent further uses of EmoEngine.

            if (status == false)
            {
                try
                {
                    // Instantiation of teh EmoEngine.
                    engine = EmoEngine.Instance;
                    engine.UserAdded += new EmoEngine.UserAddedEventHandler(Engine_UserAdded_Event);
                    engine.AffectivEmoStateUpdated += new EmoEngine.AffectivEmoStateUpdatedEventHandler(Engine_AffectivEmoUpdate_Event);

                    // Connecting to the EmoEngine.
                    engine.Connect();

                    // We create a new thread and we pass the THREADJOB method to it. 
                    thread = new Thread(ThreadJob);
                    thread.IsBackground = true;
                    thread.Start();
                }
                catch (Exception e)
                {
                    // Failure mode: inform the user, unset the status and pass the error/exception!
                    Console.WriteLine("EmoEngine not found. Maybe the headset is OFF?");
                    status = false;
                    throw new EEGFailedToOpenException();
                }

                // Set the EE status.
                status = true;
            }
            else {
                // EmoEngine already started!
                // TODO: Change return type to (-1,0,1) MS-specific touple for status encoding!
                throw new EEGAlreadyOpenedException();
            }
        }

        /// <summary>
        /// <para>Allows aquisition of EmoEngine data events. Sets the BUFFER size (default is 1 second).</para>
        /// <para>The headset default sampling rate is 128 Hz (hardare managed)! The advanced operations
        /// (like EmoStates and analysis algorithms) require buffer (not working for instant values).</para>
        /// <remarks>If the headset is not queried fast enough (via EmoEngine events) the buffer can OVERFLOW 
        /// (bad data).</remarks>
        /// </summary>
        /// <param name="sender"> the object that sends the event (should be the EmoEngine);</param>
        /// <param name="e"> the event being intercepted;</param>
        private void Engine_UserAdded_Event(object sender, EmoEngineEventArgs e)
        {
            // Record the user.
            userID = (int)e.userId;

            // Enable data acquisition for current user.
            engine.DataAcquisitionEnable((uint)userID, true);

            // Ask for up to EE_BUFF_SIZE second of buffered data (max value!).
            engine.EE_DataSetBufferSizeInSec(EE_BUFF_SIZE);
        }

        private void Engine_AffectivEmoUpdate_Event(object sender, EmoStateUpdatedEventArgs e)
        {
            esu = e;
        }

        /// <summary>
        /// <para>This method closes the EmoEngine data stream. All EDK resources are released.</para>
        /// <para>The instance status is set to FALSE.</para>
        /// </summary>
        public void Close()
        {
            if (status == true)
            {
                // Disconnecting the EmoEngine. Unsetting the EE status.
                engine.Disconnect();
                status = false;
                thread.Abort();
            }
            else {
                // EmoEngine already stopped!
                // TODO: Change return type to (-1,0,1) MS-specific touple for status encoding!
                /// throw new EEGAlreadyClosedException();
            }
        }
    }
    public class EEGFailedToOpenException : Exception
    {
    }
    public class EEGAlreadyOpenedException : Exception
    {
    }
    public class EEGFailedToCloseException : Exception
    {
    }
    public class EEGAlreadyClosedException : Exception
    {
    }
}
