/++
	OBSOLETE: Old version of my http implementation. Do not use this, instead use [arsd.http2].

	I no longer work on this, use http2.d instead.
+/
/*deprecated*/ module arsd.http; // adrdox apparently loses the comment above with deprecated, i need to fix that over there.

import std.socket;

// FIXME: check Transfer-Encoding: gzip always

version(with_openssl) {
	pragma(lib, "crypto");
	pragma(lib, "ssl");
}

ubyte[] getBinary(string url, string[string] cookies = null) {
	auto hr = httpRequest("GET", url, null, cookies);
	if(hr.code != 200)
		throw new Exception(format("HTTP answered %d instead of 200 on %s", hr.code, url));
	return hr.content;
}

/**
	Gets a textual document, ignoring headers. Throws on non-text or error.
*/
string get(string url, string[string] cookies = null) {
	auto hr = httpRequest("GET", url, null, cookies);
	if(hr.code != 200)
		throw new Exception(format("HTTP answered %d instead of 200 on %s", hr.code, url));
	if(hr.contentType.indexOf("text/") == -1)
		throw new Exception(hr.contentType ~ " is bad content for conversion to string");
	return cast(string) hr.content;

}

static import std.uri;

string post(string url, string[string] args, string[string] cookies = null) {
	string content;

	foreach(name, arg; args) {
		if(content.length)
			content ~= "&";
		content ~= std.uri.encode(name) ~ "=" ~ std.uri.encode(arg);
	}

	auto hr = httpRequest("POST", url, cast(ubyte[]) content, cookies, ["Content-Type: application/x-www-form-urlencoded"]);
	if(hr.code != 200)
		throw new Exception(format("HTTP answered %d instead of 200", hr.code));
	if(hr.contentType.indexOf("text/") == -1)
		throw new Exception(hr.contentType ~ " is bad content for conversion to string");

	return cast(string) hr.content;
}

struct HttpResponse {
	int code;
	string contentType;
	string[string] cookies;
	string[] headers;
	ubyte[] content;
}

import std.string;
static import std.algorithm;
import std.conv;

struct UriParts {
	string original;
	string method;
	string host;
	ushort port;
	string path;

	bool useHttps;

	this(string uri) {
		original = uri;

		if(uri[0 .. 8] == "https://")
			useHttps = true;
		else
		if(uri[0..7] != "http://")
			throw new Exception("You must use an absolute, http or https URL.");

		version(with_openssl) {} else
		if(useHttps)
			throw new Exception("openssl support not compiled in try -version=with_openssl");

		int start = useHttps ? 8 : 7;

		auto posSlash = uri[start..$].indexOf("/");
		if(posSlash != -1)
			posSlash += start;

		if(posSlash == -1)
			posSlash = uri.length;

		auto posColon = uri[start..$].indexOf(":");
		if(posColon != -1)
			posColon += start;

		if(useHttps)
			port = 443;
		else
			port = 80;

		if(posColon != -1 && posColon < posSlash) {
			host = uri[start..posColon];
			port = to!ushort(uri[posColon+1..posSlash]);
		} else
			host = uri[start..posSlash];

		path = uri[posSlash..$];
		if(path == "")
			path = "/";
	}
}

