///
/// Nanotec Nanolib example
/// Copyright (C) Nanotec GmbH & Co. KG - All Rights Reserved
///
/// This product includes software developed by the
/// Nanotec GmbH & Co. KG (http://www.nanotec.com/).
///
/// The Nanolib interface headers and the examples source code provided are 
/// licensed under the Creative Commons Attribution 4.0 Internaltional License. 
/// To view a copy of this license, 
/// visit https://creativecommons.org/licenses/by/4.0/ or send a letter to 
/// Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
///
/// The parts of the library provided in binary format are licensed under 
/// the Creative Commons Attribution-NoDerivatives 4.0 International License. 
/// To view a copy of this license, 
/// visit http://creativecommons.org/licenses/by-nd/4.0/ or send a letter to 
/// Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
///
/// This program is distributed in the hope that it will be useful,
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
///
/// <file_name>SamplerExampleClass.cs</file_name>
///
/// <summary>Definition of sampler example class</summary>
///
/// <date>29-10-2024</date>
///
/// <author>Michael Milbradt<author>
///

using MenuUtils;
using Nlc;
using System.Text;

namespace SamplerExample
{
    internal delegate void ProcessSampledData(SampleDataVector sampleDatas);

    /// <summary>
    /// SamplerNotifyCallback class derived from Nlc.SamplerNotify.
    /// </summary>
    internal class SamplerNotifyCallback : SamplerNotify
    {
        private readonly EventWaitHandle waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
        readonly ProcessSampledData processSampleData;

        /// <summary>
        /// Creates sampler notification callback.
        /// </summary>
        /// <param name="processSampleData">Sampled data processing delegate.</param>
        public SamplerNotifyCallback(ProcessSampledData processSampleData) : base()
        {
            this.processSampleData = processSampleData;
        }

        /// <summary>
        /// Callback used for notification on sampler events
        /// </summary>
        /// <param name="lastError">Last sampler error</param>
        /// <param name="samplerState">Current sampler state</param>
        /// <param name="sampleDatas">New sampler data</param>
        /// <param name="applicationData">Application supplied data (see Sampler.start(..))</param>
        public override void notify(Nlc.ResultVoid lastError, SamplerState samplerState, SampleDataVector sampleDatas, long applicationData)
        {
            // Be aware that notifications are executed in the context of separate threads 
            // other than thread that started the sampler.
            // 
            // Be careful when calling Nanolib functionality here, as doing so may cause this method
            // to be called recursively, potentially causing your application to deadlock.
            // 
            // For the same reason, this method should not throw exceptions.

            processSampleData(sampleDatas);

            if ((samplerState != SamplerState.Ready) && (samplerState != SamplerState.Running))
            {
                if (samplerState == SamplerState.Failed)
                {
                    Console.Error.WriteLine("Sampler execution failed with error: {0}", lastError.getError());
                }

                // It's now safe to destroy this notification object
                waitHandle.Set();
            }
        }

        /// <summary>
        /// Wait for the sampler to finish.
        /// </summary>
        public void Wait()
        {
            waitHandle.WaitOne();
        }

        /// <summary>
        /// Wait for the sampler to finish.
        /// </summary>
        /// <param name="timeOut">Timeout value.</param>
        /// <returns>True if sampler has finished, false on timeout.</returns>
        public bool Wait(int timeOut)
        {
            return waitHandle.WaitOne(timeOut);
        }
    }

    /// <summary>
    /// Demonstration sampler class.
    /// </summary>
    public class SamplerExampleClass
    {
        

        /// <summary>
        /// Container for tracked address (name, od index).
        /// </summary>
        private static readonly TrackedAddress[] trackedAddresses = {
            new TrackedAddress("Up time", new OdIndex(0x230F, 0x00)),
            new TrackedAddress("Temperature", new OdIndex(0x4014, 0x03))
        };

        private readonly string[] addressNames = { "Up time", "Temperature" };

        // Static fields for tracking addresses
        private static readonly OdIndex triggerAddress = new OdIndex(0x2400, 0x01);
        private static readonly SamplerTriggerCondition triggerCondition = SamplerTriggerCondition.TC_GREATER;
        private static readonly uint triggerValue = 10;
        private static readonly uint triggerValueInactive = triggerValue;
        private static readonly uint triggerValueActive = triggerValue + 1;
        private static readonly ushort periodMilliseconds = 1000;
        private static readonly ushort numberOfSamples = 5;

