import * as sha1 from './sha1.js';

"use strict";

var gHandlers = [];
var gSocketHandlers = [];
var gBadRequests = {};

const kRequestTimeout = 15000;
const kStallTimeout = 60000;

function logError(error) {
	print("ERROR " + error);
	if (error.stackTrace) {
		print(error.stackTrace);
	}
}

function addHandler(handler) {
	var added = false;
	for (var i in gHandlers) {
		if (gHandlers[i].path == handler.path) {
			gHandlers[i] = handler;
			added = true;
			break;
		}
	}
	if (!added) {
		gHandlers.push(handler);
		added = true;
	}
}

function all(prefix, handler) {
	addHandler({
		owner: this,
		path: prefix,
		invoke: handler,
	});
}

function registerSocketHandler(prefix, handler) {
	gSocketHandlers.push({
		owner: this,
		path: prefix,
		invoke: handler,
	});
}

function Request(method, uri, version, headers, body, client) {
	this.method = method;
	var index = uri.indexOf("?");
	if (index != -1) {
		this.uri = uri.slice(0, index);
		this.query = uri.slice(index + 1);
	} else {
		this.uri = uri;
		this.query = undefined;
	}
	this.version = version || '';
	this.headers = headers;
	this.client = {peerName: client.peerName, tls: client.tls};
	this.body = body;
	return this;
}

function findHandler(request) {
	var matchedHandler = null;
	for (var name in gHandlers) {
		var handler = gHandlers[name];
		if (request.uri == handler.path || request.uri.slice(0, handler.path.length + 1) == handler.path + '/') {
			matchedHandler = handler;
			break;
		}
	}
	return matchedHandler;
}

function findSocketHandler(request) {
	var matchedHandler = null;
	for (var name in gSocketHandlers) {
		var handler = gSocketHandlers[name];
		if (request.uri == handler.path || request.uri.slice(0, handler.path.length + 1) == handler.path + '/') {
			matchedHandler = handler;
			break;
		}
	}
	return matchedHandler;
}

function Response(request, client) {
	var kStatusText = {
		101: "Switching Protocols",
		200: 'OK',
		303: 'See other',
		304: 'Not Modified',
		403: 'Forbidden',
		404: 'File not found',
		500: 'Internal server error',
	};
	var _started = false;
	var _finished = false;
	var _keepAlive = false;
	var _chunked = false;
	return {
		writeHead: function(status) {
			if (_started) {
				throw new Error("Response.writeHead called multiple times.");
			}
			var reason;
			var headers;
			if (arguments.length == 3) {
				reason = arguments[1];
				headers = arguments[2];
			} else {
				reason = kStatusText[status];
				headers = arguments[1];
			}
			var lowerHeaders = {};
			var requestVersion = request.version.split("/")[1].split(".");
			var responseVersion = (requestVersion[0] >= 1 && requestVersion[0] >= 1) ? "1.1" : "1.0";
			var headerString = "HTTP/" + responseVersion + " " + status + " " + reason + "\r\n";
			for (var i in headers) {
				headerString += i + ": " + headers[i] + "\r\n";
				lowerHeaders[i.toLowerCase()] = headers[i];
			}
			if ("connection" in lowerHeaders) {
				_keepAlive = lowerHeaders["connection"].toLowerCase() == "keep-alive";
			} else {
				_keepAlive = ((request.version == "HTTP/1.0" && ("connection" in lowerHeaders && lowerHeaders["connection"].toLowerCase() == "keep-alive")) ||
					(request.version == "HTTP/1.1" && (!("connection" in lowerHeaders) || lowerHeaders["connection"].toLowerCase() != "close")));
				headerString += "Connection: " + (_keepAlive ? "keep-alive" : "close") + "\r\n";
			}
			_chunked = _keepAlive && !("content-length" in lowerHeaders);
			if (_chunked) {
				headerString += "Transfer-Encoding: chunked\r\n";
			}
			headerString += "\r\n";
			_started = true;
			client.write(headerString).catch(function() {});
		},
		end: function(data) {
			if (_finished) {
				throw new Error("Response.end called multiple times.");
			}
			if (data) {
				if (_chunked) {
					client.write(data.length.toString(16) + "\r\n" + data + "\r\n" + "0\r\n\r\n").catch(function() {});
				} else {
					client.write(data).catch(function() {});
				}
			} else if (_chunked) {
				client.write("0\r\n\r\n").catch(function() {});
			}
			_finished = true;
			if (!_keepAlive) {
				client.shutdown().catch(function() {});
			}
		},
		reportError: function(error) {
			if (!_started) {
				client.write("HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n").catch(function() {});
			}
			if (!_finished) {
				client.write("500 Internal Server Error\r\n\r\n" + error?.stackTrace).catch(function() {});
			}
			logError(client.peerName + " - - [" + new Date() + "] " + error);
		},
		isConnected: function() { return client.isConnected; },
	};
}

