//
// WebRequestStream.cs
//
// Author:
//       Martin Baulig <mabaul@microsoft.com>
//
// Copyright (c) 2017 Xamarin Inc. (http://www.xamarin.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.ExceptionServices;
using System.Net.Sockets;

namespace System.Net
{
	class WebRequestStream : WebConnectionStream
	{
		static byte[] crlf = new byte[] { 13, 10 };
		MemoryStream writeBuffer;
		bool requestWritten;
		bool allowBuffering;
		bool sendChunked;
		WebCompletionSource pendingWrite;
		long totalWritten;
		byte[] headers;
		bool headersSent;
		int completeRequestWritten;
		int chunkTrailerWritten;

		internal readonly string ME;

		public WebRequestStream (WebConnection connection, WebOperation operation,
					 Stream stream, WebConnectionTunnel tunnel)
			: base (connection, operation)
		{
			InnerStream = stream;

			allowBuffering = operation.Request.InternalAllowBuffering;
			sendChunked = operation.Request.SendChunked && operation.WriteBuffer == null;
			if (!sendChunked && allowBuffering && operation.WriteBuffer == null)
				writeBuffer = new MemoryStream ();

			KeepAlive = Request.KeepAlive;
			if (tunnel?.ProxyVersion != null && tunnel?.ProxyVersion != HttpVersion.Version11)
				KeepAlive = false;

#if MONO_WEB_DEBUG
			ME = $"WRQ(Cnc={Connection.ID}, Op={Operation.ID})";
#endif
		}

		internal Stream InnerStream {
			get;
		}

		public bool KeepAlive {
			get;
		}

		public override bool CanRead => false;

		public override bool CanWrite => true;

		internal bool SendChunked {
			get { return sendChunked; }
			set { sendChunked = value; }
		}

		internal bool HasWriteBuffer {
			get {
				return Operation.WriteBuffer != null || writeBuffer != null;
			}
		}

		internal int WriteBufferLength {
			get {
				if (Operation.WriteBuffer != null)
					return Operation.WriteBuffer.Size;
				if (writeBuffer != null)
					return (int)writeBuffer.Length;
				return -1;
			}
		}

		internal BufferOffsetSize GetWriteBuffer ()
		{
			if (Operation.WriteBuffer != null)
				return Operation.WriteBuffer;
			if (writeBuffer == null || writeBuffer.Length == 0)
				return null;
			var buffer = writeBuffer.GetBuffer ();
			return new BufferOffsetSize (buffer, 0, (int)writeBuffer.Length, false);
		}

		async Task FinishWriting (CancellationToken cancellationToken)
		{
			if (Interlocked.CompareExchange (ref completeRequestWritten, 1, 0) != 0)
				return;

			WebConnection.Debug ($"{ME} FINISH WRITING: {sendChunked}");
			try {
				Operation.ThrowIfClosedOrDisposed (cancellationToken);
				if (sendChunked)
					await WriteChunkTrailer_inner (cancellationToken).ConfigureAwait (false);
			} catch (Exception ex) {
				Operation.CompleteRequestWritten (this, ex);
				throw;
			} finally {
				WebConnection.Debug ($"{ME} FINISH WRITING DONE");
			}

			Operation.CompleteRequestWritten (this);
		}

		public override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken)
		{
			if (buffer == null)
				throw new ArgumentNullException (nameof (buffer));

			int length = buffer.Length;
			if (offset < 0 || length < offset)
				throw new ArgumentOutOfRangeException (nameof (offset));
			if (count < 0 || (length - offset) < count)
				throw new ArgumentOutOfRangeException (nameof (count));

			WebConnection.Debug ($"{ME} WRITE ASYNC: {buffer.Length}/{offset}/{count}");

			if (cancellationToken.IsCancellationRequested)
				return Task.FromCanceled (cancellationToken);

			Operation.ThrowIfClosedOrDisposed (cancellationToken);

			if (Operation.WriteBuffer != null)
				throw new InvalidOperationException ();

			var completion = new WebCompletionSource ();
			if (Interlocked.CompareExchange (ref pendingWrite, completion, null) != null)
				throw new InvalidOperationException (SR.GetString (SR.net_repcall));

			return WriteAsyncInner (buffer, offset, count, completion, cancellationToken);
		}

		async Task WriteAsyncInner (byte[] buffer, int offset, int size,
		                            WebCompletionSource completion,
		                            CancellationToken cancellationToken)
		{
			try {
				await ProcessWrite (buffer, offset, size, cancellationToken).ConfigureAwait (false);

				WebConnection.Debug ($"{ME} WRITE ASYNC #1: {allowBuffering} {sendChunked} {Request.ContentLength} {totalWritten}");

				if (Request.ContentLength > 0 && totalWritten == Request.ContentLength)
					await FinishWriting (cancellationToken);

				pendingWrite = null;
				completion.TrySetCompleted ();
			} catch (Exception ex) {
				KillBuffer ();
				closed = true;

				WebConnection.Debug ($"{ME} WRITE ASYNC EX: {ex.Message}");

				var oldError = Operation.CheckDisposed (cancellationToken);
				if (oldError != null)
					ex = oldError.SourceException;
				else if (ex is SocketException)
					ex = new IOException ("Error writing request", ex);

				Operation.CompleteRequestWritten (this, ex);

				pendingWrite = null;
				completion.TrySetException (ex);

				if (oldError != null)
					oldError.Throw ();
				throw;
			}
		}