HttpResponse httpRequest(string method, string uri, const(ubyte)[] content = null, string[string] cookies = null, string[] headers = null) {
	import std.socket;

	auto u = UriParts(uri);
	// auto f = openNetwork(u.host, u.port);
	auto f = new TcpSocket();
	f.connect(new InternetAddress(u.host, u.port));

	void delegate(string) write = (string d) {
		f.send(d);
	};

	char[4096] readBuffer; // rawRead actually blocks until it can fill up the whole buffer... which is broken as far as http goes so one char at a time i guess. slow lol
	char[] delegate() read = () {
		size_t num = f.receive(readBuffer);
		return readBuffer[0..num];
	};

	version(with_openssl) {
		import deimos.openssl.ssl;
		SSL* ssl;
		SSL_CTX* ctx;
		if(u.useHttps) {
			void sslAssert(bool ret){
				if (!ret){
					throw new Exception("SSL_ERROR");
				}
			}
			SSL_library_init();
			OpenSSL_add_all_algorithms();
			SSL_load_error_strings();

			ctx = SSL_CTX_new(SSLv3_client_method());
			sslAssert(!(ctx is null));

			ssl = SSL_new(ctx);
			SSL_set_fd(ssl, f.handle);
			sslAssert(SSL_connect(ssl) != -1);

			write = (string d) {
				SSL_write(ssl, d.ptr, cast(uint)d.length);
			};

			read = () {
				auto len = SSL_read(ssl, readBuffer.ptr, readBuffer.length);
				return readBuffer[0 .. len];
			};
		}
	}


	HttpResponse response = doHttpRequestOnHelpers(write, read, method, uri, content, cookies, headers, u.useHttps);

	version(with_openssl) {
		if(u.useHttps) {
			SSL_free(ssl);
			SSL_CTX_free(ctx);
		}
	}

	return response;
}

/**
	Executes a generic http request, returning the full result. The correct formatting
	of the parameters are the caller's responsibility. Content-Length is added automatically,
	but YOU must give Content-Type!
*/
HttpResponse doHttpRequestOnHelpers(void delegate(string) write, char[] delegate() read, string method, string uri, const(ubyte)[] content = null, string[string] cookies = null, string[] headers = null, bool https = false)
	in {
		assert(method == "POST" || method == "GET");
	}
do {
	auto u = UriParts(uri);





	write(format("%s %s HTTP/1.1\r\n", method, u.path));
	write(format("Host: %s\r\n", u.host));
	write(format("Connection: close\r\n"));
	if(content !is null)
		write(format("Content-Length: %d\r\n", content.length));

	if(cookies !is null) {
		string cookieHeader = "Cookie: ";
		bool first = true;
		foreach(k, v; cookies) {
			if(first)
				first = false;
			else
				cookieHeader ~= "; ";
			cookieHeader ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v);
		}

		write(format("%s\r\n", cookieHeader));
	}

	if(headers !is null)
		foreach(header; headers)
			write(format("%s\r\n", header));
	write("\r\n");
	if(content !is null)
		write(cast(string) content);


	string buffer;

	string readln() {
		auto idx = buffer.indexOf("\r\n");
		if(idx == -1) {
			auto more = read();
			if(more.length == 0) { // end of file or something
				auto ret = buffer;
				buffer = null;
				return ret;
			}
			buffer ~= more;
			return readln();
		}
		auto ret = buffer[0 .. idx + 2]; // + the \r\n
		if(idx + 2 < buffer.length)
			buffer = buffer[idx + 2 .. $];
		else
			buffer = null;
		return ret;
	}

	HttpResponse hr;
 cont:
	string l = readln();
	if(l[0..9] != "HTTP/1.1 ")
		throw new Exception("Not talking to a http server");

	hr.code = to!int(l[9..12]); // HTTP/1.1 ### OK

	if(hr.code == 100) { // continue
		do {
			l = readln();
		} while(l.length > 1);

		goto cont;
	}

	bool chunked = false;

	auto line = readln();
	while(line.length) {
		if(line.strip.length == 0)
			break;
		hr.headers ~= line;
		if(line.startsWith("Content-Type: "))
			hr.contentType = line[14..$-1];
		if(line.startsWith("Set-Cookie: ")) {
			auto hdr = line["Set-Cookie: ".length .. $-1];
			auto semi = hdr.indexOf(";");
			if(semi != -1)
				hdr = hdr[0 .. semi];

			auto equal = hdr.indexOf("=");
			string name, value;
			if(equal == -1) {
				name = hdr;
				// doesn't this mean erase the cookie?
			} else {
				name = hdr[0 .. equal];
				value = hdr[equal + 1 .. $];
			}

			name = std.uri.decodeComponent(name);
			value = std.uri.decodeComponent(value);

			hr.cookies[name] = value;
		}
		if(line.startsWith("Transfer-Encoding: chunked"))
			chunked = true;
		line = readln();
	}

	// there might be leftover stuff in the line buffer
	ubyte[] response = cast(ubyte[]) buffer.dup;
	auto part = read();
	while(part.length) {
		response ~= part;
		part = read();
	}

	if(chunked) {
		// read the hex length, stopping at a \r\n, ignoring everything between the new line but after the first non-valid hex character
		// read binary data of that length. it is our content
		// repeat until a zero sized chunk
		// then read footers as headers.

		int state = 0;
		int size;
		int start = 0;
		for(int a = 0; a < response.length; a++) {
			final switch(state) {
				case 0: // reading hex
					char c = response[a];
					if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
						// just keep reading
					} else {
						int power = 1;
						size = 0;
						for(int b = a-1; b >= start; b--) {
							char cc = response[b];
							if(cc >= 'a' && cc <= 'z')
								cc -= 0x20;
							int val = 0;
							if(cc >= '0' && cc <= '9')
								val = cc - '0';
							else
								val = cc - 'A' + 10;

							size += power * val;
							power *= 16;
						}
						state++;
						continue;
					}
				break;
				case 1: // reading until end of line
					char c = response[a];
					if(c == '\n') {
						if(size == 0)
							state = 3;
						else
							state = 2;
					}
				break;
				case 2: // reading data
					hr.content ~= response[a..a+size];
					a += size;
					a+= 1; // skipping a 13 10
					start = a + 1;
					state = 0;
				break;
				case 3: // reading footers
					goto done; // FIXME
			}
		}
	} else
		hr.content = response;
	done:

	return hr;
}


