Represents an openHistorian connection.
Inheritance: IDisposable
        /// <summary>
        /// Read historian data from server.
        /// </summary>
        /// <param name="connection">openHistorian connection.</param>
        /// <param name="startTime">Start time of query.</param>
        /// <param name="stopTime">Stop time of query.</param>
        /// <param name="measurementID">Measurement ID to test for data continuity.</param>
        /// <param name="resolution">Resolution for testing data.</param>
        /// <param name="expectedFullResolutionTicks">Expected number of ticks per interval at full resolution, e.g., 33,333 = 1/30 of a second representing a sampling interval of 30 times per second.</param>
        /// <returns>Enumeration of valid data ranges for specified time range.</returns>
        /// <remarks>
        /// 1 tick = 100 nanoseconds.
        /// </remarks>
        public static IEnumerable<Tuple<DateTime, DateTime>> GetContiguousDataRegions(Connection connection, DateTime startTime, DateTime stopTime, ulong measurementID, Resolution resolution, long expectedFullResolutionTicks = 333333)
        {
            // Setup time-range and point ID selections
            SeekFilterBase<HistorianKey> timeFilter;
            MatchFilterBase<HistorianKey, HistorianValue> pointFilter = PointIdMatchFilter.CreateFromPointID<HistorianKey, HistorianValue>(measurementID);
            HistorianKey key = new HistorianKey();
            HistorianValue value = new HistorianValue();
            TimeSpan interval, tolerance;

            // Set data scan resolution
            if (resolution == Resolution.Full)
            {
                interval = new TimeSpan(expectedFullResolutionTicks);
                timeFilter = TimestampSeekFilter.CreateFromRange<HistorianKey>(startTime, stopTime);
            }
            else
            {
                interval = resolution.GetInterval();
                timeFilter = TimestampSeekFilter.CreateFromIntervalData<HistorianKey>(startTime, stopTime, interval, new TimeSpan(TimeSpan.TicksPerMillisecond));
            }

            // PMUs times may float a little - provide a one millisecond tolerance window above the standard interval
            tolerance = interval.Add(TimeSpan.FromMilliseconds(1.0D));

            DateTime lastStartTime = startTime;
            DateTime lastStopTime = startTime;
            DateTime nextExpectedTime = startTime;
            DateTime currentTime;
            long totalRegions = 0;

            // Start stream reader for the provided time window and selected points
            using (Database database = connection.OpenDatabase())
            {
                TreeStream<HistorianKey, HistorianValue> stream = database.Read(SortedTreeEngineReaderOptions.Default, timeFilter, pointFilter);

                // Scan historian stream for given point over specified time range and data resolution
                while (stream.Read(key, value))
                {
                    currentTime = key.TimestampAsDate;

                    // See if current time was not expected time and gap is larger than resolution tolerance - could simply
                    // be user started with a time that was not aligned with desired resolution, hence the tolerance check
                    if (currentTime != nextExpectedTime && currentTime - nextExpectedTime > tolerance)
                    {
                        if (lastStartTime != lastStopTime)
                        {
                            // Detected a data gap, return last contiguous region
                            totalRegions++;
                            yield return new Tuple<DateTime, DateTime>(lastStartTime, lastStopTime);
                        }

                        // Move start time to current value
                        lastStartTime = currentTime;
                        lastStopTime = lastStartTime;
                        nextExpectedTime = lastStartTime + interval;
                    }
                    else
                    {
                        // Setup next expected timestamp
                        nextExpectedTime += interval;
                        lastStopTime = currentTime;
                    }
                }

                // If no data gaps were detected, return a single value for full region for where there was data
                if (totalRegions == 0 && lastStartTime != lastStopTime)
                    yield return new Tuple<DateTime, DateTime>(lastStartTime, lastStopTime);
            }
        }
        /// <summary>
        /// Read historian data from server.
        /// </summary>
        /// <param name="connection">openHistorian connection.</param>
        /// <param name="startTime">Start time of query.</param>
        /// <param name="stopTime">Stop time of query.</param>
        /// <param name="measurementIDs">Array of measurement IDs to query - or <c>null</c> for all available points.</param>
        /// <param name="resolution">Resolution for data query.</param>
        /// <returns>Enumeration of <see cref="IMeasurement"/> values read for time range.</returns>
        /// <example>
        /// <code>
        /// using (var connection = new Connection("127.0.0.1", "PPA"))
        ///     foreach(var measurement in GetHistorianData(connection, DateTime.UtcNow.AddMinutes(-1.0D), DateTime.UtcNow))
        ///         Console.WriteLine("{0}:{1} @ {2} = {3}, quality: {4}", measurement.Key.Source, measurement.Key.ID, measurement.Timestamp, measurement.Value, measurement.StateFlags);
        /// </code>
        /// </example>
        public static IEnumerable<IMeasurement> GetHistorianData(Connection connection, DateTime startTime, DateTime stopTime, IEnumerable<ulong> measurementIDs = null, Resolution resolution = Resolution.Full)
        {
            SeekFilterBase<HistorianKey> timeFilter;
            MatchFilterBase<HistorianKey, HistorianValue> pointFilter = null;
            HistorianKey key = new HistorianKey();
            HistorianValue value = new HistorianValue();

            // Set data scan resolution
            if (resolution == Resolution.Full)
            {
                timeFilter = TimestampSeekFilter.CreateFromRange<HistorianKey>(startTime, stopTime);                
            }
            else
            {
                TimeSpan resolutionInterval = resolution.GetInterval();
                BaselineTimeInterval interval = BaselineTimeInterval.Second;

                if (resolutionInterval.Ticks < Ticks.PerMinute)
                    interval = BaselineTimeInterval.Second;
                else if (resolutionInterval.Ticks < Ticks.PerHour)
                    interval = BaselineTimeInterval.Minute;
                else if (resolutionInterval.Ticks == Ticks.PerHour)
                    interval = BaselineTimeInterval.Hour;

                startTime = startTime.BaselinedTimestamp(interval);
                stopTime = stopTime.BaselinedTimestamp(interval);

                timeFilter = TimestampSeekFilter.CreateFromIntervalData<HistorianKey>(startTime, stopTime, resolutionInterval, new TimeSpan(TimeSpan.TicksPerMillisecond));
            }

            // Setup point ID selections
            if ((object)measurementIDs != null)
                pointFilter = PointIdMatchFilter.CreateFromList<HistorianKey, HistorianValue>(measurementIDs);

            // Start stream reader for the provided time window and selected points
            using (Database database = connection.OpenDatabase())
            {
                TreeStream<HistorianKey, HistorianValue> stream = database.Read(SortedTreeEngineReaderOptions.Default, timeFilter, pointFilter);

                while (stream.Read(key, value))
                    yield return new Measurement()
                    {
                        Metadata = MeasurementKey.LookUpOrCreate(connection.InstanceName, (uint)key.PointID).Metadata,
                        Timestamp = key.TimestampAsDate,
                        Value = value.AsSingle,
                        StateFlags = (MeasurementStateFlags)value.Value3
                    };
            }
        }
        ///// <summary>
        ///// Establish measurement key cache based on received meta-data.
        ///// </summary>
        ///// <param name="metadata">Meta-data received from a subscription.</param>
        ///// <param name="instanceName">Instance name of the historian.</param>
        ///// <remarks>
        ///// If you want the <see cref="IMeasurement.Key"/> values that are returned from a <see cref="GetHistorianData"/> query
        ///// to have properly assigned Guid based signal IDs, i.e., <see cref="MeasurementKey.SignalID"/>, establish the
        ///// measurement key cache as soon as you have received meta-data from a GEP subscription query.
        ///// </remarks>
        //public static void EstablishMeasurementKeyCache(DataSet metadata, string instanceName)
        //{
        //    // Check to see if data for the "MeasurementDetail" table was included in the meta-data
        //    if (metadata.Tables.Contains("MeasurementDetail"))
        //    {
        //        // Establish default measurement key cache
        //        foreach (DataRow row in metadata.Tables["MeasurementDetail"].Rows)
        //            MeasurementKey.CreateOrUpdate(Guid.Parse(row.Field<object>("SignalID").ToString()), instanceName, row.Field<uint>("PointID"));
        //    }
        //}

        /// <summary>
        /// Read historian data from server.
        /// </summary>
        /// <param name="connection">openHistorian connection.</param>
        /// <param name="startTime">Start time of query.</param>
        /// <param name="stopTime">Stop time of query.</param>
        /// <param name="measurementIDs">Comma separated list of measurement IDs to query - or <c>null</c> for all available points.</param>
        /// <param name="resolution">Resolution for data query.</param>
        /// <returns>Enumeration of <see cref="IMeasurement"/> values read for time range.</returns>
        /// <example>
        /// <code>
        /// using (var connection = new Connection("127.0.0.1", "PPA"))
        ///     foreach(var measurement in GetHistorianData(connection, DateTime.UtcNow.AddMinutes(-1.0D), DateTime.UtcNow))
        ///         Console.WriteLine("{0}:{1} @ {2} = {3}, quality: {4}", measurement.Key.Source, measurement.Key.ID, measurement.Timestamp, measurement.Value, measurement.StateFlags);
        /// </code>
        /// </example>
        public static IEnumerable<IMeasurement> GetHistorianData(Connection connection, DateTime startTime, DateTime stopTime, string measurementIDs = null, Resolution resolution = Resolution.Full)
        {
            return GetHistorianData(connection, startTime, stopTime, measurementIDs?.Split(',').Select(ulong.Parse), resolution);
        }
        /// <summary>
        /// Attempts to disconnect from this <see cref="LocalInputAdapter"/>.
        /// </summary>
        protected override void AttemptDisconnection()
        {
            if ((object)m_readTimer != null)
            {
                m_readTimer.Enabled = false;

                lock (m_readTimer)
                {
                    m_dataReader = null;
                }
            }

            if ((object)m_archiveReader != null)
            {
                m_archiveReader.Dispose();
                m_archiveReader = null;
            }
        }
        /// <summary>
        /// Attempts to connect to this <see cref="LocalInputAdapter"/>.
        /// </summary>
        protected override void AttemptConnection()
        {
            // This adapter is only engaged for history, so we don't process any data unless a temporal constraint is defined
            if (this.TemporalConstraintIsDefined())
            {
                // Turn off read timer if it's active
                m_readTimer.Enabled = false;

                if ((object)m_archiveReader != null)
                {
                    m_archiveReader.Dispose();
                    m_archiveReader = null;
                }

                // Attempt to connect to historian
                m_archiveReader = new Connection(m_historianServer, m_instanceName);
                ThreadPool.QueueUserWorkItem(StartDataReader);
            }
        }
        /// <summary>
        /// Releases the unmanaged resources used by the <see cref="LocalInputAdapter"/> object and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
        protected override void Dispose(bool disposing)
        {
            if (!m_disposed)
            {
                try
                {
                    if (disposing)
                    {
                        if ((object)m_readTimer != null)
                        {
                            m_readTimer.Elapsed -= m_readTimer_Elapsed;
                            m_readTimer.Dispose();
                            m_readTimer = null;
                        }

                        if ((object)m_archiveReader != null)
                        {
                            m_archiveReader.Dispose();
                            m_archiveReader = null;
                        }
                    }
                }
                finally
                {
                    m_disposed = true;          // Prevent duplicate dispose.
                    base.Dispose(disposing);    // Call base class Dispose().
                }
            }
        }