using System;
using System.Diagnostics;
using System.Linq;
using System.Timers;
using NLog;
using Timer = System.Timers.Timer;

namespace ATxCommon.Monitoring
{
    /// <summary>
    /// Abstract load monitoring class, constantly checking the load at the given <see
    /// cref="Interval"/> in a separate (timer-based) thread.
    /// 
    /// The load (depending on the implementation in the derived class) is determined using a <see
    /// cref="PerformanceCounter"/>, and is compared against a configurable <see cref="Limit"/>. If
    /// the load changes from below the limit to above, a <see cref="LoadAboveLimit"/> event will
    /// be raised. If the load has been above the limit and is then dropping below, an <see
    /// cref="OnLoadBelowLimit"/> event will be raised as soon as a given number of consecutive
    /// load measurements (defined via <see cref="Probation"/>) were found to be below the limit.
    /// </summary>
    public abstract class MonitorBase
    {
        protected static readonly Logger Log = LogManager.GetCurrentClassLogger();

        /// <summary>
        /// The generic event handler delegate for load monitoring events.
        /// </summary>
        public delegate void EventHandler(object sender, EventArgs e);
        
        /// <summary>
        /// Event raised when the load exceeds the limit for any (i.e. single!) measurement.
        /// </summary>
        public event EventHandler LoadAboveLimit;

        /// <summary>
        /// Event raised when the load is below the configured limit for at least the number of
        /// consecutive measurements configured in <see cref="Probation"/> after having exceeded
        /// this limit before.
        /// </summary>
        public event EventHandler LoadBelowLimit;

        protected readonly Timer MonitoringTimer;
        protected readonly PerformanceCounter PerfCounter;
        protected readonly float[] LoadReadings = {0F, 0F, 0F, 0F};

        private int _interval;
        private int _behaving;
        private int _probation;

        private float _limit;

        /// <summary>
        /// Description string to be used in log messages.
        /// </summary>
        private readonly string _description;


        #region properties

        /// <summary>
        /// Name of the performance counter category, see also <see cref="PerformanceCounter"/>.
        /// </summary>
        protected abstract string Category { get; set; }

        /// <summary>
        /// Current load, averaged of the last four readings.
        /// </summary>
        /// <returns>The average load from the last four readings.</returns>
        public float Load { get; private set; }

        /// <summary>
        /// Flag representing whether the load is considered to be high or low.
        /// </summary>
        public bool HighLoad { get; private set; }

        /// <summary>
        /// Time interval (in ms) after which to update the current load measurement.
        /// </summary>
        public int Interval {
            get => _interval;
            set {
                _interval = value;
                MonitoringTimer.Interval = value;
                Log.Debug("{0} monitoring interval: {1}ms", _description, _interval);
            }
        }

        /// <summary>
        /// Upper limit of the load before it is classified as "high".
        /// </summary>
        public float Limit {
            get => _limit;
            set {
                _limit = value;
                Log.Debug("{0} monitoring limit: {1:0.000}", _description, _limit);
            }
        }

        /// <summary>
        /// Number of cycles where the load value has to be below the limit before it is
        /// classified as "low" again.
        /// </summary>
        public int Probation {
            get => _probation;
            set {
                _probation = value;
                Log.Debug("{0} monitoring probation cycles when violating limit: {1}",
                    _description, _probation);
            }
        }
        
        /// <summary>
        /// Indicating whether this load monitor instance is active.
        /// </summary>
        public bool Enabled {
            get => MonitoringTimer.Enabled;
            set {
                Log.Debug("{0} - {1} monitoring.", _description, value ? "enabling" : "disabling");
                MonitoringTimer.Enabled = value;
            }
        }

        /// <summary>
        /// Log level to use for reporting current performance readings, default = Trace.
        /// </summary>
        public LogLevel LogPerformanceReadings { get; set; } = LogLevel.Trace;

        #endregion