		async Task ProcessWrite (byte[] buffer, int offset, int size, CancellationToken cancellationToken)
		{
			Operation.ThrowIfClosedOrDisposed (cancellationToken);

			if (sendChunked) {
				requestWritten = true;

				string cSize = String.Format ("{0:X}\r\n", size);
				byte[] head = Encoding.ASCII.GetBytes (cSize);
				int chunkSize = 2 + size + head.Length;
				byte[] newBuffer = new byte[chunkSize];
				Buffer.BlockCopy (head, 0, newBuffer, 0, head.Length);
				Buffer.BlockCopy (buffer, offset, newBuffer, head.Length, size);
				Buffer.BlockCopy (crlf, 0, newBuffer, head.Length + size, crlf.Length);

				if (allowBuffering) {
					if (writeBuffer == null)
						writeBuffer = new MemoryStream ();
					writeBuffer.Write (buffer, offset, size);
				}

				totalWritten += size;

				buffer = newBuffer;
				offset = 0;
				size = chunkSize;
			} else {
				CheckWriteOverflow (Request.ContentLength, totalWritten, size);

				if (allowBuffering) {
					if (writeBuffer == null)
						writeBuffer = new MemoryStream ();
					writeBuffer.Write (buffer, offset, size);
					totalWritten += size;

					if (Request.ContentLength <= 0 || totalWritten < Request.ContentLength)
						return;

					requestWritten = true;
					buffer = writeBuffer.GetBuffer ();
					offset = 0;
					size = (int)totalWritten;
				} else {
					totalWritten += size;
				}
			}

			await InnerStream.WriteAsync (buffer, offset, size, cancellationToken).ConfigureAwait (false);
		}

		void CheckWriteOverflow (long contentLength, long totalWritten, long size)
		{
			if (contentLength == -1)
				return;

			long avail = contentLength - totalWritten;
			if (size > avail) {
				KillBuffer ();
				closed = true;
				var throwMe = new ProtocolViolationException (
					"The number of bytes to be written is greater than " +
					"the specified ContentLength.");
				Operation.CompleteRequestWritten (this, throwMe);
				throw throwMe;
			}
		}

		internal async Task Initialize (CancellationToken cancellationToken)
		{
			Operation.ThrowIfClosedOrDisposed (cancellationToken);

			WebConnection.Debug ($"{ME} INIT: {Operation.WriteBuffer != null}");

			if (Operation.WriteBuffer != null) {
				if (Operation.IsNtlmChallenge)
					Request.InternalContentLength = 0;
				else
					Request.InternalContentLength = Operation.WriteBuffer.Size;
			}

			await SetHeadersAsync (false, cancellationToken).ConfigureAwait (false);

			Operation.ThrowIfClosedOrDisposed (cancellationToken);

			if (Operation.WriteBuffer != null && !Operation.IsNtlmChallenge) {
				await WriteRequestAsync (cancellationToken);
				Close ();
			}
		}

		async Task SetHeadersAsync (bool setInternalLength, CancellationToken cancellationToken)
		{
			Operation.ThrowIfClosedOrDisposed (cancellationToken);

			if (headersSent)
				return;

			string method = Request.Method;
			bool no_writestream = (method == "GET" || method == "CONNECT" || method == "HEAD" ||
					      method == "TRACE");
			bool webdav = (method == "PROPFIND" || method == "PROPPATCH" || method == "MKCOL" ||
				      method == "COPY" || method == "MOVE" || method == "LOCK" ||
				      method == "UNLOCK");

			if (Operation.IsNtlmChallenge)
				no_writestream = true;

			if (setInternalLength && !no_writestream && HasWriteBuffer)
				Request.InternalContentLength = WriteBufferLength;

			bool has_content = !no_writestream && (!HasWriteBuffer || Request.ContentLength > -1);
			if (!(sendChunked || has_content || no_writestream || webdav))
				return;

			headersSent = true;
			headers = Request.GetRequestHeaders ();

			WebConnection.Debug ($"{ME} SET HEADERS: {Request.ContentLength}");

			try {
				await InnerStream.WriteAsync (headers, 0, headers.Length, cancellationToken).ConfigureAwait (false);
				var cl = Request.ContentLength;
				if (!sendChunked && cl == 0)
					requestWritten = true;
			} catch (Exception e) {
				if (e is WebException || e is OperationCanceledException)
					throw;
				throw new WebException ("Error writing headers", WebExceptionStatus.SendFailure, WebExceptionInternalStatus.RequestFatal, e);
			}
		}

