/*
 *****************************************************************************
 *                                                                           *
 *              IMPINJ CONFIDENTIAL AND PROPRIETARY                          *
 *                                                                           *
 * This source code is the sole property of Impinj, Inc. Reproduction or     *
 * utilization of this source code in whole or in part is forbidden without  *
 * the prior written consent of Impinj, Inc.                                 *
 *                                                                           *
 * (c) Copyright Impinj, Inc. 2015. All rights reserved.                     *
 *                                                                           *
 *****************************************************************************/
using System;
using System.Collections.Generic;
using System.Threading;
using Impinj.STP;
using OxyPlot;
using OxyPlot.Annotations;
using OxyPlot.Axes;
using OxyPlot.Series;

namespace Impinj
{
    // Class for holding the GPI event and Tag inventory data for plotting
    class DataToPlot
    {
        public List<GpiTriggerEvent> GpiEvents = new List<GpiTriggerEvent>();
        public List<TagInfo> TagInfos = new List<TagInfo>();
    };

    // Class that is used to control an ItemEncode device and plot the trigger
    // data to graph created using the OxyPlot graphing library:
    //  http://oxyplot.org/
    //
    public class DeviceController : Observable
    {
        // Private data members
        private Device ItemEncodeVerifyDevice;
        private string SpeedwayRHostname;

        private bool isConnected = false;
        private bool isEngaged = false;
        private bool isOperating = false;

        private const double timeWindowInSeconds = 10;

        // Create a ReadsAndTriggers object to contain the dataset to be graphed
        ReadsAndTriggers _readsAndTriggers = new ReadsAndTriggers();
        System.Windows.Threading.DispatcherTimer _refreshTimer;

        PlotModel _plotModel = new PlotModel();

        // Public data members
        public bool IsConnected
        {
            get { return isConnected; }
        }

        public bool IsEngaged
        {
            get { return isEngaged; }
        }

        public bool IsOperating
        {
            get { return isOperating; }
        }

        public PlotModel Model
        {
            get { return _plotModel; }
            set { _plotModel = value; }
        }

        /// <summary>
        /// Connect to an ItemEncode device
        /// </summary>
        /// <param name="deviceHostname">
        /// Hostname/IP Address of the ItemEncode device to connect to
        /// </param>
        /// <returns>
        /// Firmware version of the ItemEncode device
        /// </returns>
        public string Connect(string deviceHostname)
        {
            // Firmware version string to return
            string returnFirmwareversion = string.Empty;

            // Retain hostname
            SpeedwayRHostname = deviceHostname;

            try
            {
                ItemEncodeVerifyDevice = new Device();

                // Connect to ItemEncode device
                ItemEncodeVerifyDevice.NetConnect(SpeedwayRHostname);

                // Get the OperationData from the ItemEncodeVerify device
                OperationData operationData = ItemEncodeVerifyDevice.GetOperationData();
                returnFirmwareversion = operationData.OperationDetails.FirmwareVersion;

                // Indicate that the connection has been successful
                isConnected = true;

                // Bind handlers to the device events
                ItemEncodeVerifyDevice.DeviceEventReportReceived += OnDeviceEventReport;
                ItemEncodeVerifyDevice.TagOperationReportReceived += OnTagOperationReport;
            }
            catch (Exception ex)
            {
                // An exception has occurred, so inform the user
                string ex_msg = string.Format(
                    "Device connection exception:{0}{1}{2}",
                    Environment.NewLine,
                    ex.Message,
                    (null == ex.InnerException) ? string.Empty : Environment.NewLine + ex.InnerException.Message
                    );

                // Disconnect from the ItemEncodeVerify device
                if (true == isConnected)
                {
                    ItemEncodeVerifyDevice.NetDisconnect();
                }

                throw (new Exception(ex_msg));
            }

            // Return the firmware version
            return returnFirmwareversion;
        }

        /// <summary>
        /// Disconnect from ItemEncode device
        /// </summary>
        public virtual void Disconnect()
        {
            // Continue only if a connection already exists and a reference to
            // an ItemEncode control device still exists
            if ((true == isConnected) &&
                (null != ItemEncodeVerifyDevice))
            {
                // Just in case, disengage the device
                Disengage();

                // Now terminate the network connection
                ItemEncodeVerifyDevice.NetDisconnect();

                // Remove the event handlers
                ItemEncodeVerifyDevice.TagOperationReportReceived -= OnTagOperationReport;
                ItemEncodeVerifyDevice.DeviceEventReportReceived -= OnDeviceEventReport;

                // Clear out the ItemEncode device handle
                ItemEncodeVerifyDevice = null;
            }

            // Flag that device is now disconnected
            isConnected = false;
        }