function handleRequest(request, response) {
	var handler = findHandler(request);

	print(request.client.peerName + " - - [" + new Date() + "] " + request.method + " " + request.uri + " " + request.version + " \"" + request.headers["user-agent"] + "\"");

	if (handler) {
		try {
			var promise = handler.invoke(request, response);
			if (promise) {
				promise.catch(function(error) {
					response.reportError(error);
					request.client.close();
				});
			}
		} catch (error) {
			response.reportError(error);
			request.client.close();
		}
	} else {
		response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"});
		response.end("No handler found for request: " + request.uri);
	}
}

function handleWebSocketRequest(request, response, client) {
	var buffer = new Uint8Array(0);
	var frame = new Uint8Array(0);
	var frameOpCode = 0x0;

	var handler = findSocketHandler(request);
	if (!handler) {
		client.close();
		return;
	}

	response.send = function(message, opCode) {
		if (opCode === undefined) {
			opCode = 0x2;
		}
		if (opCode == 0x1 && (typeof message == "string" || message instanceof String)) {
			message = utf8Encode(message);
		}
		var fin = true;
		var packet = [(fin ? (1 << 7) : 0) | (opCode & 0xf)];
		var mask = false;
		if (message.length < 126) {
			packet.push((mask ? (1 << 7) : 0) | message.length);
		} else if (message.length < (1 << 16)) {
			packet.push((mask ? (1 << 7) : 0) | 126);
			packet.push((message.length >> 8) & 0xff);
			packet.push(message.length & 0xff);
		} else {
			var high = 0; //(message.length / (1 ** 32)) & 0xffffffff;
			var low = message.length & 0xffffffff;
			packet.push((mask ? (1 << 7) : 0) | 127);
			packet.push((high >> 24) & 0xff);
			packet.push((high >> 16) & 0xff);
			packet.push((high >> 8) & 0xff);
			packet.push((high >> 0) & 0xff);
			packet.push((low >> 24) & 0xff);
			packet.push((low >> 16) & 0xff);
			packet.push((low >> 8) & 0xff);
			packet.push(low & 0xff);
		}

		var array = new Uint8Array(packet.length + message.length);
		array.set(packet, 0);
		array.set(message, packet.length);
		try {
			return client.write(array);
		} catch (error) {
			client.close();
			throw error;
		}
	}
	response.onMessage = null;

	let extra_headers = handler.invoke(request, response);

	client.read(function(data) {
		if (data) {
			var newBuffer = new Uint8Array(buffer.length + data.length);
			newBuffer.set(buffer, 0);
			newBuffer.set(data, buffer.length);
			buffer = newBuffer;

			while (buffer.length >= 2) {
				var bits0 = buffer[0];
				var bits1 = buffer[1];
				if (bits1 & (1 << 7) == 0) {
					// Unmasked message.
					client.close();
				}
				var opCode = bits0 & 0xf;
				var fin = bits0 & (1 << 7);
				var payloadLength = bits1 & 0x7f;
				var maskStart = 2;

				if (payloadLength == 126) {
					payloadLength = 0;
					for (var i = 0; i < 2; i++) {
						payloadLength <<= 8;
						payloadLength |= buffer[2 + i];
					}
					maskStart = 4;
				} else if (payloadLength == 127) {
					payloadLength = 0;
					for (var i = 0; i < 8; i++) {
						payloadLength <<= 8;
						payloadLength |= buffer[2 + i];
					}
					maskStart = 10;
				}
				var havePayload = buffer.length >= payloadLength + 2 + 4;
				if (havePayload) {
					var mask = buffer.slice(maskStart, maskStart + 4);
					var dataStart = maskStart + 4;
					var decoded = new Array(payloadLength);
					var payload = buffer.slice(dataStart, dataStart + payloadLength);
					buffer = buffer.slice(dataStart + payloadLength);
					for (var i = 0; i < payloadLength; i++) {
						decoded[i] = payload[i] ^ mask[i % 4];
					}

					var newBuffer = new Uint8Array(frame.length + decoded.length);
					newBuffer.set(frame, 0);
					newBuffer.set(decoded, frame.length);
					frame = newBuffer;

					if (opCode) {
						frameOpCode = opCode;
					}

					if (fin) {
						if (response.onMessage) {
							response.onMessage({
								data: frameOpCode == 0x1 ? utf8Decode(frame) : frame,
								opCode: frameOpCode,
							});
						}
						frame = new Uint8Array(0);
					}
				} else {
					break;
				}
			}
		} else {
			response.onClose();
			client.close();
		}
	});
	client.onError(function(error) {
		logError(client.peerName + " - - [" + new Date() + "] " + error);
		response.onError(error);
	});

	let headers = {
		"Upgrade": "websocket",
		"Connection": "Upgrade",
		"Sec-WebSocket-Accept": webSocketAcceptResponse(request.headers["sec-websocket-key"]),
	};
	if (request.headers["sec-websocket-version"] != "13") {
		headers["Sec-WebSocket-Version"] = "13";
	}
	response.writeHead(101, Object.assign({}, headers, extra_headers));
}

