// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using System.Collections.Generic;
using Microsoft.Win32;

namespace System.Diagnostics.Eventing.Reader
{
    /// <summary>
    /// This public class is used for reading event records from event log.
    /// </summary>
    public class EventLogReader : IDisposable
    {
        private readonly EventLogQuery _eventQuery;

        private int _batchSize;

        //
        // access to the data member reference is safe, while
        // invoking methods on it is marked SecurityCritical as appropriate.
        //
        private readonly EventLogHandle _handle;

        /// <summary>
        /// events buffer holds batched event (handles).
        /// </summary>
        private IntPtr[] _eventsBuffer;
        /// <summary>
        /// The current index where the function GetNextEvent is (inside the eventsBuffer).
        /// </summary>
        private int _currentIndex;
        /// <summary>
        /// The number of events read from the batch into the eventsBuffer
        /// </summary>
        private int _eventCount;

        /// <summary>
        /// When the reader finishes (will always return only ERROR_NO_MORE_ITEMS).
        /// For subscription, this means we need to wait for next event.
        /// </summary>
        private bool _isEof;

        /// <summary>
        /// Maintains cached display / metadata information returned from
        /// EventRecords that were obtained from this reader.
        /// </summary>
        private readonly ProviderMetadataCachedInformation _cachedMetadataInformation;

        public EventLogReader(string path)
            : this(new EventLogQuery(path, PathType.LogName), null)
        {
        }

        public EventLogReader(string path, PathType pathType)
            : this(new EventLogQuery(path, pathType), null)
        {
        }

        public EventLogReader(EventLogQuery eventQuery)
            : this(eventQuery, null)
        {
        }

        public EventLogReader(EventLogQuery eventQuery, EventBookmark bookmark)
        {
            ArgumentNullException.ThrowIfNull(eventQuery);

            string logfile = null;
            if (eventQuery.ThePathType == PathType.FilePath)
                logfile = eventQuery.Path;

            _cachedMetadataInformation = new ProviderMetadataCachedInformation(eventQuery.Session, logfile, 50);

            // Explicit data
            _eventQuery = eventQuery;

            // Implicit
            _batchSize = 64;
            _eventsBuffer = new IntPtr[_batchSize];

            //
            // compute the flag.
            //
            int flag = 0;

            if (_eventQuery.ThePathType == PathType.LogName)
                flag |= (int)UnsafeNativeMethods.EvtQueryFlags.EvtQueryChannelPath;
            else
                flag |= (int)UnsafeNativeMethods.EvtQueryFlags.EvtQueryFilePath;

            if (_eventQuery.ReverseDirection)
                flag |= (int)UnsafeNativeMethods.EvtQueryFlags.EvtQueryReverseDirection;

            if (_eventQuery.TolerateQueryErrors)
                flag |= (int)UnsafeNativeMethods.EvtQueryFlags.EvtQueryTolerateQueryErrors;

            _handle = NativeWrapper.EvtQuery(_eventQuery.Session.Handle,
                _eventQuery.Path, _eventQuery.Query,
                flag);

            EventLogHandle bookmarkHandle = EventLogRecord.GetBookmarkHandleFromBookmark(bookmark);

            if (!bookmarkHandle.IsInvalid)
            {
                using (bookmarkHandle)
                {
                    NativeWrapper.EvtSeek(_handle, 1, bookmarkHandle, 0, UnsafeNativeMethods.EvtSeekFlags.EvtSeekRelativeToBookmark);
                }
            }
        }

        public int BatchSize
        {
            get
            {
                return _batchSize;
            }
            set
            {
                if (value < 1)
                    throw new ArgumentOutOfRangeException(nameof(value));
                _batchSize = value;
            }
        }

        private bool GetNextBatch(TimeSpan ts)
        {
            int timeout = -1;
            if (ts != TimeSpan.MaxValue)
                timeout = (int)ts.TotalMilliseconds;

            // batchSize was changed by user, reallocate buffer.
            if (_batchSize != _eventsBuffer.Length)
                _eventsBuffer = new IntPtr[_batchSize];

            int newEventCount = 0;
            bool results = NativeWrapper.EvtNext(_handle, _batchSize, _eventsBuffer, timeout, 0, ref newEventCount);

            if (!results)
            {
                _eventCount = 0;
                _currentIndex = 0;
                return false; // No more events in the result set
            }

            _currentIndex = 0;
            _eventCount = newEventCount;
            return true;
        }

        public EventRecord ReadEvent()
        {
            return ReadEvent(TimeSpan.MaxValue);
        }

