//
// System.Net.WebClient
//
// Authors:
// 	Lawrence Pit (loz@cable.a2000.nl)
//	Gonzalo Paniagua Javier (gonzalo@ximian.com)
//
// (c) 2003 Ximian, Inc. (http://www.ximian.com)
//

using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Text;

namespace System.Net 
{
	[ComVisible(true)]
	public sealed class WebClient : Component
	{
		static readonly string urlEncodedCType = "application/x-www-form-urlencoded";
		static byte [] hexBytes;
		ICredentials credentials;
		WebHeaderCollection headers;
		WebHeaderCollection responseHeaders;
		Uri baseAddress;
		string baseString;
		NameValueCollection queryString;
	
		// Constructors
		static WebClient ()
		{
			hexBytes = new byte [16];
			int index = 0;
			for (int i = '0'; i <= '9'; i++, index++)
				hexBytes [index] = (byte) i;

			for (int i = 'A'; i <= 'F'; i++, index++)
				hexBytes [index] = (byte) i;
		}
		
		public WebClient ()
		{
		}
		
		// Properties
		
		public string BaseAddress {
			get {
				if (baseString == null) {
					if (baseAddress == null)
						return "";
				}

				baseString = baseAddress.ToString ();
				return baseString;
			}
			
			set {
				if (value == null || value == "") {
					baseAddress = null;
				} else {
					baseAddress = new Uri (value);
				}
			}
		}
		
		public ICredentials Credentials {
			get { return credentials; }
			set { credentials = value; }
		}
		
		public WebHeaderCollection Headers {
			get {
				if (headers == null)
					headers = new WebHeaderCollection ();

				return headers;
			}
			set { headers = value; }
		}
		
		public NameValueCollection QueryString {
			get {
				if (queryString == null)
					queryString = new NameValueCollection ();

				return queryString;
			}
			set { queryString = value; }
		}
		
		public WebHeaderCollection ResponseHeaders {
			get { return responseHeaders; }
		}

		// Methods
		
		public byte [] DownloadData (string address)
		{
			WebRequest request = SetupRequest (address);
			WebResponse response = request.GetResponse ();
			Stream st = ProcessResponse (response);
			return ReadAll (st, (int) response.ContentLength);
		}
		
		public void DownloadFile (string address, string fileName)
		{
			WebRequest request = SetupRequest (address);
			WebResponse response = request.GetResponse ();
			Stream st = ProcessResponse (response);

			int cLength = (int) response.ContentLength;
			int length = (cLength <= -1 || cLength > 8192) ? 8192 : cLength;
			byte [] buffer = new byte [length];
			FileStream f = new FileStream (fileName, FileMode.CreateNew);

			int nread = 0;
			while ((nread = st.Read (buffer, 0, length)) != 0)
				f.Write (buffer, 0, nread);

			f.Close ();
		}
		
		public Stream OpenRead (string address)
		{
			WebRequest request = SetupRequest (address);
			WebResponse response = request.GetResponse ();
			return ProcessResponse (response);
		}
		
		public Stream OpenWrite (string address)
		{
			return OpenWrite (address, "POST");
		}
		
		public Stream OpenWrite (string address, string method)
		{
			WebRequest request = SetupRequest (address, method);
			return request.GetRequestStream ();
		}
				
		public byte [] UploadData (string address, byte [] data)
		{
			return UploadData (address, "POST", data);
		}
		
		public byte [] UploadData (string address, string method, byte [] data)
		{
			if (data == null)
				throw new ArgumentNullException ("data");

			int contentLength = data.Length;
			WebRequest request = SetupRequest (address, method, contentLength);
			using (Stream stream = request.GetRequestStream ()) {
				stream.Write (data, 0, contentLength);
			}

			WebResponse response = request.GetResponse ();
			Stream st = ProcessResponse (response);
			return ReadAll (st, (int) response.ContentLength);
		}
		
		public byte [] UploadFile (string address, string fileName)
		{
			return UploadFile (address, "POST", fileName);
		}
		
