using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;
using System.Linq;

using NetHttpClient = System.Net.Http.HttpClient;
using System.Net.Http.Headers;
using System.Collections.Generic;

namespace LeanCloud.Storage.Internal
{
    public class HttpClient : IHttpClient
    {
        private static HashSet<string> HttpContentHeaders = new HashSet<string> {
            { "Allow" },
            { "Content-Disposition" },
            { "Content-Encoding" },
            { "Content-Language" },
            { "Content-Length" },
            { "Content-Location" },
            { "Content-MD5" },
            { "Content-Range" },
            { "Content-Type" },
            { "Expires" },
            { "Last-Modified" }
        };

        public HttpClient()
        {
            client = new NetHttpClient();
            client.DefaultRequestHeaders.Add("User-Agent", "LeanCloud-dotNet-SDK/" + "2.0.0");
        }

        public HttpClient(NetHttpClient client)
        {
            this.client = client;
        }

        private NetHttpClient client;

        public Task<Tuple<HttpStatusCode, string>> ExecuteAsync(HttpRequest httpRequest,
            IProgress<AVUploadProgressEventArgs> uploadProgress,
            IProgress<AVDownloadProgressEventArgs> downloadProgress,
            CancellationToken cancellationToken)
        {
            uploadProgress = uploadProgress ?? new Progress<AVUploadProgressEventArgs>();
            downloadProgress = downloadProgress ?? new Progress<AVDownloadProgressEventArgs>();

            HttpMethod httpMethod = new HttpMethod(httpRequest.Method);
            HttpRequestMessage message = new HttpRequestMessage(httpMethod, httpRequest.Uri);

            // Fill in zero-length data if method is post.
            Stream data = httpRequest.Data;
            if (httpRequest.Data == null && httpRequest.Method.ToLower().Equals("post"))
            {
                data = new MemoryStream(new byte[0]);
            }

            if (data != null)
            {
                message.Content = new StreamContent(data);
            }

            if (httpRequest.Headers != null)
            {
                foreach (var header in httpRequest.Headers)
                {
                    if (!string.IsNullOrEmpty(header.Value))
                    {
                        if (HttpContentHeaders.Contains(header.Key))
                        {
                            message.Content.Headers.Add(header.Key, header.Value);
                        }
                        else
                        {
                            message.Headers.Add(header.Key, header.Value);
                        }
                    }
                }
            }

            // Avoid aggressive caching on Windows Phone 8.1.
            message.Headers.Add("Cache-Control", "no-cache");
            message.Headers.IfModifiedSince = DateTimeOffset.UtcNow;

            // TODO: (richardross) investigate progress here, maybe there's something we're missing in order to support this.
            uploadProgress.Report(new AVUploadProgressEventArgs { Progress = 0 });

            return client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
              .ContinueWith(httpMessageTask =>
              {
                  var response = httpMessageTask.Result;

                  uploadProgress.Report(new AVUploadProgressEventArgs { Progress = 1 });

                  return response.Content.ReadAsStreamAsync().ContinueWith(streamTask =>
                  {
                      var resultStream = new MemoryStream();
                      var responseStream = streamTask.Result;

                      int bufferSize = 4096;
                      byte[] buffer = new byte[bufferSize];
                      int bytesRead = 0;
                      long totalLength = -1;
                      long readSoFar = 0;

                      try
                      {
                          totalLength = responseStream.Length;
                      }
                      catch (NotSupportedException)
                      {

                      }

                      return InternalExtensions.WhileAsync(() =>
                      {
                          return responseStream.ReadAsync(buffer, 0, bufferSize, cancellationToken).OnSuccess(readTask =>
                          {
                              bytesRead = readTask.Result;
                              return bytesRead > 0;
                          });
                      }, () =>
                      {
                          cancellationToken.ThrowIfCancellationRequested();

                          return resultStream.WriteAsync(buffer, 0, bytesRead, cancellationToken).OnSuccess(_ =>
                          {
                              cancellationToken.ThrowIfCancellationRequested();
                              readSoFar += bytesRead;

                              if (totalLength > -1)
                              {
                                  downloadProgress.Report(new AVDownloadProgressEventArgs { Progress = 1.0 * readSoFar / totalLength });
                              }
                          });
                      }).ContinueWith(_ =>
                      {
                          responseStream.Dispose();
                          return _;
                      }).Unwrap().OnSuccess(_ =>
                      {
                          // If getting stream size is not supported, then report download only once.
                          if (totalLength == -1)
                          {
                              downloadProgress.Report(new AVDownloadProgressEventArgs { Progress = 1.0 });
                          }

                          // Assume UTF-8 encoding.
                          var resultAsArray = resultStream.ToArray();
                          var resultString = Encoding.UTF8.GetString(resultAsArray, 0, resultAsArray.Length);
                          resultStream.Dispose();
                          return new Tuple<HttpStatusCode, string>(response.StatusCode, resultString);
                      });
                  });
              }).Unwrap().Unwrap();
        }
    }
}