        private Context ctx; // Menu context
        private DeviceHandle deviceHandle; // Device handle to use
        private ulong lastIteration; // Last iteration counter
        private ulong sampleNumber; // Sample number
        private bool headerPrinted; // Output flag for 'table header'

        /// <summary>
        /// Constructor of SamplerExampleClass.
        /// </summary>
        /// <param name="menuContext">The context to use.</param>
        /// <param name="connectedDeviceHandle">The device handle to use.</param>
        public SamplerExampleClass(Context menuContext, DeviceHandle connectedDeviceHandle)
        {
            ctx = menuContext;
            deviceHandle = connectedDeviceHandle;
        }

         ~SamplerExampleClass()
        {
            // Destructor logic if needed
        }

        /// <summary>
        /// Execute all defined example functions.
        /// </summary>
        public void Process()
        {
            ProcessExamplesWithoutNotification();
            ProcessExamplesWithNotification();
        }

        /// <summary>
        /// Execute all example functions without notification callback.
        /// </summary>
        public void ProcessExamplesWithoutNotification()
        {
            ProcessSamplerWithoutNotificationNormal();
            ProcessSamplerWithoutNotificationRepetitive();
            ProcessSamplerWithoutNotificationContinuous();
        }

        /// <summary>
        /// Execute example function for normal mode without notification callback.
        /// </summary>
        public void ProcessSamplerWithoutNotificationNormal()
        {
            var sleepTimeMsec = TimeSpan.FromMilliseconds(periodMilliseconds);

            Console.WriteLine("\nSampler without notification in normal mode:");

            Configure(SamplerMode.Normal);
            Start();

            SamplerState samplerState;

            do
            {
                Thread.Sleep(sleepTimeMsec);

                ProcessSampledData(GetSamplerData());
                samplerState = GetSamplerState();

            } while (samplerState == SamplerState.Ready || samplerState == SamplerState.Running);

            // Process any remaining data
            ProcessSampledData(GetSamplerData());

            if (samplerState == SamplerState.Failed)
                HandleSamplerFailed();
        }

        /// <summary>
        /// Execute example function for repetitive mode without notification callback.
        /// </summary>
        public void ProcessSamplerWithoutNotificationRepetitive()
        {
            var sleepTimeMsec = TimeSpan.FromMilliseconds(periodMilliseconds);
            var waitTimeMsec = TimeSpan.FromMilliseconds(50);

            Console.WriteLine("\nSampler without notification in repetitive mode:");

            Configure(SamplerMode.Repetitive);
            Start();

            // Wait for sampler to run
            SamplerState samplerState;
            do
            {
                Thread.Sleep(waitTimeMsec);
                samplerState = GetSamplerState();
            } while (samplerState != SamplerState.Running && samplerState != SamplerState.Failed);

            // Start processing sampled data
            do
            {
                Thread.Sleep(sleepTimeMsec);

                ProcessSampledData(GetSamplerData());

                if (lastIteration >= 4)
                {
                    ctx.NanolibAccessor.getSamplerInterface().stop(deviceHandle);
                }

                samplerState = GetSamplerState();

            } while (samplerState == SamplerState.Ready || samplerState == SamplerState.Running);

            // Process any remaining data
            ProcessSampledData(GetSamplerData());

            if (samplerState == SamplerState.Failed)
                HandleSamplerFailed();
        }

        /// <summary>
        /// Execute example function for continuous mode without notification callback.
        /// </summary>
        public void ProcessSamplerWithoutNotificationContinuous()
        {
            var sleepTimeMsec = TimeSpan.FromMilliseconds(periodMilliseconds);

            Console.WriteLine("\nSampler without notification in continuous mode:");

            Configure(SamplerMode.Continuous);
            Start();

            SamplerState samplerState = SamplerState.Ready;
            const uint maxCycles = 10;
            uint cycles = 0;

            do
            {
                Thread.Sleep(sleepTimeMsec);

                ProcessSampledData(GetSamplerData());

                if (++cycles == maxCycles)
                {
                    ctx.NanolibAccessor.getSamplerInterface().stop(deviceHandle);
                }

                samplerState = GetSamplerState();

            } while (samplerState == SamplerState.Ready || samplerState == SamplerState.Running);

            // Process any remaining data
            ProcessSampledData(GetSamplerData());

            if (samplerState == SamplerState.Failed)
                HandleSamplerFailed();
        }

