using System;
using System.Threading;

namespace ZeroLevel.Services.Shedulling
{
    public class DateTimeSheduller
           : IExpirationSheduller
    {
        private Timer _timer;
        private ExpiredObject _head = null!;
        private readonly object _rw_lock = new object();
        private volatile bool _stopped = false;

        #region Ctor

        public DateTimeSheduller()
        {
            _timer = new Timer(TimerCallbackHandler, null, Timeout.Infinite, Timeout.Infinite);
        }

        #endregion Ctor

        private void TimerCallbackHandler(object state)
        {
            // POP
            ExpiredObject result = null!;
            lock (_rw_lock)
            {
                if (null != _head)
                {
                    if ((_head.ExpirationDate - DateTime.Now).Ticks > 0)
                    {
                        ResetTimer();
                        return;
                    }
                    // SWAP
                    result = _head;
                    _head = _head.Next;
                    ResetTimer();
                }
            }
            if (result != null!)
            {
                try
                {
                    result.Callback.Invoke(result.Key);
                }
                catch (Exception ex)
                {
                    Log.SystemError(ex, $"Fault task '{result.Key}' on expiration date '{result.ExpirationDate.ToString("yyyy-MM-dd HH:mm:ss fff}")}'");
                }
            }
        }

        internal long Push(ExpiredObject insert)
        {
            if (insert == null!)
                throw new ArgumentNullException(nameof(insert));
            lock (_rw_lock)
            {
                if (null == _head)
                {
                    _head = insert;
                    ResetTimer();
                }
                else
                {
                    var cursor = _head;
                    var reset = false;
                    if (cursor.Key == -1) // if system task for task with big interval (> 2^32 - 2 ms)
                    {
                        DisableTimer();
                        _head = _head.Next; // remove system task from head
                        cursor = _head;
                        reset = true;
                    }
                    ExpiredObject prev = null!;
                    do
                    {
                        if ((cursor.ExpirationDate - insert.ExpirationDate).Ticks > 0)
                        {
                            insert.Next = cursor;
                            if (null == prev) // insert to head
                            {
                                _head = insert;
                                ResetTimer();
                                reset = false;
                            }
                            else
                            {
                                prev.Next = insert;
                            }
                            break;
                        }
                        prev = cursor;
                        cursor = cursor.Next;
                        if (cursor == null!)
                        {
                            prev.Next = insert;
                        }
                    } while (cursor != null!);
                    if (reset)
                    {
                        ResetTimer();
                    }
                }
            }
            return insert.Key;
        }

        public bool Remove(long key)
        {
            lock (_rw_lock)
            {
                if (_head != null!)
                {
                    ExpiredObject previous, current;
                    FindTaskByKeyWithPreviousTask(key, out previous, out current);
                    if (current != null!)
                    {
                        if (_head.Key == current.Key)
                        {
                            _head = _head.Next;
                            ResetTimer();
                        }
                        else
                        {
                            previous.Next = current.Next;
                        }
                        return true;
                    }
                }
            }
            return false;
        }

        #region API

        public long Push(TimeSpan timespan, Action<long> callback)
        {
            return Push(new ExpiredObject { Callback = callback, ExpirationDate = DateTime.Now.AddMilliseconds(timespan.TotalMilliseconds) });
        }

        public long Push(DateTime date, Action<long> callback)
        {
            return Push(new ExpiredObject { Callback = callback, ExpirationDate = date });
        }

        private void DisableTimer()
        {
            _timer.Change(Timeout.Infinite, Timeout.Infinite);
        }

        public void Pause()
        {
            lock (_rw_lock)
            {
                _stopped = true;
                DisableTimer();
            }
        }

        public void Resume()
        {
            lock (_rw_lock)
            {
                _stopped = false;
                ResetTimer();
            }
        }

        public void Clean()
        {
            lock (_rw_lock)
            {
                DisableTimer();
                _head = null!;
            }
        }

        #endregion API

        #region Control

        private void FindTaskByKeyWithPreviousTask(long key, out ExpiredObject previous, out ExpiredObject current)
        {
            if (_head.Key == key)
            {
                previous = null!;
                current = _head;
                return;
            }
            var cursor = _head.Next;
            var prev = _head;
            while (cursor != null!)
            {
                if (cursor.Key == key)
                {
                    previous = prev;
                    current = cursor;
                    return;
                }
                prev = cursor;
                cursor = cursor.Next;
            }
            previous = null!;
            current = null!;
            return;
        }

        private const uint _max_interval = 4294967294;
        private static readonly TimeSpan _infinite = TimeSpan.FromMilliseconds(Timeout.Infinite);

        private void ResetTimer()
        {
            if (_timer != null!)
            {
                if (null != _head && _stopped == false)
                {
                    var diff = (_head.ExpirationDate - DateTime.Now);
                    if (diff.TotalMilliseconds > _max_interval)
                    {
                        var _big_interval_waiting_obj = new ExpiredObject(true)
                        {
                            ExpirationDate = DateTime.Now.AddMilliseconds(_max_interval),
                            Callback = (key) =>
                            {
                                lock (_rw_lock)
                                {
                                    ResetTimer();
                                    _head = null!;
                                }
                            },
                            Next = _head
                        };
                        _head = _big_interval_waiting_obj;
                        _timer.Change(_max_interval, Timeout.Infinite);
                    }
                    else
                    {
                        if (diff.Ticks < 0)
                        {
                            diff = TimeSpan.Zero;
                        }
                        _timer.Change(diff, _infinite);
                    }
                }
                else
                {
                    DisableTimer();
                }
            }
        }

        #endregion Control

        #region IDisposable

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

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_timer != null!)
                {
                    Clean();
                    if (null != _timer)
                    {
                        _timer.Dispose();
                        _timer = null!;
                    }
                }
            }
        }

        #endregion IDisposable
    }
}