        public EventRecord ReadEvent(TimeSpan timeout)
        {
            if (_isEof)
                throw new InvalidOperationException();

            if (_currentIndex >= _eventCount)
            {
                // buffer is empty, get next batch.
                GetNextBatch(timeout);

                if (_currentIndex >= _eventCount)
                {
                    _isEof = true;
                    return null;
                }
            }

            EventLogRecord eventInstance = new EventLogRecord(new EventLogHandle(_eventsBuffer[_currentIndex], true), _eventQuery.Session, _cachedMetadataInformation);
            _currentIndex++;
            return eventInstance;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            while (_currentIndex < _eventCount)
            {
                NativeWrapper.EvtClose(_eventsBuffer[_currentIndex]);
                _currentIndex++;
            }

            if (_handle != null && !_handle.IsInvalid)
                _handle.Dispose();
        }

        internal void SeekReset()
        {
            //
            // Close all unread event handles in the buffer
            //
            while (_currentIndex < _eventCount)
            {
                NativeWrapper.EvtClose(_eventsBuffer[_currentIndex]);
                _currentIndex++;
            }

            // Reset the indexes used by Next
            _currentIndex = 0;
            _eventCount = 0;
            _isEof = false;
        }

        internal void SeekCommon(long offset)
        {
            //
            // modify offset that we're going to send to service to account for the
            // fact that we've already read some events in our buffer that the user
            // hasn't seen yet.
            //
            offset -= (_eventCount - _currentIndex);

            SeekReset();

            NativeWrapper.EvtSeek(_handle, offset, EventLogHandle.Zero, 0, UnsafeNativeMethods.EvtSeekFlags.EvtSeekRelativeToCurrent);
        }

        public void Seek(EventBookmark bookmark)
        {
            Seek(bookmark, 0);
        }

        public void Seek(EventBookmark bookmark, long offset)
        {
            ArgumentNullException.ThrowIfNull(bookmark);

            SeekReset();
            using (EventLogHandle bookmarkHandle = EventLogRecord.GetBookmarkHandleFromBookmark(bookmark))
            {
                NativeWrapper.EvtSeek(_handle, offset, bookmarkHandle, 0, UnsafeNativeMethods.EvtSeekFlags.EvtSeekRelativeToBookmark);
            }
        }

        public void Seek(SeekOrigin origin, long offset)
        {
            switch (origin)
            {
                case SeekOrigin.Begin:

                    SeekReset();
                    NativeWrapper.EvtSeek(_handle, offset, EventLogHandle.Zero, 0, UnsafeNativeMethods.EvtSeekFlags.EvtSeekRelativeToFirst);
                    return;

                case SeekOrigin.End:

                    SeekReset();
                    NativeWrapper.EvtSeek(_handle, offset, EventLogHandle.Zero, 0, UnsafeNativeMethods.EvtSeekFlags.EvtSeekRelativeToLast);
                    return;

                case SeekOrigin.Current:
                    if (offset >= 0)
                    {
                        // We can reuse elements in the batch.
                        if (_currentIndex + offset < _eventCount)
                        {
                            //
                            // We don't call Seek here, we can reposition within the batch.
                            //

                            // Close all event handles between [currentIndex, currentIndex + offset)
                            int index = _currentIndex;
                            while (index < _currentIndex + offset)
                            {
                                NativeWrapper.EvtClose(_eventsBuffer[index]);
                                index++;
                            }

                            _currentIndex = (int)(_currentIndex + offset);
                            // Leave the eventCount unchanged
                            // Leave the same Eof
                        }
                        else
                        {
                            SeekCommon(offset);
                        }
                    }
                    else
                    {
                        SeekCommon(offset);
                    }
                    return;
            }
        }

        public void CancelReading()
        {
            NativeWrapper.EvtCancel(_handle);
        }

        public IList<EventLogStatus> LogStatus
        {
            get
            {
                EventLogHandle queryHandle = _handle;

                if (queryHandle.IsInvalid)
                    throw new InvalidOperationException();

                string[] channelNames = (string[])NativeWrapper.EvtGetQueryInfo(queryHandle, UnsafeNativeMethods.EvtQueryPropertyId.EvtQueryNames);
                int[] errorStatuses = (int[])NativeWrapper.EvtGetQueryInfo(queryHandle, UnsafeNativeMethods.EvtQueryPropertyId.EvtQueryStatuses);

                if (channelNames.Length != errorStatuses.Length)
                    throw new InvalidOperationException();

                var list = new List<EventLogStatus>(channelNames.Length);
                for (int i = 0; i < channelNames.Length; i++)
                {
                    EventLogStatus cs = new EventLogStatus(channelNames[i], errorStatuses[i]);
                    list.Add(cs);
                }
                return list.AsReadOnly();
            }
        }
    }
}