        /// <summary>
        /// Execute all example functions with notification callback.
        /// </summary>
        public void ProcessExamplesWithNotification()
        {
            ProcessSamplerWithNotificationNormal();
            ProcessSamplerWithNotificationRepetitive();
            ProcessSamplerWithNotificationContinuous();
        }

        /// <summary>
        /// Execute example function for normal mode with notification callback.
        /// </summary>
        public void ProcessSamplerWithNotificationNormal()
        {
            Console.WriteLine("\nSampler with notification in normal mode:");

            Configure(SamplerMode.Normal);

            var callback = new SamplerNotifyCallback(ProcessSampledData);
            
            Start(callback);
            callback.Wait();
        }

        /// <summary>
        /// Execute example function for repetitive mode with notification callback.
        /// </summary>
        public void ProcessSamplerWithNotificationRepetitive()
        {
            var sleepTimeMsec = TimeSpan.FromMilliseconds(periodMilliseconds);
            var waitTimeMsec = TimeSpan.FromMilliseconds(50);

            Console.WriteLine("\nSampler with notification in repetitive mode:");

            Configure(SamplerMode.Repetitive);

            var callback = new SamplerNotifyCallback(ProcessSampledData);
            Start(callback);

            // Wait for sampler to run
            SamplerState samplerState;
            do
            {
                Thread.Sleep(waitTimeMsec);
                samplerState = GetSamplerState();
            } while (samplerState != SamplerState.Running && samplerState != SamplerState.Failed);

            // start processing sampled data
            while (!callback.Wait(1000))
            {
                if (lastIteration >= 4)
                {
                    // In repeat mode the sampler will continue to run until it is stopped or an error occurs
                    ctx.NanolibAccessor.getSamplerInterface().stop(deviceHandle);
                }
            }
        }

        /// <summary>
        /// Execute example function for continuous mode with notification callback.
        /// </summary>
        public void ProcessSamplerWithNotificationContinuous()
        {
            Console.WriteLine("\nSampler with notification in continuous mode:");

            Configure(SamplerMode.Continuous);

            var callback = new SamplerNotifyCallback(ProcessSampledData);
            Start(callback);
            Thread.Sleep(periodMilliseconds * 10);
            ctx.NanolibAccessor.getSamplerInterface().stop(deviceHandle);
        }

        /// <summary>
        /// Function used for sampler configuration.
        /// </summary>
        /// <param name="mode">The mode to use (normal, repetitive, continuous).</param>
        private void Configure(SamplerMode mode)
        {
            var samplerConfiguration = new SamplerConfiguration();

            foreach (var trackedAddress in trackedAddresses)
            {
                samplerConfiguration.trackedAddresses.Add(trackedAddress.getIndex());
            }

            // Setup start trigger
            var samplerTriggerStart = new SamplerTrigger
            {
                condition = triggerCondition,
                address = triggerAddress,
                value = triggerValue
            };

            // Set start trigger
            samplerConfiguration.startTrigger = samplerTriggerStart;
            samplerConfiguration.periodMilliseconds = periodMilliseconds;
            samplerConfiguration.durationMilliseconds = (mode == SamplerMode.Continuous) ? (uint)0 : (uint)4000;
            samplerConfiguration.preTriggerNumberOfSamples = 0;
            samplerConfiguration.mode = mode;
            samplerConfiguration.usingSoftwareImplementation = (mode == SamplerMode.Continuous);

            ctx.NanolibAccessor.getSamplerInterface().configure(deviceHandle, samplerConfiguration);
        }

