/* Copyright (c) Dovecot authors, see top-level COPYING file */

#include "lib.h"
#include "array.h"
#include "bsearch-insert-pos.h"
#include "str-sanitize.h"

#include "http-url.h"
#include "http-server-private.h"

static struct event_category event_category_http_server_resource = {
	.name = "http-server-resource"
};

/*
 * Location
 */

static int
http_server_location_cmp(struct http_server_location *const *loc1,
			 struct http_server_location *const *loc2)
{
	return strcmp((*loc1)->path, (*loc2)->path);
}

static struct http_server_location *
http_server_location_add(struct http_server *server, pool_t pool,
			 const char *path)
{
	struct http_server_location qloc, *loc;
	unsigned int insert_idx;

	i_zero(&qloc);
	qloc.path = path;
	loc = &qloc;

	if (array_bsearch_insert_pos(&server->locations, &loc,
				     http_server_location_cmp, &insert_idx))
		return array_idx_elem(&server->locations, insert_idx);

	loc = p_new(pool, struct http_server_location, 1);
	loc->path = p_strdup(pool, path);
	array_insert(&server->locations, insert_idx, &loc, 1);
	return loc;
}

static int
http_server_location_find(struct http_server *server, const char *path,
			  struct http_server_location **loc_r,
			  const char **sub_path_r)
{
	struct http_server_location qloc, *loc;
	const char *loc_suffix;
	unsigned int insert_idx;

	*sub_path_r = NULL;
	*loc_r = NULL;

	i_zero(&qloc);
	qloc.path = path;
	loc = &qloc;

	if (array_bsearch_insert_pos(&server->locations, &loc,
				     http_server_location_cmp, &insert_idx)) {
		/* Exact match */
		*loc_r = array_idx_elem(&server->locations, insert_idx);
		*sub_path_r = "";
		return 1;
	}
	if (insert_idx == 0) {
		/* Not found at all */
		return -1;
	}
	loc = array_idx_elem(&server->locations, insert_idx-1);

	if (!str_begins(path, loc->path, &loc_suffix)) {
		/* Location isn't a prefix of path */
		return -1;
	} else if (loc_suffix[0] != '/') {
		/* Match doesn't end at '/' */
		return -1;
	}

	*sub_path_r = loc_suffix + 1;
	*loc_r = loc;
	return 0;
}

static void
http_server_location_remove(struct http_server *server,
			    struct http_server_location *loc)
{
	unsigned int idx;

	if (!array_lsearch_ptr_idx(&server->locations, loc, &idx))
		i_unreached();
	array_delete(&server->locations, idx, 1);
}

/*
 * Resource
 */

static void http_server_resource_update_event(struct http_server_resource *res)
{
	struct http_server_location *const *locs;
	unsigned int locs_count;

	locs = array_get(&res->locations, &locs_count);
	if (locs_count == 0) {
		event_set_append_log_prefix(res->event, "resource: ");
		return;
	}

	event_add_str(res->event, "path", locs[0]->path);
	event_set_append_log_prefix(
		res->event, t_strdup_printf("resource %s: ",
			str_sanitize(locs[0]->path, 128)));
}

#undef http_server_resource_create
struct http_server_resource *
http_server_resource_create(struct http_server *server, pool_t pool,
			    http_server_resource_callback_t *callback,
			    void *context)
{
	struct http_server_resource *res;

	pool_ref(pool);

	res = p_new(pool, struct http_server_resource, 1);
	res->pool = pool;
	res->server = server;

	res->callback = callback;
	res->context = context;

	p_array_init(&res->locations, pool, 4);

	res->event = event_create(server->event);
	event_add_category(res->event, &event_category_http_server_resource);
	http_server_resource_update_event(res);

	array_append(&server->resources, &res, 1);

	return res;
}

void http_server_resource_free(struct http_server_resource **_res)
{
	struct http_server_resource *res = *_res;
	struct http_server_location *loc;

	if (res == NULL)
		return;

	*_res = NULL;

	e_debug(res->event, "Free");

	if (res->destroy_callback != NULL) {
		res->destroy_callback(res->destroy_context);
		res->destroy_callback = NULL;
	}

	array_foreach_elem(&res->locations, loc)
		http_server_location_remove(res->server, loc);

	event_unref(&res->event);
	pool_unref(&res->pool);
}

pool_t http_server_resource_get_pool(struct http_server_resource *res)
{
	return res->pool;
}

const char *http_server_resource_get_path(struct http_server_resource *res)
{
	struct http_server_location *const *locs;
	unsigned int locs_count;

	locs = array_get(&res->locations, &locs_count);
	i_assert(locs_count > 0);

	return locs[0]->path;
}

struct event *http_server_resource_get_event(struct http_server_resource *res)
{
	return res->event;
}

void http_server_resource_add_location(struct http_server_resource *res,
				       const char *path)
{
	struct http_server_location *loc;

	i_assert(*path == '\0' || *path == '/');

	loc = http_server_location_add(res->server, res->pool, path);
	i_assert(loc->resource == NULL);

	loc->resource = res;
	array_append(&res->locations, &loc, 1);

	if (array_count(&res->locations) == 1)
		http_server_resource_update_event(res);
}

int http_server_resource_find(struct http_server *server, const char *path,
			      struct http_server_resource **res_r,
			      const char **sub_path_r)
{
	struct http_server_location *loc;
	int ret;

	if (path == NULL)
		return -1;

	*res_r = NULL;
	*sub_path_r = NULL;

	ret = http_server_location_find(server, path, &loc, sub_path_r);
	if (ret < 0)
		return -1;

	i_assert(loc->resource != NULL);
	*res_r = loc->resource;
	return ret;
}

bool http_server_resource_callback(struct http_server_request *req)
{
	struct http_server *server = req->server;
	struct http_server_resource *res;
	const char *sub_path;

	switch (req->req.target.format) {
	case HTTP_REQUEST_TARGET_FORMAT_ORIGIN:
		/* According to RFC 7240, Section 5.3.1 only the origin form is
		   applicable to local resources on an origin server.
		*/
		break;
	case HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE:
	case HTTP_REQUEST_TARGET_FORMAT_AUTHORITY:
	case HTTP_REQUEST_TARGET_FORMAT_ASTERISK:
		/* Not applicable for a local resource. */
		return FALSE;
	}

	if (http_server_resource_find(server, req->req.target.url->path,
				      &res, &sub_path) < 0)
		return FALSE;

	e_debug(res->event, "Got request: %s", http_server_request_label(req));

	i_assert(res->callback != NULL);
	res->callback(res->context, req, sub_path);
	return TRUE;
}

#undef http_server_resource_set_destroy_callback
void http_server_resource_set_destroy_callback(struct http_server_resource *res,
					       void (*callback)(void *),
					       void *context)
{
	res->destroy_callback = callback;
	res->destroy_context = context;
}
