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

using System;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.HttpSys.Internal;

namespace Microsoft.AspNetCore.Server.HttpSys
{
    // See the native HTTP_TIMEOUT_LIMIT_INFO structure documentation for additional information.
    // http://msdn.microsoft.com/en-us/library/aa364661.aspx

    /// <summary>
    /// Exposes the Http.Sys timeout configurations.  These may also be configured in the registry.
    /// These settings do not apply when attaching to an existing queue.
    /// </summary>
    public sealed class TimeoutManager
    {
        private static readonly int TimeoutLimitSize =
            Marshal.SizeOf<HttpApiTypes.HTTP_TIMEOUT_LIMIT_INFO>();

        private UrlGroup? _urlGroup;
        private readonly int[] _timeouts;
        private uint _minSendBytesPerSecond;

        internal TimeoutManager()
        {
            // We have to maintain local state since we allow applications to set individual timeouts. Native Http
            // API for setting timeouts expects all timeout values in every call so we have remember timeout values
            // to fill in the blanks. Except MinSendBytesPerSecond, local state for remaining five timeouts is
            // maintained in timeouts array.
            //
            // No initialization is required because a value of zero indicates that system defaults should be used.
            _timeouts = new int[5];
        }

        #region Properties

        /// <summary>
        /// The time, in seconds, allowed for the request entity body to arrive.  The default timer is 2 minutes.
        ///
        /// The HTTP Server API turns on this timer when the request has an entity body. The timer expiration is
        /// initially set to the configured value. When the HTTP Server API receives additional data indications on the
        /// request, it resets the timer to give the connection another interval.
        ///
        /// Use TimeSpan.Zero to indicate that system defaults should be used.
        /// </summary>
        public TimeSpan EntityBody
        {
            get
            {
                return GetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.EntityBody);
            }
            set
            {
                SetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.EntityBody, value);
            }
        }

        /// <summary>
        /// The time, in seconds, allowed for the HTTP Server API to drain the entity body on a Keep-Alive connection.
        /// The default timer is 2 minutes.
        ///
        /// On a Keep-Alive connection, after the application has sent a response for a request and before the request
        /// entity body has completely arrived, the HTTP Server API starts draining the remainder of the entity body to
        /// reach another potentially pipelined request from the client. If the time to drain the remaining entity body
        /// exceeds the allowed period the connection is timed out.
        ///
        /// Use TimeSpan.Zero to indicate that system defaults should be used.
        /// </summary>
        public TimeSpan DrainEntityBody
        {
            get
            {
                return GetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.DrainEntityBody);
            }
            set
            {
                SetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.DrainEntityBody, value);
            }
        }

        /// <summary>
        /// The time, in seconds, allowed for the request to remain in the request queue before the application picks
        /// it up.  The default timer is 2 minutes.
        ///
        /// Use TimeSpan.Zero to indicate that system defaults should be used.
        /// </summary>
        public TimeSpan RequestQueue
        {
            get
            {
                return GetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.RequestQueue);
            }
            set
            {
                SetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.RequestQueue, value);
            }
        }

        /// <summary>
        /// The time, in seconds, allowed for an idle connection.  The default timer is 2 minutes.
        ///
        /// This timeout is only enforced after the first request on the connection is routed to the application.
        ///
        /// Use TimeSpan.Zero to indicate that system defaults should be used.
        /// </summary>
        public TimeSpan IdleConnection
        {
            get
            {
                return GetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.IdleConnection);
            }
            set
            {
                SetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.IdleConnection, value);
            }
        }

        /// <summary>
        /// The time, in seconds, allowed for the HTTP Server API to parse the request header.  The default timer is
        /// 2 minutes.
        ///
        /// This timeout is only enforced after the first request on the connection is routed to the application.
        ///
        /// Use TimeSpan.Zero to indicate that system defaults should be used.
        /// </summary>
        public TimeSpan HeaderWait
        {
            get
            {
                return GetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.HeaderWait);
            }
            set
            {
                SetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.HeaderWait, value);
            }
        }

        /// <summary>
        /// The minimum send rate, in bytes-per-second, for the response. The default response send rate is 150
        /// bytes-per-second.
        ///
        /// Use 0 to indicate that system defaults should be used.
        ///
        /// To disable this timer set it to UInt32.MaxValue
        /// </summary>
        public long MinSendBytesPerSecond
        {
            get
            {
                // Since we maintain local state, GET is local.
                return _minSendBytesPerSecond;
            }
            set
            {
                // MinSendRate value is ULONG in native layer.
                if (value < 0 || value > uint.MaxValue)
                {
                    throw new ArgumentOutOfRangeException(nameof(value));
                }

                SetUrlGroupTimeouts(_timeouts, (uint)value);
                _minSendBytesPerSecond = (uint)value;
            }
        }

        #endregion Properties

        #region Helpers

        private TimeSpan GetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE type)
        {
            // Since we maintain local state, GET is local.
            return new TimeSpan(0, 0, (int)_timeouts[(int)type]);
        }

        private void SetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE type, TimeSpan value)
        {
            // All timeouts are defined as USHORT in native layer (except MinSendRate, which is ULONG). Make sure that
            // timeout value is within range.

            var timeoutValue = Convert.ToInt64(value.TotalSeconds);

            if (timeoutValue < 0 || timeoutValue > ushort.MaxValue)
            {
                throw new ArgumentOutOfRangeException(nameof(value));
            }

            // Use local state to get values for other timeouts. Call into the native layer and if that
            // call succeeds, update local state.
            var newTimeouts = (int[])_timeouts.Clone();
            newTimeouts[(int)type] = (int)timeoutValue;
            SetUrlGroupTimeouts(newTimeouts, _minSendBytesPerSecond);
            _timeouts[(int)type] = (int)timeoutValue;
        }

        internal void SetUrlGroupTimeouts(UrlGroup urlGroup)
        {
            _urlGroup = urlGroup;
            SetUrlGroupTimeouts(_timeouts, _minSendBytesPerSecond);
        }

        private unsafe void SetUrlGroupTimeouts(int[] timeouts, uint minSendBytesPerSecond)
        {
            if (_urlGroup == null)
            {
                // Not started yet
                return;
            }

            var timeoutinfo = new HttpApiTypes.HTTP_TIMEOUT_LIMIT_INFO();

            timeoutinfo.Flags = HttpApiTypes.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT;
            timeoutinfo.DrainEntityBody =
                (ushort)timeouts[(int)HttpApiTypes.HTTP_TIMEOUT_TYPE.DrainEntityBody];
            timeoutinfo.EntityBody =
                (ushort)timeouts[(int)HttpApiTypes.HTTP_TIMEOUT_TYPE.EntityBody];
            timeoutinfo.RequestQueue =
                (ushort)timeouts[(int)HttpApiTypes.HTTP_TIMEOUT_TYPE.RequestQueue];
            timeoutinfo.IdleConnection =
                (ushort)timeouts[(int)HttpApiTypes.HTTP_TIMEOUT_TYPE.IdleConnection];
            timeoutinfo.HeaderWait =
                (ushort)timeouts[(int)HttpApiTypes.HTTP_TIMEOUT_TYPE.HeaderWait];
            timeoutinfo.MinSendRate = minSendBytesPerSecond;

            var infoptr = new IntPtr(&timeoutinfo);

            _urlGroup.SetProperty(
                HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerTimeoutsProperty,
                infoptr, (uint)TimeoutLimitSize);
        }

        #endregion Helpers
    }
}
