using System;
using System.Threading;

namespace Renci.SshNet.Common
{
    /// <summary>
    /// Light implementation of SemaphoreSlim.
    /// </summary>
    public class SemaphoreLight : IDisposable
    {
        private readonly object _lock = new object();
        private ManualResetEvent _waitHandle;

        private int _currentCount;

        /// <summary>
        /// Initializes a new instance of the <see cref="SemaphoreLight"/> class, specifying the initial number of requests that can
        /// be granted concurrently.
        /// </summary>
        /// <param name="initialCount">The initial number of requests for the semaphore that can be granted concurrently.</param>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="initialCount"/> is a negative number.</exception>
        public SemaphoreLight(int initialCount)
        {
            if (initialCount < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(initialCount), "The value cannot be negative.");
            }

            _currentCount = initialCount;
        }

        /// <summary>
        /// Gets the current count of the <see cref="SemaphoreLight"/>.
        /// </summary>
        public int CurrentCount
        {
            get { return _currentCount; }
        }

        /// <summary>
        /// Gets a <see cref="WaitHandle"/> that can be used to wait on the semaphore.
        /// </summary>
        /// <value>
        /// A <see cref="WaitHandle"/> that can be used to wait on the semaphore.
        /// </value>
        /// <remarks>
        /// A successful wait on the <see cref="AvailableWaitHandle"/> does not imply a successful
        /// wait on the <see cref="SemaphoreLight"/> itself. It should be followed by a true wait
        /// on the semaphore.
        /// </remarks>
        public WaitHandle AvailableWaitHandle
        {
            get
            {
                if (_waitHandle is null)
                {
                    lock (_lock)
                    {
                        _waitHandle ??= new ManualResetEvent(_currentCount > 0);
                    }
                }

                return _waitHandle;
            }
        }

        /// <summary>
        /// Exits the <see cref="SemaphoreLight"/> once.
        /// </summary>
        /// <returns>The previous count of the <see cref="SemaphoreLight"/>.</returns>
        public int Release()
        {
            return Release(1);
        }

        /// <summary>
        /// Exits the <see cref="SemaphoreLight"/> a specified number of times.
        /// </summary>
        /// <param name="releaseCount">The number of times to exit the semaphore.</param>
        /// <returns>
        /// The previous count of the <see cref="SemaphoreLight"/>.
        /// </returns>
        public int Release(int releaseCount)
        {
            lock (_lock)
            {
                var oldCount = _currentCount;

                _currentCount += releaseCount;

                // signal waithandle when the original semaphore count was zero
                if (_waitHandle != null && oldCount == 0)
                {
                    _ = _waitHandle.Set();
                }

                Monitor.PulseAll(_lock);

                return oldCount;
            }
        }

        /// <summary>
        /// Blocks the current thread until it can enter the <see cref="SemaphoreLight"/>.
        /// </summary>
        public void Wait()
        {
            lock (_lock)
            {
                while (_currentCount < 1)
                {
                    _ = Monitor.Wait(_lock);
                }

                _currentCount--;

                // unsignal waithandle when the semaphore count reaches zero
                if (_waitHandle != null && _currentCount == 0)
                {
                    _ = _waitHandle.Reset();
                }

                Monitor.PulseAll(_lock);
            }
        }

        /// <summary>
        /// Blocks the current thread until it can enter the <see cref="SemaphoreLight"/>, using a 32-bit signed
        /// integer that specifies the timeout.
        /// </summary>
        /// <param name="millisecondsTimeout">The number of milliseconds to wait, or Infinite(-1) to wait indefinitely.</param>
        /// <returns>
        /// <c>true</c> if the current thread successfully entered the <see cref="SemaphoreLight"/>; otherwise, <c>false</c>.
        /// </returns>
        public bool Wait(int millisecondsTimeout)
        {
            if (millisecondsTimeout < -1)
            {
                throw new ArgumentOutOfRangeException(nameof(millisecondsTimeout), "The timeout must represent a value between -1 and Int32.MaxValue, inclusive.");
            }

            return WaitWithTimeout(millisecondsTimeout);
        }

        /// <summary>
        /// Blocks the current thread until it can enter the <see cref="SemaphoreLight"/>, using a <see cref="TimeSpan"/>
        /// to specify the timeout.
        /// </summary>
        /// <param name="timeout">A <see cref="TimeSpan"/> that represents the number of milliseconds to wait, or a <see cref="TimeSpan"/> that represents -1 milliseconds to wait indefinitely.</param>
        /// <returns>
        /// <c>true</c> if the current thread successfully entered the <see cref="SemaphoreLight"/>; otherwise, <c>false</c>.
        /// </returns>
        public bool Wait(TimeSpan timeout)
        {
            var timeoutInMilliseconds = timeout.TotalMilliseconds;
            if (timeoutInMilliseconds is < -1d or > int.MaxValue)
            {
                throw new ArgumentOutOfRangeException(nameof(timeout), "The timeout must represent a value between -1 and Int32.MaxValue, inclusive.");
            }

            return WaitWithTimeout((int) timeoutInMilliseconds);
        }

        private bool WaitWithTimeout(int timeoutInMilliseconds)
        {
            lock (_lock)
            {
                if (timeoutInMilliseconds == Session.Infinite)
                {
                    while (_currentCount < 1)
                    {
                        _ = Monitor.Wait(_lock);
                    }
                }
                else
                {
                    if (_currentCount < 1)
                    {
                        if (timeoutInMilliseconds > 0)
                        {
                            return false;
                        }

                        var remainingTimeInMilliseconds = timeoutInMilliseconds;
                        var startTicks = Environment.TickCount;

                        while (_currentCount < 1)
                        {
                            if (!Monitor.Wait(_lock, remainingTimeInMilliseconds))
                            {
                                return false;
                            }

                            var elapsed = Environment.TickCount - startTicks;
                            remainingTimeInMilliseconds -= elapsed;
                            if (remainingTimeInMilliseconds < 0)
                            {
                                return false;
                            }
                        }
                    }
                }

                _currentCount--;

                // unsignal waithandle when the semaphore count is zero
                if (_waitHandle != null && _currentCount == 0)
                {
                    _ = _waitHandle.Reset();
                }

                Monitor.PulseAll(_lock);

                return true;
            }
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected void Dispose(bool disposing)
        {
            if (disposing)
            {
                var waitHandle = _waitHandle;
                if (waitHandle is not null)
                {
                    waitHandle.Dispose();
                    _waitHandle = null;
                }
            }
        }
    }
}