        /// <summary>
        /// Uploads the required device configuration to the ItemEncode device
        /// </summary>
        /// <param name="fixedFrequency">
        /// A fixed frequency value to set the ItemEncode device to
        /// </param>
        private void SetDeviceConfig(int fixedFrequency)
        {
            // Create a device configuration object
            DeviceConfig deviceConfig = new DeviceConfig();

            // Determine whether a fixed frequency has been defined
            if (0 != fixedFrequency)
            {
                // Configure the ItemEncode device to use the specified fixed frequency
                deviceConfig.Region = new Region();
                deviceConfig.Region.RegionId = RegulatoryRegion.REGULATORY_REGION_FCC_PART_15_247;
                deviceConfig.Region.FixedFrequencyList.Add((uint)fixedFrequency);
            }

            // Configure the ItemEncode device to operate in continuous inventory mode
            deviceConfig.DeviceMode = DeviceMode.DEVICE_MODE_INVENTORY;
            deviceConfig.OperationMode = OperationMode.OPERATION_MODE_CONTINUOUS;

            // Enable GPI Event reports
            deviceConfig.ReportConfig = new ReportConfig();
            deviceConfig.ReportConfig.DeviceEventConfig = new DeviceEventConfig();
            deviceConfig.ReportConfig.DeviceEventConfig.EnableGpiEvents = true;

            // Enable Tag Operation reports
            deviceConfig.ReportConfig.TagOperationReportConfig = new TagOperationReportConfig();
            deviceConfig.ReportConfig.TagOperationReportConfig.TagOperationReportTrigger =
                TagOperationReportTrigger.TAG_OPERATION_REPORT_TRIGGER_IMMEDIATE;
            deviceConfig.ReportConfig.TagOperationReportConfig.EnableTagOperationReports = true;

            // Upload device configuration to ItemEncode device
            ItemEncodeVerifyDevice.SetDeviceConfig(deviceConfig);
        }

        /// <summary>
        /// Uploads the required antenna configuration to the ItemEncode device
        /// </summary>
        /// <param name="txPower">
        /// The transmit power to use in cdBm (e.g. 2000 cdBm = 20 dBm)
        /// </param>
        /// <param name="rxSensitivity">
        /// The receive sensitivity to use in cdBm
        /// </param>
        private void SetAntennaConfig(int txPower, int rxSensitivity)
        {
            // Create an antenna configurations object
            AntennasConfig antennaConfigs = new AntennasConfig();

            // Create an antenna configuration object for Antenna 1
            AntennaConfig antennaConfig = new AntennaConfig();
            antennaConfig.AntennaId = 1;
            antennaConfig.Gen2Mode = Gen2Mode.GEN2_MODE_MAX_THROUGHPUT;
            antennaConfig.InventorySearchMode = InventorySearchMode.INVENTORY_SEARCH_MODE_DUAL_TARGET;
            antennaConfig.Gen2Session = Gen2Session.GEN2_SESSION_0;
            antennaConfig.TransmitPower = txPower;
            antennaConfig.ReceiveSensitivity = rxSensitivity;
            antennaConfig.TagPopulationEstimate = 1;
            antennaConfig.EnableQueryForQIncrease = false;
            antennaConfig.EnableQueryForQDecrease = false;
            antennaConfig.EnableQueryAtUCycleEnd = false;

            // Add the antenna configuration for Antenna 1 to the antenna
            // configurations list
            antennaConfigs.AntennaConfig.Add(antennaConfig);

            // Upload the antenna configurations to the ItemEncode device
            ItemEncodeVerifyDevice.SetAntennaConfig(antennaConfigs);
        }

        /// <summary>
        /// Put the ItemEncode device into the Engaged state.
        /// </summary>
        /// <remarks>
        /// The ItemEncode device makes the transition from the Configure state
        /// to the Engaged state.
        /// </remarks>
        private void Engage()
        {
            // Only request this transition if the reader is in the
            // Configuration state
            if (false == isEngaged)
            {
                // Request the ItemEncode device to transition to the Engaged
                // state
                ItemEncodeVerifyDevice.Engage();
                isEngaged = true;
            }
        }

