#include "httpd.js.h"

#include "file.js.h"
#include "http.h"
#include "mem.h"
#include "task.h"
#include "util.js.h"

#include <assert.h>
#include <stdlib.h>

#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif

typedef struct _http_file_t
{
	tf_http_request_t* request;
	char etag[512];
} http_file_t;

static bool _ends_with(const char* a, const char* suffix)
{
	if (!a || !suffix)
	{
		return false;
	}
	size_t alen = strlen(a);
	size_t suffixlen = strlen(suffix);
	return alen >= suffixlen && strcmp(a + alen - suffixlen, suffix) == 0;
}

static const char* _after(const char* text, const char* prefix)
{
	size_t prefix_length = strlen(prefix);
	if (text && strncmp(text, prefix, prefix_length) == 0)
	{
		return text + prefix_length;
	}
	return NULL;
}

static double _time_spec_to_double(const uv_timespec_t* time_spec)
{
	return (double)time_spec->tv_sec + (double)(time_spec->tv_nsec) / 1e9;
}

static void _httpd_endpoint_static_read(tf_task_t* task, const char* path, int result, const void* data, void* user_data)
{
	http_file_t* file = user_data;
	tf_http_request_t* request = file->request;
	if (result >= 0)
	{
		if (strcmp(path, "core/tfrpc.js") == 0 || _ends_with(path, "core/tfrpc.js"))
		{
			const char* content_type = tf_httpd_ext_to_content_type(strrchr(path, '.'), true);
			const char* headers[] = {
				"Content-Type",
				content_type,
				"etag",
				file->etag,
				"Access-Control-Allow-Origin",
				"null",
			};
			tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result);
		}
		else
		{
			const char* content_type = tf_httpd_ext_to_content_type(strrchr(path, '.'), true);
			const char* headers[] = {
				"Content-Type",
				content_type,
				"etag",
				file->etag,
			};
			tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result);
		}
	}
	else
	{
		const char* k_payload = tf_http_status_text(404);
		tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
	}
	tf_http_request_unref(request);
	tf_free(file);
}

static void _httpd_endpoint_static_stat(tf_task_t* task, const char* path, int result, const uv_stat_t* stat, void* user_data)
{
	tf_http_request_t* request = user_data;
	const char* match = tf_http_request_get_header(request, "if-none-match");
	if (result != 0)
	{
		const char* k_payload = tf_http_status_text(404);
		tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
		tf_http_request_unref(request);
	}
	else
	{
		char etag[512];
		snprintf(etag, sizeof(etag), "\"%f_%zd\"", _time_spec_to_double(&stat->st_mtim), (size_t)stat->st_size);
		if (match && strcmp(match, etag) == 0)
		{
			tf_http_respond(request, 304, NULL, 0, NULL, 0);
			tf_http_request_unref(request);
		}
		else
		{
			http_file_t* file = tf_malloc(sizeof(http_file_t));
			*file = (http_file_t) { .request = request };
			static_assert(sizeof(file->etag) == sizeof(etag), "Size mismatch");
			memcpy(file->etag, etag, sizeof(etag));
			tf_file_read(task, path, _httpd_endpoint_static_read, file);
		}
	}
}

void tf_httpd_endpoint_static(tf_http_request_t* request)
{
	if (request->path && strncmp(request->path, "/.well-known/", strlen("/.well-known/")) && tf_httpd_redirect(request))
	{
		return;
	}

	const char* k_static_files[] = {
		"index.html",
		"client.js",
		"tildefriends.svg",
		"jszip.min.js",
		"style.css",
		"tfrpc.js",
		"w3.css",
	};

	const char* k_map[][2] = {
		{ "/static/", "core/" },
		{ "/lit/", "deps/lit/" },
		{ "/codemirror/", "deps/codemirror/" },
		{ "/prettier/", "deps/prettier/" },
		{ "/speedscope/", "deps/speedscope/" },
		{ "/.well-known/", "data/global/.well-known/" },
	};

	bool is_core = false;
	const char* after = NULL;
	const char* file_path = NULL;
	for (int i = 0; i < tf_countof(k_map) && !after; i++)
	{
		const char* next_after = _after(request->path, k_map[i][0]);
		if (next_after)
		{
			after = next_after;
			file_path = k_map[i][1];
			is_core = after && i == 0;
		}
	}

	if ((!after || !*after) && request->path[strlen(request->path) - 1] == '/')
	{
		after = "index.html";
		if (!file_path)
		{
			file_path = "core/";
			is_core = true;
		}
	}

	if (!after || strstr(after, ".."))
	{
		const char* k_payload = tf_http_status_text(404);
		tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
		return;
	}

	if (is_core)
	{
		bool found = false;
		for (int i = 0; i < tf_countof(k_static_files); i++)
		{
			if (strcmp(after, k_static_files[i]) == 0)
			{
				found = true;
				break;
			}
		}

		if (!found)
		{
			const char* k_payload = tf_http_status_text(404);
			tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
			return;
		}
	}

	tf_task_t* task = request->user_data;
	const char* root_path = tf_task_get_root_path(task);
	size_t size = (root_path ? strlen(root_path) + 1 : 0) + strlen(file_path) + strlen(after) + 1;
	char* path = alloca(size);
	snprintf(path, size, "%s%s%s%s", root_path ? root_path : "", root_path ? "/" : "", file_path, after);
	tf_http_request_ref(request);
	tf_file_stat(task, path, _httpd_endpoint_static_stat, request);
}
