// This thing sucks. It's primarily to prove that cgi.d *can* work
// with an embedded http server without much difficulty, so you aren't
// tied to CGI if you don't want to be, even if you have a huge codebase
// built on cgi.d already.

// But this particular module is no where near ready for serious use.
// (it does do reasonably well under controlled conditions though)

module arsd.httpd;

public import arsd.cgi;

import arsd.netman;

import std.range;

/*

import arsd.curl;
void handler(Cgi cgi) {
	cgi.write("hello world!");
	cgi.close();
}

void main() {
	serveHttp(&handler, 5000);
}

*/

void serveHttp(CustomCgi)(void function(Cgi) requestHandler, ushort port) if(is(CustomCgi: Cgi)) {
	auto netman = new NetMan!(CustomCgi)(requestHandler);
	netman.listen(port);
	for(;;)
		try
			netman.proceed();
		catch(ConnectionException e)
			e.c.disconnectNow();
		catch(Exception e)
			writefln("Exception: %s", e.toString());
}

class NetMan(CustomCgi) : NetworkManager /*if(is(CustomCgi : Cgi))*/ {
	void function(Cgi) requestHandler;

	this(void function(Cgi) requestHandler) {
		this.requestHandler = requestHandler;
	}

	override Connection allocConnection(int port) {
		return new HttpdConnection!CustomCgi(requestHandler);
	}
}

class HttpdConnection(CustomCgi) : Connection /* if(is(CustomCgi : Cgi)) */ {
	// The way this rolls is to get the whole thing in memory, then pass it off to Cgi to do the rest

	this(void function(Cgi) requestHandler) {
		handler = requestHandler;
	}

	void function(Cgi) handler;

	int state;
	string[] headers;
	immutable(ubyte)[] data;
	int contentLength;
	bool chunked;
	int chunkSize = 0;
	string separator = "\r\n";
	bool closeConnection;

	void log(in ubyte[] a) {
		data ~= a;
	}

	void finishRequest() {
		state = 0;
		separator = "\r\n";

		// writeln("FINISHED");

		scope(exit) {
			if(closeConnection)
				disconnect();
			closeConnection = false;
		}

		Cgi cgi;

		try {
			cgi = new CustomCgi(headers, data, peerAddress(),
				cast(void delegate(const(ubyte)[])) &this.write, 0, &this.flush);
		} catch(Throwable t) {
			write("HTTP/1.1 400 Bad Request\r\n");
			write("Content-Type: text/plain\r\n");
			string s = t.toString();
			write("Content-Length: "~to!string(s.length)~"\r\n");
			write("Connection: close\r\n");
			write("\r\n");
			write(s);

			closeConnection = true;

			return;
		}

		scope(exit) {
			cgi.close();
			cgi.dispose();
		}

		try {
			handler(cgi);
		} catch(Throwable e) {
			cgi.setResponseStatus("500 Internal Server Error");
			cgi.write(e.toString());
		}
	}

	override void onDataReceived(){
		auto a = read();

		// writeln("data received ", state, "\n", cast(string) a);

	more:
		switch(state) {
			default: assert(0);
			case 0: // reading the headers
				// while it's supposed to be \r\n, we want
				// to be permissive here to avoid hanging if
				// the wrong thing comes.
				try_again:
				int l = locationOf(a, separator ~ separator);
				if(l == -1) {
					if(separator.length > 1) {
						separator = "\n";
						goto try_again;
					} else {
						separator = "\r\n";
					}

					return; // not enough data
				}
				changeReadPosition(l+separator.length * 2); // we're now at the beginning of the data

				data.length = 0;
				contentLength = 0;

				string hdrs = cast(string) a[0..l].idup;
				a = read(); // advance ourselves
				headers = hdrs.split(separator);

				chunked = false;

				if(headers.length == 0) {
					disconnect();
					return;
				}

				if(headers[0].indexOf("HTTP/1.0") != -1)
					closeConnection = true; // always one request per connection with 1.0

				foreach(ref h; headers[1..$]) {
					int colon = h.indexOf(":");
					if(colon == -1)
						throw new Exception("Http headers need colons");
					string name = h[0..colon].tolower;
					string value = h[colon+2..$]; // FIXME?

					switch(name) {
					    case "transfer-encoding":
					    	if(value == "chunked")
							chunked = true;
					    break;
					    case "content-length":
						contentLength = to!int(value);
					    break;
					    case "connection":
						if(value == "close")
							closeConnection = true;
					    break;
					    default:
					    	// leave it unmolested for passthrough
					}
				}

				// forward the header and advance our state
				state = 1;
		//	break; // fall through to read some more data if we have any
			case 1: // reading Content-Length type data
				// We need to read up the data we have, and write it out as a chunk.
				if(!chunked) {
					if(a.length <= contentLength) {
						log(a);
						contentLength -= a.length;
						resetRead();
						// we just finished it off, terminate the chunks
						if(contentLength == 0) {
							finishRequest();
						}
					} else {
						// we actually have *more* here than we need....
						log(a[0..contentLength]);
						contentLength = 0;
						finishRequest();

						changeReadPosition(contentLength);
						a = read();
						// we're done
						goto more; // see if we can make use of the rest of the data
					}
				} else {
					// decode it, modify it, then reencode it
					// If here, we are at the beginning of a chunk.
					int loc = locationOf(a, "\r\n");
					if(loc == -1) {
						return; // don't have the length
					}

					string hex;
					hex = "";
					for(int i = 0; i < loc; i++) {
						char c = a[i];
						if(c >= 'A' && c <= 'Z')
							c += 0x20;
						if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')) {
							hex ~= c;
						} else {
							break;
						}
					}

					assert(hex.length);

					int power = 1;
					int size = 0;
					foreach(cc1; retro(hex)) {
						dchar cc = cc1;
						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;
					}

					chunkSize = size;
					assert(size >= 0);

					if(loc + 2 > a.length) {
						return; // need more data
					}
					changeReadPosition(loc+2); // skips the chunk header
					a = read();

					if(chunkSize == 0) { // we're done with the response
						state = 3;
						goto more;
					} else {
						state = 2;
						goto more;
					}

					resetRead();
				}
			break;
			case 2: // reading a chunk
				// if we got here, will change must be true....
				if(a.length < chunkSize + 2) {
					return; // we want to handle the whole chunk at once
				}

				log(a[0..chunkSize]);

				state = 1;

				if(a.length > chunkSize + 2) {
					assert(a[chunkSize] == 13);
					assert(a[chunkSize+1] == 10);
					changeReadPosition(chunkSize + 2); // skip the \r\n
					a = read();
					chunkSize = 0;
					state = 1;
					goto more;
				} else {
					chunkSize = 0;
					resetRead();
				}
			break;
			case 3: // reading footers
				// if we got here, will change must be true....
				int loc = locationOf(a, "\r\n");
				if(loc == -1) {
					return; // not done yet
				} else {
					assert(loc == 0);
					changeReadPosition(loc+2); // FIXME: should handle footers properly
					finishRequest();
					a = read();

					goto more;
				}
			break;
		}
	}
}