		public byte [] UploadFile (string address, string method, string fileName)
		{
			string fileCType = Headers ["Content-Type"];
			if (fileCType != null) {
				string lower = fileCType.ToLower ();
				if (lower.StartsWith ("multipart/"))
					throw new WebException ("Content-Type cannot be set to a multipart" +
								" type for this request.");
			} else {
				fileCType = "application/octet-stream";
			}

			string boundary = "------------" + DateTime.Now.Ticks.ToString ("x");
			Headers ["Content-Type"] = String.Format ("multipart/form-data; boundary={0}", boundary);
			WebRequest request = SetupRequest (address, method);
			Stream reqStream = null;
			Stream fStream = null;
			byte [] resultBytes = null;

			try {
				fStream = File.OpenRead (fileName);
				reqStream = request.GetRequestStream ();
				byte [] realBoundary = Encoding.ASCII.GetBytes ("--" + boundary + "\r\n");
				reqStream.Write (realBoundary, 0, realBoundary.Length);
				string partHeaders = String.Format ("Content-Disposition: form-data; " +
								    "name=\"file\"; filename=\"{0}\"\r\n" +
								    "Content-Type: {1}\r\n\r\n",
								    Path.GetFileName (fileName), fileCType);

				byte [] partHeadersBytes = Encoding.UTF8.GetBytes (partHeaders);
				reqStream.Write (partHeadersBytes, 0, partHeadersBytes.Length);
				int nread;
				byte [] buffer = new byte [4096];
				while ((nread = fStream.Read (buffer, 0, 4096)) != 0)
					reqStream.Write (buffer, 0, nread);

				reqStream.WriteByte ((byte) '\r');
				reqStream.WriteByte ((byte) '\n');
				reqStream.Write (realBoundary, 0, realBoundary.Length);
				reqStream.Close ();
				reqStream = null;
				WebResponse response = request.GetResponse ();
				Stream st = ProcessResponse (response);
				resultBytes = ReadAll (st, (int) response.ContentLength);
			} catch (WebException) {
				throw;
			} catch (Exception e) {
				throw new WebException ("Error uploading file.", e);
			} finally {
				if (fStream != null)
					fStream.Close ();

				if (reqStream != null)
					reqStream.Close ();
			}
			
			return resultBytes;	
		}
		
		public byte[] UploadValues (string address, NameValueCollection data)
		{
			return UploadValues (address, "POST", data);
		}
		
		public byte[] UploadValues (string address, string method, NameValueCollection data)
		{
			if (data == null)
				throw new ArgumentNullException ("data"); // MS throws a nullref

			string cType = Headers ["Content-Type"];
			if (cType != null && String.Compare (cType, urlEncodedCType, true) != 0)
				throw new WebException ("Content-Type header cannot be changed from its default " +
							"value for this request.");

			Headers ["Content-Type"] = urlEncodedCType;
			WebRequest request = SetupRequest (address, method);
			Stream rqStream = request.GetRequestStream ();
			MemoryStream tmpStream = new MemoryStream ();
			foreach (string key in data) {
				byte [] bytes = Encoding.ASCII.GetBytes (key);
				UrlEncodeAndWrite (tmpStream, bytes);
				tmpStream.WriteByte ((byte) '=');
				bytes = Encoding.ASCII.GetBytes (data [key]);
				UrlEncodeAndWrite (tmpStream, bytes);
				tmpStream.WriteByte ((byte) '&');
			}

			int length = (int) tmpStream.Length;
			if (length > 0)
				tmpStream.SetLength (--length); // remove trailing '&'

			tmpStream.WriteByte ((byte) '\r');
			tmpStream.WriteByte ((byte) '\n');

			byte [] buf = tmpStream.GetBuffer ();
			rqStream.Write (buf, 0, length + 2);
			rqStream.Close ();
			tmpStream.Close ();

			WebResponse response = request.GetResponse ();
			Stream st = ProcessResponse (response);
			return ReadAll (st, (int) response.ContentLength);
		}

		Uri MakeUri (string path)
		{
			string query = null;
			if (queryString != null && queryString.Count != 0) {
				// This is not the same as UploadValues, because these 'keys' are not
				// urlencoded here.
				StringBuilder sb = new StringBuilder ();
				sb.Append ('?');
				foreach (string key in queryString)
					sb.AppendFormat ("{0}={1}&", key, UrlEncode (queryString [key]));

				if (sb.Length != 0) {
					sb.Length--; // remove trailing '&'
					query = sb.ToString ();
				}
			}
			

			if (baseAddress == null && query == null) {
				try {
					return new Uri (path);
				}
				catch (System.UriFormatException) {
					if ((path[0] == Path.DirectorySeparatorChar) || (path[1] == ':' && Char.ToLower(path[0]) > 'a' && Char.ToLower(path[0]) < 'z')) {
						return new Uri ("file://" + path);
					}
					else {
						return new Uri ("file://" + Environment.CurrentDirectory + Path.DirectorySeparatorChar + path);
					}
				}
			}

			if (baseAddress == null)
				return new Uri (path + query, (query != null));

			if (query == null)
				return new Uri (baseAddress, path);

			return new Uri (baseAddress, path + query, (query != null));
		}
		