/*
void main(string args[]) {
	write(post("http://arsdnet.net/bugs.php", ["test" : "hey", "again" : "what"]));
}
*/

version(none):

struct Url {
	string url;
}

struct BasicAuth {
	string username;
	string password;
}

/*
	When you send something, it creates a request
	and sends it asynchronously. The request object

	auto request = new HttpRequest();
	// set any properties here

	// synchronous usage
	auto reply = request.perform();

	// async usage, type 1:
	request.send();
	request2.send();

	// wait until the first one is done, with the second one still in-flight
	auto response = request.waitForCompletion();


	// async usage, type 2:
	request.onDataReceived = (HttpRequest hr) {
		if(hr.state == HttpRequest.State.complete) {
			// use hr.responseData
		}
	};
	request.send(); // send, using the callback

	// before terminating, be sure you wait for your requests to finish!

	request.waitForCompletion();

*/

class HttpRequest {
	private static {
		// we manage the actual connections. When a request is made on a particular
		// host, we try to reuse connections. We may open more than one connection per
		// host to do parallel requests.
		//
		// The key is the *domain name*. Multiple domains on the same address will have separate connections.
		Socket[][string] socketsPerHost;

		// only one request can be active on a given socket (at least HTTP < 2.0) so this is that
		HttpRequest[Socket] activeRequestOnSocket;
		HttpRequest[] pending; // and these are the requests that are waiting

		SocketSet readSet;


		void advanceConnections() {
			if(readSet is null)
				readSet = new SocketSet();

			// are there pending requests? let's try to send them

			readSet.reset();

			// active requests need to be read or written to
			foreach(sock, request; activeRequestOnSocket)
				readSet.add(sock);

			// check the other sockets just for EOF, if they close, take them out of our list,
			// we'll reopen if needed upon request.

			auto got = Socket.select(readSet, writeSet, null, 10.seconds /* timeout */);
			if(got == 0) /* timeout */
				{}
			else
			if(got == -1) /* interrupted */
				{}
			else /* ready */
				{}

			// call select(), do what needs to be done
			// no requests are active, send the ones pending connection now
			// we've completed a request, are there any more pending connection? if so, send them now

			auto readSet = new SocketSet();
		}
	}

	this() {
		addConnection(this);
	}

	~this() {
		removeConnection(this);
	}

