/*
 *****************************************************************************
 *                                                                           *
 *              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;

namespace Impinj
{
    /// <summary>
    /// Class that is used to contain GPI Events and tag read data for
    /// presentation
    /// </summary>
    class ReadsAndTriggers
    {
        // Constants
        const int MAX_QUEUE_SIZE = 4096;

        // Private data members
        Dictionary<string, TagInfo> _tagReads = new Dictionary<string, TagInfo>();

        SizeQueue<GpiTriggerEvent> _triggerEvents = new SizeQueue<GpiTriggerEvent>(MAX_QUEUE_SIZE);
        SizeQueue<double> _timestampOffsets = new SizeQueue<double>(MAX_QUEUE_SIZE, true);

        // Constructor
        public ReadsAndTriggers()
        {
        }

        /// <summary>
        /// Add the data for a GPI Event to the queue 
        /// </summary>
        /// <param name="gpiEvent">
        /// The GPI Trigger Event object to add data for
        /// </param>
        public void AddGpiTriggerEvent(GpiTriggerEvent gpiEvent)
        {
            // Get the current time in seconds since epoch
            double now = secondsSinceEpoch();
            // Determine the offset between now and the GPI event timestamp
            double delta = now - gpiEvent.Timestamp;
            // Add the GPI event and timestamp offset to the requisite queues
            _timestampOffsets.Enqueue(delta);
            _triggerEvents.Enqueue(gpiEvent);
        }

        /// <summary>
        /// Add a tag read entry to the queue
        /// </summary>
        /// <param name="tagRead">
        /// A TagRead object with the details of the current tag read.
        /// </param>
        public void AddTagRead(TagRead tagRead)
        {
            // Get the current time in seconds since epoch
            double now = secondsSinceEpoch();
            // Determine the offset between now and the GPI event timestamp
            double delta = now - tagRead.Timestamp;
            // Add the timestamp offset to the queue
            _timestampOffsets.Enqueue(delta);

            TagInfo tagInfo;
            // Wait for explicit access to the _tagReads Dictionary
            lock (_tagReads)
            {
                // If the tag has already been seen, simply add new read data
                if (true == _tagReads.TryGetValue(tagRead.Tid, out tagInfo))
                {
                    tagInfo.AddRead(tagRead);
                }
                // Otherwise create a entry for this tag and add read data
                else
                {
                    // Create a new TagInfo object for this tag
                    tagInfo = new TagInfo(tagRead.Tid);
                    // Add an entry to the _tagReads dictionary
                    _tagReads.Add(tagRead.Tid, tagInfo);
                    // Update the entry to add the details for this tag read
                    _tagReads[tagRead.Tid].AddRead(tagRead);
                }
            }
        }

        /// <summary>
        /// Get the Tag Read and Gpi Trigger Event data for the specified
        /// number of seconds
        /// </summary>
        /// <param name="numSeconds">
        /// The number of seconds of history data to get plot data for
        /// </param>
        /// <returns>
        /// A DataToPlot object containing tag read and GPI Trigger Event data
        /// </returns>
        public DataToPlot GetDataToPlot(double numSeconds)
        {
            // Define the return data object
            DataToPlot dataToPlot = new DataToPlot();

            // Extract the current average timestamp offset
            double averageTimeDelta = AverageTimestampOffset;

            // Calculate a limit for the plot data window.
            // The average time delta offset is used to ensure that a dataset
            // less than numSeconds long is presented correctly
            double limit = secondsSinceEpoch() - averageTimeDelta - numSeconds;

            // Remove all trigger event data outside the current limits
            while (0 < _triggerEvents.Count && _triggerEvents.Peek().Timestamp < limit)
            {
                // Remove the entries from the queue using the TryDequeue
                // method to prevent application lock-ups
                GpiTriggerEvent dQ;
                _triggerEvents.TryDequeue(out dQ);
            }

            // Create a list of TID references
            List<string> tidsToDelete = new List<string>();
            // Ensure exclusive access to the _tagReads Dictionary
            lock (_tagReads)
            {
                // Iterate through each entry in the _TagRead Dictionary
                foreach (TagInfo tagInfo in _tagReads.Values)
                {
                    // Remove any reads for the current tag that are outside
                    // the current limits
                    tagInfo.EvictTagReadsOlderThan(limit);
                    // If there are no tag reads left for the current tag, add
                    // the tag to the list of TIDs for which the data wil be
                    // deleted
                    if (0 == tagInfo.NumReads)
                    {
                        tidsToDelete.Add(tagInfo.Tid);
                    }
                }

                // Remove any tag data flagged for deletion
                foreach (string tid in tidsToDelete)
                {
                    _tagReads.Remove(tid);
                }
            }

            // Now that we've cleaned everything up, start populating the
            // plot data to return, starting with the GPI trigger events
            foreach (GpiTriggerEvent e in _triggerEvents)
            {
                dataToPlot.GpiEvents.Add(new GpiTriggerEvent
                    {
                        Timestamp = e.Timestamp + averageTimeDelta,
                        GpiState = e.GpiState
                    });
            }

            // Populate the plot data with tag read data
            lock (_tagReads)
            {
                foreach (TagInfo t in _tagReads.Values)
                {
                    dataToPlot.TagInfos.Add(new TagInfo(t, averageTimeDelta));
                }
            }

            // Return the collected plot data
            return dataToPlot;
        }

        /// <summary>
        /// Stops the queue synchronization so that the application does not
        /// lock up
        /// </summary>
        public void Stop()
        {
            _timestampOffsets.Close();
            _triggerEvents.Close();
        }

        /// <summary>
        /// Calculates the seconds since the Epoch
        /// </summary>
        /// <returns>
        /// The number of seconds from the Epoch until the procedure is called
        /// </returns>
        public double secondsSinceEpoch()
        {
            return DateTime.Now.Ticks / 1e7;
        }

        /// <summary>
        /// Accessor that returns the arithmetic mean of the timestampOffset
        /// queue value
        /// </summary>
        protected double AverageTimestampOffset
        {
            get
            {
                if (0 < _timestampOffsets.Count)
                {
                    return _timestampOffsets.Average();
                }
                else
                {
                    return 0;
                }
            }
        }
    }
}