        /// <summary>
        /// Function to start a sampler.
        /// </summary>
        /// <param name="callback">Callback for notification.</param>
        /// <param name="applicationData">Data from application for device (not used).</param>
        private void Start(SamplerNotifyCallback? callback = null, long applicationData = 0)
        {
            lastIteration = 0;
            sampleNumber = 0;
            headerPrinted = false;

            if (trackedAddresses.Count() < 1 || trackedAddresses.Count() > 12)
            {
                throw new InvalidOperationException("The number of tracked addresses must be between 1 and 12");
            }

            if (trackedAddresses.Count() != addressNames.Length)
            {
                throw new InvalidOperationException("The number of tracked addresses is not equal to the number of address names");
            }

            // Reset the trigger
            ctx.NanolibAccessor.writeNumber(deviceHandle, triggerValueInactive, triggerAddress, 32);

            // Start the sampler
            try
            {
                ctx.NanolibAccessor.getSamplerInterface().start(deviceHandle, callback, applicationData);
            }
            catch
            {
                ctx.NanolibAccessor.getSamplerInterface().stop(deviceHandle);
                throw;
            }
      
            // Activate the trigger
            try
            {
                ctx.NanolibAccessor.writeNumber(deviceHandle, triggerValueActive, triggerAddress, 32);
            }
            catch
            {
                ctx.NanolibAccessor.getSamplerInterface().stop(deviceHandle);
                throw;
            }
        }

        /// <summary>
        /// Get the state of the sampler.
        /// </summary>
        /// <returns>Returns the sampler state.</returns>
        private SamplerState GetSamplerState()
        {
            return ctx.NanolibAccessor.getSamplerInterface().getState(deviceHandle).getResult();
        }

        /// <summary>
        /// Get the sampled data from device buffer.
        /// </summary>
        /// <returns>Returns a list with the sampled data.</returns>
        private SampleDataVector GetSamplerData()
        {
            return ctx.NanolibAccessor.getSamplerInterface().getData(deviceHandle).getResult();
        }

        /// <summary>
        /// Error handling.
        /// </summary>
        /// <param name="lastErrorPtr">Pointer to the last error.</param>
        private void HandleSamplerFailed(ResultVoid lastErrorPtr = null)
        {
            ResultVoid lastError;

            if (lastErrorPtr != null)
            {
                lastError = lastErrorPtr;
            }
            else
            {
                if (GetSamplerState() != SamplerState.Failed)
                    throw new InvalidOperationException("Sampler state is not failed.");

                lastError = ctx.NanolibAccessor.getSamplerInterface().getLastError(deviceHandle);
            }

            if (lastError.hasError())
            {
                Console.WriteLine($"\nSampler execution failed with error: {lastError.getError()}");
            }
        }

        /// <summary>
        /// Process and display the sampled data.
        /// </summary>
        /// <param name="sampleDatas">List with the sampled data.</param>
        private void ProcessSampledData(SampleDataVector sampleDatas)
        {
            foreach (var sampleData in sampleDatas)
            {
                var sampledValues = sampleData.sampledValues;
                var numberOfSampledValues = sampledValues.Count;

                if (numberOfSampledValues % trackedAddresses.Length != 0)
                    throw new InvalidOperationException("Sampled values do not match tracked addresses.");

                if (lastIteration != sampleData.iterationNumber)
                {
                    sampleNumber = 0;
                    lastIteration = sampleData.iterationNumber;
                }

                if (!headerPrinted)
                {
                    const string cszHorzLine = "------------------------------------------------------------";

                    Console.WriteLine(cszHorzLine);
                    Console.Write("{0,-11}{1,-9}", "Iteration", "Sample");
                    foreach (var addressName in addressNames)
                    {
                        Console.Write("{0,-14}{1,-8}", string.Format("[{0}]", addressName), "Time");
                    }
                    Console.WriteLine();
                    Console.WriteLine(cszHorzLine);
                    headerPrinted = true;
                }

                for (int index = 0; index < numberOfSampledValues; index += trackedAddresses.Length)
                {
                    var stream = new StringBuilder();
                    stream.AppendFormat("{0,-10}{1,-10}", lastIteration, sampleNumber);

                    for (int trackedAddressIndex = 0; trackedAddressIndex < trackedAddresses.Length; trackedAddressIndex++)
                    {
                        var sampledValue = sampledValues[index + trackedAddressIndex];

                        stream.AppendFormat("{0,-14}{1,-8}", sampledValue.value, sampledValue.collectTimeMsec);
                    }

                    sampleNumber++;

                    Console.WriteLine(stream.ToString());
                }

                
            }
        }

        /// <summary>
        /// Container class for tracked addresses
        /// </summary>
        public class TrackedAddress
        {
            private string name;
            private OdIndex index;

            public TrackedAddress(string name, OdIndex index)
            {
                this.name = name;
                this.index = index;
            }

            public OdIndex getIndex() { return index; }
            public string getName() { return name; }    
        }

    }
}