        /// <summary>
        /// Put the ItemEncode device into the Disengaged state.
        /// <remarks>
        /// The ItemEncode device makes the transition from the Engaged state
        /// to the Configure state
        /// </remarks>
        /// </summary>
        private void Disengage()
        {
            // Only request this transition if the reader is in the
            // Engaged state
            if (true == isEngaged)
            {
                // Request the ItemEncode device to transition to the Configure
                // state
                ItemEncodeVerifyDevice.Disengage();
                isEngaged = false;
            }
        }

        /// <summary>
        /// Start the ItemEncode device with a device configuration based 
        /// on the provided parameters
        /// </summary>
        /// The transmit power to use in cdBm (e.g. 2000 cdBm = 20 dBm)
        /// <param name="txPower">
        /// </param>
        /// <param name="rxSensitivity">
        /// The receive sensitivity to use in cdBm
        /// </param>
        /// <param name="fixedFrequency">
        /// The fixed frequency to use
        /// </param>
        public void Start(int txPower, int rxSensitivity, int fixedFrequency)
        {
            // Upload a device configuration to the ItemEncode device
            SetDeviceConfig(fixedFrequency);
            // Upload an antenna configuration to the ItemEncode device
            SetAntennaConfig(txPower, rxSensitivity);
            // Engage the ItemEncode device
            Engage();
            // Wait briefly for the ItemEncode device transition to the
            // Engaged state to take effect
            Thread.Sleep(100);
            // If the reader successfully Engages, then start the ItemEncode
            // device
            if ((true == isEngaged) &&
                (false == isOperating))
            {
                // Start the ItemEncode device, triggering the device to
                // transition to the Operating state
                ItemEncodeVerifyDevice.Start();
                isOperating = true;
            }
            // Initialize the OxyPlot graph
            InitializePlot();
            // Configure and start the refresh timer used to update the OxyPlot
            // graph
            _refreshTimer = new System.Windows.Threading.DispatcherTimer();
            _refreshTimer.Interval = new TimeSpan((long)0.01e7); // 10 milliseconds
            _refreshTimer.Tick += new EventHandler(RefreshTimer_Tick);
            _refreshTimer.Start();
        }

        /// <summary>
        /// Stop the ItemEncode device and the refresh timer used to update the
        /// OxyPlot graph
        /// </summary>
        public void Stop()
        {
            // Stop the refresh timer
            _refreshTimer.Stop();
            // Stop the ItemEncode device, prompting it to transition from the
            // Operating state to the Engaged state.
            if (isOperating)
            {
                ItemEncodeVerifyDevice.Stop();
                _readsAndTriggers.Stop();
                isOperating = false;
            }

            // Disengage the ItemEncode device, prompting it to transition from
            // the Engaged state to the Configure state
            Disengage();
        }

        /// <summary>
        /// Extract the ItemEncode device firmware version
        /// </summary>
        /// <returns>
        /// A string containing the ItemEncode firmware version
        /// </returns>
        public string GetFirmwareVersion()
        {
            // Extract the OperationData from the device
            Impinj.STP.OperationData operationData =
                ItemEncodeVerifyDevice.GetOperationData();

            // Return the firmware version
            return operationData.OperationDetails.FirmwareVersion;
        }

        /// <summary>
        /// On device report event handler
        /// </summary>
        /// <param name="report">
        /// The Device Event report from the ItemEncode device
        /// </param>
        private void OnDeviceEventReport(DeviceEventReport report)
        {
            // Only process GPI events
            if (null != report.GpiEvent)
            {
                // Pass the GPI event information to the dataset being graphed
                _readsAndTriggers.AddGpiTriggerEvent(new GpiTriggerEvent
                    {
                        Timestamp = (double)(report.GpiEvent.Timestamp.UsecondsSinceEpoch) / 1e6,
                        GpiState = (uint)report.GpiEvent.GpiState
                    });
            }
        }

