/**
 * \file
 * \defgroup tfhttp Tilde Friends HTTP Client JS
 * Tilde Friends server-side HTTP client.
 * @{
 */

/**
 * Parse a URL into protocol, host, path, and port parts.
 * @param url
 * @return An object of the URL parts.
 */
function parseUrl(url) {
	// XXX: Hack.
	let match = url.match(new RegExp('(\\w+)://([^/:]+)(?::(\\d+))?(.*)'));
	return {
		protocol: match[1],
		host: match[2],
		path: match[4],
		port: match[3] ? parseInt(match[3]) : match[1] == 'http' ? 80 : 443,
	};
}

/**
 * Parse an HTTP response into headers and body content.
 * @param data The response data, headers and body included.
 * @return headers and body data.
 */
function parseResponse(data) {
	let firstLine;
	let headers = {};
	while (true) {
		let endLine = data.indexOf('\r\n');
		let line = data.substring(0, endLine);
		data = data.substring(endLine + 2);
		if (!line.length) {
			break;
		} else if (!firstLine) {
			firstLine = line;
		} else {
			let colon = line.indexOf(':');
			headers[line.substring(colon)] = line.substring(colon + 1);
		}
	}
	return {headers: headers, body: data};
}

/**
 * Make an HTTP request.
 * @param url The URL.
 * @param options Request options.
 * @param allowed_hosts List of allowed hosts.
 * @return A promise resolved with the response headers and body.
 */
export function fetch(url, options, allowed_hosts) {
	let parsed = parseUrl(url);
	return new Promise(function (resolve, reject) {
		if ((allowed_hosts ?? []).indexOf(parsed.host) == -1) {
			throw new Error(`fetch() request to host ${parsed.host} is not allowed.`);
		}
		let socket = new Socket();
		let buffer = new Uint8Array(0);

		return socket
			.connect(parsed.host, parsed.port)
			.then(function () {
				socket.read(function (data) {
					if (data && data.length) {
						let newBuffer = new Uint8Array(buffer.length + data.length);
						newBuffer.set(buffer, 0);
						newBuffer.set(data, buffer.length);
						buffer = newBuffer;
					} else {
						let result = parseHttpResponse(buffer);
						if (!result) {
							reject(new Exception('Parse failed.'));
						}
						if (typeof result == 'number') {
							if (result == -2) {
								reject('Incomplete request.');
							} else {
								reject('Bad request.');
							}
						} else if (typeof result == 'object') {
							resolve({
								body: buffer.slice(result.bytes_parsed),
								status: result.status,
								message: result.message,
								headers: result.headers,
							});
						} else {
							reject(new Exception('Unexpected parse result.'));
						}
						resolve(parseResponse(utf8Decode(buffer)));
					}
				});

				if (parsed.port == 443) {
					return socket.startTls();
				}
			})
			.then(function () {
				let body =
					typeof options?.body == 'string'
						? utf8Encode(options.body)
						: options.body || new Uint8Array(0);
				let headers = utf8Encode(
					`${options?.method ?? 'GET'} ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\nContent-Length: ${body.length}\r\n\r\n`
				);
				let fullRequest = new Uint8Array(headers.length + body.length);
				fullRequest.set(headers, 0);
				fullRequest.set(body, headers.length);
				socket.write(fullRequest);
			})
			.catch(function (error) {
				reject(error);
			});
	});
}

/** @} */
