// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.IO;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;

namespace Microsoft.AspNetCore.TestHost
{
    internal class ResponseFeature : IHttpResponseFeature, IHttpResponseBodyFeature
    {
        private readonly HeaderDictionary _headers = new HeaderDictionary();
        private readonly Action<Exception> _abort;

        private Func<Task> _responseStartingAsync = () => Task.FromResult(true);
        private Func<Task> _responseCompletedAsync = () => Task.FromResult(true);
        private int _statusCode;
        private string _reasonPhrase;

        public ResponseFeature(Action<Exception> abort)
        {
            Headers = _headers;

            // 200 is the default status code all the way down to the host, so we set it
            // here to be consistent with the rest of the hosts when writing tests.
            StatusCode = 200;
            _abort = abort;
        }

        public int StatusCode
        {
            get => _statusCode;
            set
            {
                if (HasStarted)
                {
                    throw new InvalidOperationException("The status code cannot be set, the response has already started.");
                }
                if (value < 100)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), value, "The status code cannot be set to a value less than 100");
                }

                _statusCode = value;
            }
        }

        public string ReasonPhrase
        {
            get => _reasonPhrase;
            set
            {
                if (HasStarted)
                {
                    throw new InvalidOperationException("The reason phrase cannot be set, the response has already started.");
                }

                _reasonPhrase = value;
            }
        }

        public IHeaderDictionary Headers { get; set; }

        public Stream Body { get; set; }

        public Stream Stream => Body;

        internal PipeWriter BodyWriter { get; set; }

        public PipeWriter Writer => BodyWriter;

        public bool HasStarted { get; set; }

        public void OnStarting(Func<object, Task> callback, object state)
        {
            if (HasStarted)
            {
                throw new InvalidOperationException();
            }

            var prior = _responseStartingAsync;
            _responseStartingAsync = async () =>
            {
                await callback(state);
                await prior();
            };
        }

        public void OnCompleted(Func<object, Task> callback, object state)
        {
            var prior = _responseCompletedAsync;
            _responseCompletedAsync = async () =>
            {
                try
                {
                    await callback(state);
                }
                finally
                {
                    await prior();
                }
            };
        }

        public async Task FireOnSendingHeadersAsync()
        {
            if (!HasStarted)
            {
                try
                {
                    await _responseStartingAsync();
                }
                finally
                {
                    HasStarted = true;
                    _headers.IsReadOnly = true;
                }
            }
        }

        public Task FireOnResponseCompletedAsync()
        {
            return _responseCompletedAsync();
        }

        public async Task StartAsync(CancellationToken token = default)
        {
            try
            {
                await FireOnSendingHeadersAsync();
            }
            catch (Exception ex)
            {
                _abort(ex);
                throw;
            }
        }

        public void DisableBuffering()
        {
        }

        public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
        {
            return SendFileFallback.SendFileAsync(Stream, path, offset, count, cancellation);
        }

        public Task CompleteAsync()
        {
            return Writer.CompleteAsync().AsTask();
        }
    }
}