	HttpResponse responseData;
	HttpRequestParameters parameters;
	private HttpClient parentClient;

	size_t bodyBytesSent;
	size_t bodyBytesReceived;

	State state;
	/// Called when data is received. Check the state to see what data is available.
	void delegate(AsynchronousHttpRequest) onDataReceived;

	enum State {
		/// The request has not yet been sent
		unsent,

		/// The send() method has been called, but no data is
		/// sent on the socket yet because the connection is busy.
		pendingAvailableConnection,

		/// The headers are being sent now
		sendingHeaders,

		/// The body is being sent now
		sendingBody,

		/// The request has been sent but we haven't received any response yet
		waitingForResponse,

		/// We have received some data and are currently receiving headers
		readingHeaders,

		/// All headers are available but we're still waiting on the body
		readingBody,

		/// The request is complete.
		complete,

		/// The request is aborted, either by the abort() method, or as a result of the server disconnecting
		aborted
	}

	/// Sends now and waits for the request to finish, returning the response.
	HttpResponse perform() {
		send();
		return waitForCompletion();
	}

	/// Sends the request asynchronously.
	void send() {
		if(state != State.unsent && state != State.aborted)
			return; // already sent

		responseData = HttpResponse.init;
		bodyBytesSent = 0;
		bodyBytesReceived = 0;
		state = State.pendingAvailableConnection;

		HttpResponse.advanceConnections();
	}


	/// Waits for the request to finish or timeout, whichever comes furst.
	HttpResponse waitForCompletion() {
		while(state != State.aborted && state != State.complete)
			HttpResponse.advanceConnections();
		return responseData;
	}

	/// Aborts this request.
	/// Due to the nature of the HTTP protocol, aborting one request will result in all subsequent requests made on this same connection to be aborted as well.
	void abort() {
		parentClient.close();
	}
}

struct HttpRequestParameters {
	Duration timeout;

	// debugging
	bool useHttp11 = true;
	bool acceptGzip = true;

	// the request itself
	HttpVerb method;
	string host;
	string uri;

	string userAgent;

	string[string] cookies;

	string[] headers; /// do not duplicate host, content-length, content-type, or any others that have a specific property

	string contentType;
	ubyte[] bodyData;
}

interface IHttpClient {

}

enum HttpVerb { GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, CONNECT }

/*
	Usage:

	auto client = new HttpClient("localhost", 80);
	// relative links work based on the current url
	client.get("foo/bar");
	client.get("baz"); // gets foo/baz

	auto request = client.get("rofl");
	auto response = request.waitForCompletion();
*/

/// HttpClient keeps cookies, location, and some other state to reuse connections, when possible, like a web browser.
class HttpClient {
	/* Protocol restrictions, useful to disable when debugging servers */
	bool useHttp11 = true;
	bool useGzip = true;

	/// Automatically follow a redirection?
	bool followLocation = false;

	@property Url location() {
		return currentUrl;
	}

	/// High level function that works similarly to entering a url
	/// into a browser.
	///
	/// Follows locations, updates the current url.
	AsynchronousHttpRequest navigateTo(Url where) {
		currentUrl = where.basedOn(currentUrl);
		assert(0);
	}

	private Url currentUrl;

	this() {

	}

	this(Url url) {
		open(url);
	}

	this(string host, ushort port = 80, bool useSsl = false) {
		open(host, port);
	}

	// FIXME: add proxy
	// FIXME: some kind of caching

	void open(Url url) {

	}

	void open(string host, ushort port = 80, bool useSsl = false) {

	}

	void close() {
		socket.close();
	}

	void setCookie(string name, string value) {

	}

	void clearCookies() {

	}

	HttpResponse sendSynchronously() {
		auto request = sendAsynchronously();
		return request.waitForCompletion();
	}

	AsynchronousHttpRequest sendAsynchronously() {

	}

	string method;
	string host;
	ushort port;
	string uri;

	string[] headers;
	ubyte[] requestBody;

	string userAgent;

	/* inter-request state */
	string[string] cookies;
}

// FIXME: websocket