        /// <summary>
        /// On Tag Operation report event handler
        /// </summary>
        /// <param name="report">
        /// The Tag Operation report from the ItemEncode device
        /// </param>
        private void OnTagOperationReport(TagOperationReport report)
        {
            // Only process Inventory reports
            switch (report.TagOperationReportType)
            {
                case TagOperationReportType.TAG_OPERATION_REPORT_TYPE_INVENTORY:
                    // Pass the tag operation report information to the dataset
                    // being graphed
                    _readsAndTriggers.AddTagRead(
                        new TagRead
                        {
                            Tid = report.Tid.ToHexString(),
                            Timestamp = (double)(report.Timestamp.UsecondsSinceEpoch) / 1e6,
                            RSSI = ((double)report.Rssi / 100)
                        });
                    break;
                default:
                    break;
            }
        }

        /// <summary>
        /// Handler for the refresh timer elapsed event. This presents any new
        /// data to the OxyPlot graph
        /// </summary>
        private void RefreshTimer_Tick(object sender, EventArgs e)
        {
            // Get a current timestamp
            double now = _readsAndTriggers.secondsSinceEpoch();
            // Get the data to plot from the dataset being graphed
            DataToPlot dataToPlot = _readsAndTriggers.GetDataToPlot(timeWindowInSeconds);

            // Clear the current plot data
            _plotModel.Series.Clear();
            _plotModel.Annotations.Clear();

            // Plot the tag operation data
            foreach (TagInfo tagInfo in dataToPlot.TagInfos)
            {
                // Create a line to plot the current tag information
                LineSeries lineSeries = new LineSeries();
                lineSeries.CanTrackerInterpolatePoints = false;

                lineSeries.MarkerType = MarkerType.Plus;
                lineSeries.MarkerSize = 0;
                lineSeries.MarkerStroke = tagInfo.Color;
                lineSeries.Smooth = false;

                // Populate the line with tag read data 
                foreach (TagRead tagRead in tagInfo.Reads)
                {
                    lineSeries.Points.Add(new DataPoint(tagRead.Timestamp - now, tagRead.RSSI));
                }

                // Add the line data to the graph
                _plotModel.Series.Add(lineSeries);
            }

            // Plot the GPI Event data. If no Tag data exists, then we need to
            // create a line by placing a single dot at 0,0 on the OxyPlot
            // graph, otherwise the graph annotations for GPI events do not
            // appear
            if (0 == dataToPlot.TagInfos.Count)
            {
                // Plot a single white "dummy" dot at 0,0
                LineSeries lineSeries = new LineSeries()
                {
                    MarkerStroke = OxyColor.FromRgb(255, 255, 255)
                };
                lineSeries.Points.Add(new DataPoint(0, 0));

                // Add the line data to the graph
                _plotModel.Series.Add(lineSeries);
            }

            foreach (GpiTriggerEvent gpiEvent in dataToPlot.GpiEvents)
            {
                LineAnnotation lineAnnotation = new LineAnnotation();
                lineAnnotation.Type = LineAnnotationType.Vertical;
                lineAnnotation.MaximumY = -100;
                lineAnnotation.MinimumY = 0;
                lineAnnotation.Color = OxyColors.Black;
                lineAnnotation.StrokeThickness = 2;
                lineAnnotation.X = gpiEvent.Timestamp - now;
                lineAnnotation.Text = gpiEvent.GpiState.ToString();
                lineAnnotation.TextMargin = 16;
                lineAnnotation.TextPadding = 3;
                lineAnnotation.FontWeight = FontWeights.Normal;
                lineAnnotation.TextOrientation = AnnotationTextOrientation.Horizontal;
                lineAnnotation.TextHorizontalAlignment = HorizontalAlignment.Left;

                _plotModel.Annotations.Add(lineAnnotation);
            }

            _plotModel.DefaultXAxis.Zoom(-timeWindowInSeconds, 0);
            _plotModel.DefaultYAxis.Zoom(-100, 0);
            _plotModel.InvalidatePlot(true);
        }

        /// <summary>
        /// Initialize the OxyPlot model
        /// </summary>
        private void InitializePlot()
        {
            // Create an X Axis
            var xAxis = _plotModel.DefaultXAxis;
            xAxis.Position = AxisPosition.Bottom;
            xAxis.Title = "Time";
            xAxis.Unit = "Seconds";

            // Create a Y Axis
            var yAxis = _plotModel.DefaultYAxis;
            yAxis.Position = AxisPosition.Left;
            yAxis.Title = "Tag RSSI";
            yAxis.Unit = "dBm";

            _plotModel.InvalidatePlot(true);
        }
    }
}