		WebRequest SetupRequest (string address)
		{
			Uri uri = MakeUri (address);
			WebRequest request = WebRequest.Create (uri);
			request.Credentials = credentials;

			// Special headers. These are properties of HttpWebRequest.
			// What do we do with other requests differnt from HttpWebRequest?
			if (headers != null && headers.Count != 0 && (request is HttpWebRequest)) {
				HttpWebRequest req = (HttpWebRequest) request;
				string expect = headers ["Expect"];
				string contentType = headers ["Content-Type"];
				string accept = headers ["Accept"];
				string connection = headers ["Connection"];
				string userAgent = headers ["User-Agent"];
				string referer = headers ["Referer"];
				headers.RemoveInternal ("Expect");
				headers.RemoveInternal ("Content-Type");
				headers.RemoveInternal ("Accept");
				headers.RemoveInternal ("Connection");
				headers.RemoveInternal ("Referer");
				headers.RemoveInternal ("User-Agent");
				request.Headers = headers;

				if (expect != null && expect != "")
					req.Expect = expect;

				if (accept != null && accept != "")
					req.Accept = accept;

				if (contentType != null && contentType != "")
					req.ContentType = contentType;

				if (connection != null && connection != "")
					req.Connection = connection;

				if (userAgent != null && userAgent != "")
					req.UserAgent = userAgent;

				if (referer != null && referer != "")
					req.Referer = referer;
			}

			responseHeaders = null;
			return request;
		}

		WebRequest SetupRequest (string address, string method)
		{
			WebRequest request = SetupRequest (address);
			request.Method = method;
			return request;
		}

		WebRequest SetupRequest (string address, string method, int contentLength)
		{
			WebRequest request = SetupRequest (address, method);
			request.ContentLength = contentLength;
			return request;
		}

		Stream ProcessResponse (WebResponse response)
		{
			responseHeaders = response.Headers;
			return response.GetResponseStream ();
		}

		static byte [] ReadAll (Stream stream, int length)
		{
			MemoryStream ms = null;
			
			bool nolength = (length == -1);
			int size = ((nolength) ? 8192 : length);
			if (nolength)
				ms = new MemoryStream ();

			int nread = 0;
			int offset = 0;
			byte [] buffer = new byte [size];
			while ((nread = stream.Read (buffer, offset, size)) != 0) {
				if (nolength) {
					ms.Write (buffer, 0, nread);
				} else {
					offset += nread;
					size -= nread;
				}
			}

			if (nolength)
				return ms.ToArray ();

			return buffer;
		}

		string UrlEncode (string str)
		{
			StringBuilder result = new StringBuilder ();

			int len = str.Length;
			for (int i = 0; i < len; i++) {
				char c = str [i];
				if (c == ' ')
					result.Append ('+');
				else if ((c < '0' && c != '-' && c != '.') ||
					 (c < 'A' && c > '9') ||
					 (c > 'Z' && c < 'a' && c != '_') ||
					 (c > 'z')) {
					result.Append ('%');
					int idx = ((int) c) >> 4;
					result.Append ((char) hexBytes [idx]);
					idx = ((int) c) & 0x0F;
					result.Append ((char) hexBytes [idx]);
				} else {
					result.Append (c);
				}
			}

			return result.ToString ();
		}

		static void UrlEncodeAndWrite (Stream stream, byte [] bytes)
		{
			if (bytes == null)
				return;

			int len = bytes.Length;
			if (len == 0)
				return;

			for (int i = 0; i < len; i++) {
				char c = (char) bytes [i];
				if (c == ' ')
					stream.WriteByte ((byte) '+');
				else if ((c < '0' && c != '-' && c != '.') ||
					 (c < 'A' && c > '9') ||
					 (c > 'Z' && c < 'a' && c != '_') ||
					 (c > 'z')) {
					stream.WriteByte ((byte) '%');
					int idx = ((int) c) >> 4;
					stream.WriteByte (hexBytes [idx]);
					idx = ((int) c) & 0x0F;
					stream.WriteByte (hexBytes [idx]);
				} else {
					stream.WriteByte ((byte) c);
				}
			}
		}
	}
}