		internal async Task WriteRequestAsync (CancellationToken cancellationToken)
		{
			Operation.ThrowIfClosedOrDisposed (cancellationToken);

			WebConnection.Debug ($"{ME} WRITE REQUEST: {requestWritten} {sendChunked} {allowBuffering} {HasWriteBuffer}");

			if (requestWritten)
				return;

			requestWritten = true;
			if (sendChunked || !HasWriteBuffer)
				return;

			BufferOffsetSize buffer = GetWriteBuffer ();
			if (buffer != null && !Operation.IsNtlmChallenge && Request.ContentLength != -1 && Request.ContentLength < buffer.Size) {
				closed = true;
				var throwMe = new WebException ("Specified Content-Length is less than the number of bytes to write", null,
					WebExceptionStatus.ServerProtocolViolation, null);
				Operation.CompleteRequestWritten (this, throwMe);
				throw throwMe;
			}

			await SetHeadersAsync (true, cancellationToken).ConfigureAwait (false);

			WebConnection.Debug ($"{ME} WRITE REQUEST #1: {buffer != null}");

			Operation.ThrowIfClosedOrDisposed (cancellationToken);
			if (buffer != null && buffer.Size > 0)
				await InnerStream.WriteAsync (buffer.Buffer, 0, buffer.Size, cancellationToken);

			await FinishWriting (cancellationToken);
		}

		async Task WriteChunkTrailer_inner (CancellationToken cancellationToken)
		{
			if (Interlocked.CompareExchange (ref chunkTrailerWritten, 1, 0) != 0)
				return;
			Operation.ThrowIfClosedOrDisposed (cancellationToken);
			byte[] chunk = Encoding.ASCII.GetBytes ("0\r\n\r\n");
			await InnerStream.WriteAsync (chunk, 0, chunk.Length, cancellationToken).ConfigureAwait (false);
		}

		async Task WriteChunkTrailer ()
		{
			var cts = new CancellationTokenSource ();
			try {
				cts.CancelAfter (WriteTimeout);
				var timeoutTask = Task.Delay (WriteTimeout, cts.Token);
				while (true) {
					var completion = new WebCompletionSource ();
					var oldCompletion = Interlocked.CompareExchange (ref pendingWrite, completion, null);
					if (oldCompletion == null)
						break;
					var oldWriteTask = oldCompletion.WaitForCompletion ();
					var ret = await Task.WhenAny (timeoutTask, oldWriteTask).ConfigureAwait (false);
					if (ret == timeoutTask)
						throw new WebException ("The operation has timed out.", WebExceptionStatus.Timeout);
				}

				await WriteChunkTrailer_inner (cts.Token).ConfigureAwait (false);
			} catch {
				// Intentionally eating exceptions.
			} finally {
				pendingWrite = null;
				cts.Cancel ();
				cts.Dispose ();
			}
		}

		internal void KillBuffer ()
		{
			writeBuffer = null;
		}

		public override Task<int> ReadAsync (byte[] buffer, int offset, int size, CancellationToken cancellationToken)
		{
			return Task.FromException<int> (new NotSupportedException (SR.net_writeonlystream));
		}

		protected override bool TryReadFromBufferedContent (byte[] buffer, int offset, int count, out int result) => throw new InvalidOperationException ();

		protected override void Close_internal (ref bool disposed)
		{
			WebConnection.Debug ($"{ME} CLOSE: {disposed} {requestWritten} {allowBuffering}");

			if (disposed)
				return;
			disposed = true;

			if (sendChunked) {
				// Don't use FinishWriting() here, we need to block on 'pendingWrite' to ensure that
				// any pending WriteAsync() has been completed.
				//
				// FIXME: I belive .NET simply aborts if you call Close() or Dispose() while writing,
				//        need to check this.  2017/07/17 Martin.
				WriteChunkTrailer ().Wait ();
				return;
			}

			if (!allowBuffering || requestWritten) {
				Operation.CompleteRequestWritten (this);
				return;
			}

			long length = Request.ContentLength;

			if (!sendChunked && !Operation.IsNtlmChallenge && length != -1 && totalWritten != length) {
				IOException io = new IOException ("Cannot close the stream until all bytes are written");
				closed = true;
				disposed = true;
				var throwMe = new WebException ("Request was cancelled.", WebExceptionStatus.RequestCanceled, WebExceptionInternalStatus.RequestFatal, io);
				Operation.CompleteRequestWritten (this, throwMe);
				throw throwMe;
			}

			// Commented out the next line to fix xamarin bug #1512
			//WriteRequest ();
			disposed = true;
			Operation.CompleteRequestWritten (this);
		}
	}
}