function webSocketAcceptResponse(key) {
	var kMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
	var kAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
	var hex = sha1.hash(key + kMagic)
	var binary = "";
	for (var i = 0; i < hex.length; i += 6) {
		var characters = hex.substring(i, i + 6);
		if (characters.length < 6) {
			characters += "0".repeat(6 - characters.length);
		}
		var value = parseInt(characters, 16);
		for (var bit = 0; bit < 8 * 3; bit += 6) {
			if (i * 8 / 2 + bit >= 8 * hex.length / 2) {
				binary += kAlphabet.charAt(64);
			} else {
				binary += kAlphabet.charAt((value >> (18 - bit)) & 63);
			}
		}
	}
	return binary;
}

function badRequest(client, reason) {
	var now = new Date();
	var count = 0;
	var old = gBadRequests[client.peerName];
	if (!old) {
		gBadRequests[client.peerName] = {
			expire: new Date(now.getTime() + 1 * 60 * 1000),
			count: 1,
			reason: reason,
		};
		count = 1;
	} else {
		old.count++;
		old.reason = reason;
		count = old.count;
	}
	new Response({version: '1.0'}, client).reportError(reason + ': ' + count);
	client.close();
}

function allowRequest(client) {
	var old = gBadRequests[client.peerName];
	if (old) {
		var now = new Date();
		if (old.expire < now) {
			delete gBadRequests[client.peerName];
			return true;
		} else {
			return old.count < 3;
		}
	} else {
		return true;
	}
}

