using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using Newtonsoft.Json;

namespace DataUtils
{
	[ComVisible (true)]
	[InterfaceType (ComInterfaceType.InterfaceIsDual)]
	public interface IHttpResponse: IDisposable
	{
		int Status { get; }              // 兼容旧版，等同于 StatusCode
		int StatusCode { get; }
		string StatusText { get; }       // 等同于 StatusDescription
		string StatusDescription { get; }
		string ResponseUrl { get; }
		Uri ResponseUri { get; }
		string ContentType { get; }
		long ContentLength { get; }      // 注意：JS 中 Number 可表示 2^53 以内整数
		string CharacterSet { get; }
		string ContentEncoding { get; }
		DateTime LastModified { get; }
		string Method { get; }           // 原始请求方法 (GET, POST...)
		Version ProtocolVersion { get; } // 如 1.1，JS 中可能转为字符串
		bool IsFromCache { get; }
		bool IsMutuallyAuthenticated { get; }
		string Text ();
		_I_Enumerable Bytes ();
	}
	[ComVisible (true)]
	[ClassInterface (ClassInterfaceType.AutoDual)]
	public class HttpResponse: IHttpResponse, IDisposable
	{
		private readonly byte [] _data;
		private readonly Dictionary<string, string> _headersDict;
		private bool _disposed = false;
		public int Status => StatusCode;
		public int StatusCode { get; private set; }
		public string StatusText => StatusDescription;
		public string StatusDescription { get; private set; }
		public string ResponseUrl => ResponseUri?.ToString ();
		public Uri ResponseUri { get; private set; }
		public string ContentType { get; private set; }
		public long ContentLength { get; private set; }
		public string CharacterSet { get; private set; }
		public string ContentEncoding { get; private set; }
		public DateTime LastModified { get; private set; }
		public string Method { get; private set; }
		public Version ProtocolVersion { get; private set; }
		public bool IsFromCache { get; private set; }
		public bool IsMutuallyAuthenticated { get; private set; }
		public HttpResponse (HttpWebResponse response)
		{
			if (response == null) throw new ArgumentNullException (nameof (response));
			using (var stream = response.GetResponseStream ())
			using (var ms = new MemoryStream ())
			{
				stream.CopyTo (ms);
				_data = ms.ToArray ();
			}
			StatusCode = (int)response.StatusCode;
			StatusDescription = response.StatusDescription;
			ResponseUri = response.ResponseUri;
			Method = response.Method;
			ProtocolVersion = new Version ((ushort)response.ProtocolVersion.Major, (ushort)response.ProtocolVersion.Minor, (ushort)response.ProtocolVersion.Build, (ushort)response.ProtocolVersion.Revision);
			IsFromCache = response.IsFromCache;
			IsMutuallyAuthenticated = response.IsMutuallyAuthenticated;
			ContentType = response.ContentType ?? "";
			ContentLength = response.ContentLength;
			CharacterSet = response.CharacterSet ?? "";
			ContentEncoding = response.ContentEncoding ?? "";
			LastModified = response.LastModified;
			_headersDict = new Dictionary<string, string> (StringComparer.OrdinalIgnoreCase);
			foreach (string key in response.Headers.AllKeys)
			{
				if (!string.IsNullOrEmpty (key))
					_headersDict [key] = response.Headers [key];
			}
		}
		public HttpResponse (int statusCode, string statusDescription, string responseUrl, byte [] data, Dictionary<string, string> headers)
		{
			StatusCode = statusCode;
			StatusDescription = statusDescription ?? "";
			ResponseUri = string.IsNullOrEmpty (responseUrl) ? null : new Uri (responseUrl);
			_data = data ?? new byte [0];
			_headersDict = headers ?? new Dictionary<string, string> ();
			ContentType = GetHeader ("Content-Type") ?? "";
			ContentLength = _data.Length;
			CharacterSet = "";
			ContentEncoding = "";
			LastModified = DateTime.MinValue;
			Method = "";
			ProtocolVersion = new Version (0, 0);
			IsFromCache = false;
			IsMutuallyAuthenticated = false;
		}
		public string GetHeader (string sName)
		{
			if (string.IsNullOrEmpty (sName)) return null;
			try
			{
				return _headersDict [sName];
			}
			catch { return null; }
		}
		public string GetHeadersToJson ()
		{
			return JsonConvert.SerializeObject (_headersDict);
		}
		public string Text ()
		{
			string charset = CharacterSet;
			Encoding enc = Encoding.UTF8;
			if (!string.IsNullOrEmpty (charset))
			{
				try { enc = Encoding.GetEncoding (charset); }
				catch { /* 保持 UTF-8 */ }
			}
			return enc.GetString (_data);
		}
		public _I_Enumerable Bytes ()
		{
			var list = new List<object> ();
			foreach (byte b in _data)
				list.Add (b);
			return new _I_List (list);
		}
		public void Dispose ()
		{
			Dispose (true);
			GC.SuppressFinalize (this);
		}
		protected virtual void Dispose (bool disposing)
		{
			if (!_disposed)
			{
				_disposed = true;
			}
		}
	}
	[ComVisible (true)]
	[ClassInterface (ClassInterfaceType.AutoDual)]
	public class HttpRequest: IDisposable
	{
		private string _method;
		private string _url;
		private Dictionary<string, string> _headers = new Dictionary<string, string> ();
		public int Timeout { get; set; } = 100000;          // 毫秒，默认 100 秒
		public int ReadWriteTimeout { get; set; } = 300000; // 读写超时，默认 5 分钟
		public bool AllowAutoRedirect { get; set; } = true;
		public bool AllowWriteStreamBuffering { get; set; } = true;
		public bool KeepAlive { get; set; } = true;
		public int MaximumAutomaticRedirections { get; set; } = 50;
		public string UserAgent
		{
			get { return GetHeader ("User-Agent"); }
			set { SetHeader ("User-Agent", value); }
		}
		public string Referer
		{
			get { return GetHeader ("Referer"); }
			set { SetHeader ("Referer", value); }
		}
		public string ContentType
		{
			get { return GetHeader ("Content-Type"); }
			set { SetHeader ("Content-Type", value); }
		}
		public string Accept
		{
			get { return GetHeader ("Accept"); }
			set { SetHeader ("Accept", value); }
		}
		public IWebProxy Proxy { get; set; } = null;
		public CookieContainer CookieContainer { get; set; } = null;
		private string _httpver = "1.1";
		public string ProtocolVersion
		{
			get { return _httpver; }
			set { _httpver = value; }
		}
		public System.Version ProtVer
		{
			get
			{
				switch (_httpver)
				{
					case "1.0": return HttpVersion.Version10;
					default:
					case "1.1": return HttpVersion.Version11;
				}
			}
		}
		public bool PreAuthenticate { get; set; } = false;
		public ICredentials Credentials { get; set; } = null;
		public bool AutomaticDecompression { get; set; } = false;
		public Action<long, long> UploadProgressCallback { get; set; } = null;
		public void Open (string sMethod, string sUrl)
		{
			_method = sMethod;
			_url = sUrl;
		}
		public void SetHeader (string sName, string sValue) => _headers [sName] = sValue;
		public string GetHeader (string sName) => _headers [sName];
		public string GetHeadersToJson () => JsonConvert.SerializeObject (_headers);
		public void RemoveHeader (string sName) => _headers.Remove (sName);
		public void ClearHeader () => _headers.Clear ();
		public IHttpResponse Send (string sBody, string encoding)
		{
			var req = (HttpWebRequest)WebRequest.Create (_url);
			req.Method = _method;
			req.Timeout = Timeout;
			req.ReadWriteTimeout = ReadWriteTimeout;
			req.AllowAutoRedirect = AllowAutoRedirect;
			req.AllowWriteStreamBuffering = AllowWriteStreamBuffering;
			req.KeepAlive = KeepAlive;
			req.MaximumAutomaticRedirections = MaximumAutomaticRedirections;
			if (Proxy != null) req.Proxy = Proxy;
			if (CookieContainer != null) req.CookieContainer = CookieContainer;
			req.ProtocolVersion = ProtVer;
			req.PreAuthenticate = PreAuthenticate;
			if (Credentials != null) req.Credentials = Credentials;
			if (AutomaticDecompression)
				req.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
			if (_headers.ContainsKey ("User-Agent"))
				req.UserAgent = _headers ["User-Agent"];
			foreach (var h in _headers)
			{
				if (string.Equals (h.Key, "User-Agent", StringComparison.OrdinalIgnoreCase))
					continue;
				if (string.Equals (h.Key, "Content-Type", StringComparison.OrdinalIgnoreCase))
				{
					string ct = h.Value;
					if (!string.IsNullOrEmpty (sBody) && !ct.Contains ("charset"))
					{
						Encoding enc = Encoding.GetEncoding (encoding);
						ct = ct + "; charset=" + enc.WebName;
					}
					req.ContentType = ct;
				}
				else if (string.Equals (h.Key, "Accept", StringComparison.OrdinalIgnoreCase))
				{
					req.Accept = h.Value;
				}
				else if (string.Equals (h.Key, "Referer", StringComparison.OrdinalIgnoreCase))
				{
					req.Referer = h.Value;
				}
				else
				{
					req.Headers [h.Key] = h.Value;
				}
			}

			// 如果没有显式设置 Content-Type 且是 POST/PUT 且有请求体，设置默认值
			bool hasContentType = _headers.Keys.Any (k => string.Equals (k, "Content-Type", StringComparison.OrdinalIgnoreCase));
			if (!hasContentType && (string.Equals (_method, "POST", StringComparison.OrdinalIgnoreCase) ||
									string.Equals (_method, "PUT", StringComparison.OrdinalIgnoreCase)) &&
				!string.IsNullOrEmpty (sBody))
			{
				Encoding enc = Encoding.GetEncoding (encoding);
				req.ContentType = "application/x-www-form-urlencoded; charset=" + enc.WebName;
			}

			// 写入请求体
			if (!string.IsNullOrEmpty (sBody))
			{
				Encoding enc = Encoding.GetEncoding (encoding);
				byte [] bytes = enc.GetBytes (sBody);
				req.ContentLength = bytes.Length;
				using (var stream = req.GetRequestStream ())
				{
					if (UploadProgressCallback != null)
					{
						int totalWritten = 0;
						int bufferSize = 8192;
						for (int offset = 0; offset < bytes.Length; offset += bufferSize)
						{
							int chunkSize = Math.Min (bufferSize, bytes.Length - offset);
							stream.Write (bytes, offset, chunkSize);
							totalWritten += chunkSize;
							UploadProgressCallback (totalWritten, bytes.Length);
						}
					}
					else
					{
						stream.Write (bytes, 0, bytes.Length);
					}
				}
			}
			using (var res = (HttpWebResponse)req.GetResponse ())
			{
				return new HttpResponse (res);
			}
		}
		public void SendAsync (string sBody, string encoding, object pfResolve, object pfReject)
		{
			System.Threading.ThreadPool.QueueUserWorkItem (delegate
			{
				try
				{
					JsUtils.Call (pfResolve, Send (sBody, encoding));
				}
				catch (Exception ex)
				{
					JsUtils.Call (pfReject, new _I_Exception (ex));
				}
			});
		}
		public void Dispose () { }
	}
	[ComVisible (true)]
	[ClassInterface (ClassInterfaceType.AutoDual)]
	public class _I_Http
	{
		public HttpRequest CreateHttpRequest () => new HttpRequest ();
	}
	[ComVisible (true)]
	[ClassInterface (ClassInterfaceType.AutoDual)]
	public class _I_Web
	{
		public _I_Http Http => new _I_Http ();
	}
}