        /// <summary>
        /// Create performance counter and initialize it.
        /// </summary>
        /// <param name="counterName">The counter to use for the monitoring, depends on the
        /// category used in the derived class.
        /// </param>
        protected MonitorBase(string counterName) {
            _interval = 250;
            _limit = 0.5F;
            _probation = 40;
            Log.Info("Initializing {0} PerformanceCounter for [{1}].", Category, counterName);
            // assemble the description string to be used in messages:
            _description = $"{Category} {counterName}";
            try {
                PerfCounter = new PerformanceCounter(Category, counterName, "_Total");
                var curLoad = PerfCounter.NextValue();
                Log.Debug("{0} initial value: {1:0.000}", _description, curLoad);
                /* this initialization might be necessary for "Processor" counters, so we just
                 * temporarily disable those calls:
                Thread.Sleep(1000);
                curLoad = _perfCounter.NextValue();
                Log.Debug("{0} current value: {1:0.000}", _description, curLoad);
                 */
                // initialize the load state as high, so we have to pass probation at least once:
                HighLoad = true;
                MonitoringTimer = new Timer(_interval);
                MonitoringTimer.Elapsed += UpdateLoadReadings;
            }
            catch (Exception) {
                Log.Error("{0} monitoring initialization failed!", Category);
                throw;
            }

            Log.Debug("{0} monitoring initialization completed.", _description);
        }

        /// <summary>
        /// Check current load value, update the history of readings and trigger the corresponding
        /// events if the required criteria are met.
        /// </summary>
        private void UpdateLoadReadings(object sender, ElapsedEventArgs e) {
            MonitoringTimer.Enabled = false;
            try {
                // ConstrainedCopy seems to be the most efficient approach to shift the array:
                Array.ConstrainedCopy(LoadReadings, 1, LoadReadings, 0, 3);
                LoadReadings[3] = PerfCounter.NextValue();
                Load = LoadReadings.Average();
                if (LoadReadings[3] > _limit) {
                    if (_behaving > _probation) {
                        // this means the load was considered as "low" before, so raise an event:
                        OnLoadAboveLimit();
                        Log.Trace("{0} ({1:0.00}) violating limit ({2})!",
                            _description, LoadReadings[3], _limit);
                    } else if (_behaving > 0) {
                        // this means we were still in probation, so no need to trigger again...
                        Log.Trace("{0}: resetting behaving counter (was {1}).",
                            _description, _behaving);
                    }
                    _behaving = 0;
                } else {
                    _behaving++;
                    if (_behaving == _probation) {
                        Log.Trace("{0} below limit for {1} cycles, passing probation!",
                            _description, _probation);
                        OnLoadBelowLimit();
                    } else if (_behaving > _probation) {
                        Log.Trace("{0} behaving well since {1} cycles.",
                            _description, _behaving);
                    } else if (_behaving < 0) {
                        Log.Info("{0}: integer wrap around happened, resetting probation " +
                                 "counter (no reason to worry).", _description);
                        _behaving = _probation + 1;
                    }
                }
            }
            catch (Exception ex) {
                Log.Error("Updating {0} counters failed: {1}", _description, ex.Message);
            }
            finally {
                MonitoringTimer.Enabled = true;
            }
            Log.Log(LogPerformanceReadings, "{0}: {1:0.000} {2}", _description,
                LoadReadings[3], LoadReadings[3] < Limit ? " [" + _behaving + "]" : "");
        }

        /// <summary>
        /// Raise the "LoadAboveLimit" event.
        /// </summary>
        protected virtual void OnLoadAboveLimit() {
            HighLoad = true;
            LoadAboveLimit?.Invoke(this, EventArgs.Empty);
        }

        /// <summary>
        /// Raise the "LoadBelowLimit" event.
        /// </summary>
        protected virtual void OnLoadBelowLimit() {
            HighLoad = false;
            LoadBelowLimit?.Invoke(this, EventArgs.Empty);
        }
    }
}