function handleConnection(client) {
	if (!allowRequest(client)) {
		print('Rejecting client for too many bad requests: ', client.peerName, gBadRequests[client.peerName].reason);
		client.info = 'rejected';
		client.close();
		return;
	}

	client.info = 'accepted';
	var inputBuffer = new Uint8Array(0);
	var request;
	var headers = {};
	var lineByLine = true;
	var bodyToRead = -1;
	var body;
	var requestCount = -1;
	var readCount = 0;
	var isWebsocket = false;

	function resetTimeout(requestIndex) {
		if (isWebsocket) {
			return;
		}
		if (bodyToRead == -1) {
			setTimeout(function() {
				if (requestCount == requestIndex) {
					client.info = 'timed out';
					if (requestCount == 0) {
						badRequest(client, 'Timed out waiting for request.');
					} else {
						client.close();
					}
				}
			}, kRequestTimeout);
		} else {
			var lastReadCount = readCount;
			setTimeout(function() {
				if (readCount == lastReadCount) {
					client.info = 'stalled';
					if (requestCount == 0) {
						badRequest(client, 'Request stalled.');
					} else {
						client.close();
					}
				}
			}, kStallTimeout);
		}
	}

	resetTimeout(++requestCount);

	function reset() {
		inputBuffer = new Uint8Array(0);
		request = undefined;
		headers = {};
		lineByLine = true;
		bodyToRead = -1;
		body = undefined;
		client.info = 'reset';
		resetTimeout(++requestCount);
	}

	function finish() {
		client.info = 'finishing';
		var requestObject = new Request(request[0], request[1], request[2], headers, body, client);
		var response = new Response(requestObject, client);
		try {
			handleRequest(requestObject, response)
			if (client.isConnected) {
				reset();
			}
		} catch (error) {
			response.reportError(error);
			client.close();
		}
	}

	function handleLine(line, length) {
		if (bodyToRead == -1) {
			line = utf8Decode(line);
			if (!request) {
				if (!line) {
					badRequest(client, 'Empty request.');
					return false;
				}
				request = line.split(' ');
				if (request.length != 3 || !request[2].startsWith('HTTP/1.')) {
					badRequest(client, 'Bad request.');
					request = null;
					return false;
				}
				return true;
			} else if (line) {
				var colon = line.indexOf(':');
				var key = line.slice(0, colon).trim();
				var value = line.slice(colon + 1).trim();
				headers[key.toLowerCase()] = value;
				return true;
			} else {
				if (headers["content-length"] != undefined) {
					bodyToRead = parseInt(headers["content-length"]);
					lineByLine = false;
					if (bodyToRead > 16 * 1024 * 1024) {
						badRequest(client, 'Request too large: ' + bodyToRead + '.');
						return false;
					}
					body = new Uint8Array(bodyToRead);
					client.info = 'waiting for body';
					resetTimeout(requestCount);
					return true;
				} else if (headers["connection"]
					&& headers["connection"].toLowerCase().split(",").map(x => x.trim()).indexOf("upgrade") != -1
					&& headers["upgrade"]
					&& headers["upgrade"].toLowerCase() == "websocket") {
					isWebsocket = true;
					client.info = 'websocket';
					var requestObject = new Request(request[0], request[1], request[2], headers, body, client);
					var response = new Response(requestObject, client);
					handleWebSocketRequest(requestObject, response, client);
					/* Prevent the timeout from disconnecting us. */
					requestCount++;
					return false;
				} else {
					finish();
					return false;
				}
			}
		} else {
			var offset = body.length - bodyToRead;
			if (line.length > body.length - offset) {
				line = line.slice(0, body.length - offset);
			}
			body.set(line, offset);
			bodyToRead -= line.length;
			if (bodyToRead <= 0) {
				finish();
			}
		}
	}

	client.noDelay = true;

	client.onError(function(error) {
		logError(client.peerName + " - - [" + new Date() + "] " + error);
	});

	client.read(function(data) {
		readCount++;
		if (data) {
			if (bodyToRead != -1 && !isWebsocket) {
				resetTimeout(requestCount);
			}
			const kMaxLineLength = 4096;
			var newBuffer = new Uint8Array(inputBuffer.length + data.length);
			newBuffer.set(inputBuffer, 0);
			newBuffer.set(data, inputBuffer.length);
			inputBuffer = newBuffer;

			var newLine = '\n'.charCodeAt(0);
			var carriageReturn = '\r'.charCodeAt(0);

			var more = true;
			while (more) {
				if (lineByLine) {
					more = false;
					var end = inputBuffer.indexOf(newLine);
					var realEnd = end;
					if (end > 0 && inputBuffer[end - 1] == carriageReturn) {
						--end;
					}
					if (end > kMaxLineLength || end == -1 && inputBuffer.length > kMaxLineLength) {
						badRequest(client, 'Request too long.');
						return;
					}
					if (end != -1) {
						var line = inputBuffer.slice(0, end);
						inputBuffer = inputBuffer.slice(realEnd + 1);
						more = handleLine(line, realEnd + 1);
					}
				} else {
					more = handleLine(inputBuffer, inputBuffer.length);
					inputBuffer = new Uint8Array(0);
				}
			}
		} else {
			client.info = 'EOF';
			client.close();
		}
	});
}

var kBacklog = 8;
var kHost = "0.0.0.0"

var socket = new Socket();
socket.bind(kHost, tildefriends.http_port).then(function() {
	var listenResult = socket.listen(kBacklog, function() {
		socket.accept().then(handleConnection).catch(function(error) {
			logError("[" + new Date() + "] accept error " + error);
		});
	});
}).catch(function(error) {
	logError("[" + new Date() + "] bind error " + error);
});

if (tildefriends.https_port) {
	var tls = {};
	var secureSocket = new Socket();
	secureSocket.bind(kHost, tildefriends.https_port).then(function() {
		return secureSocket.listen(kBacklog, async function() {
			try {
				var client = await secureSocket.accept();
				client.tls = true;
				const kCertificatePath = "data/httpd/certificate.pem";
				const kPrivateKeyPath = "data/httpd/privatekey.pem";

				var stat = await Promise.all([
					await File.stat(kCertificatePath),
					await File.stat(kPrivateKeyPath),
				]);
				if (!tls.context ||
					tls.certStat.mtime != stat[0].mtime ||
					tls.certStat.size != stat[0].size ||
					tls.keyStat.mtime != stat[1].mtime ||
					tls.keyStat.size != stat[1].size) {
					print("Reloading " + kCertificatePath + " and " + kPrivateKeyPath);
					var privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
					var certificate = utf8Decode(await File.readFile(kCertificatePath));

					tls.context = new TlsContext();
					tls.context.setPrivateKey(privateKey);
					tls.context.setCertificate(certificate);
					tls.certStat = stat[0];
					tls.keyStat = stat[1];
				}

				let result = client.startTls(tls.context);
				handleConnection(client);
				return result;
			} catch (error) {
				logError("[" + new Date() + "] [" + client.peerName + "] " + error);
			}
		});
	}).catch(function(error) {
		logError("[" + new Date() + "] bind error " + error);
	});
}

export { all, registerSocketHandler };