/*
 * Copyright 2009 John-Mark Bell <jmb@netsurf-browser.org>
 *
 * This file is part of NetSurf, http://www.netsurf-browser.org/
 *
 * NetSurf is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * NetSurf is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/** \file
 * High-level resource cache (implementation)
 */

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

#include "content/content.h"
#include "content/hlcache.h"
#include "utils/http.h"
#include "utils/log.h"
#include "utils/messages.h"
#include "utils/ring.h"
#include "utils/url.h"
#include "utils/utils.h"

typedef struct hlcache_entry hlcache_entry;
typedef struct hlcache_retrieval_ctx hlcache_retrieval_ctx;

/** High-level cache retrieval context */
struct hlcache_retrieval_ctx {
	struct hlcache_retrieval_ctx *r_prev; /**< Previous retrieval context in the ring */
	struct hlcache_retrieval_ctx *r_next; /**< Next retrieval context in the ring */
	
	llcache_handle *llcache;	/**< Low-level cache handle */

	hlcache_handle *handle;		/**< High-level handle for object */

	uint32_t flags;			/**< Retrieval flags */

	const content_type *accepted_types;	/**< Accepted types, or NULL */

	hlcache_child_context child;	/**< Child context */	
};

/** High-level cache handle */
struct hlcache_handle {
	hlcache_entry *entry;		/**< Pointer to cache entry */

	hlcache_handle_callback cb;	/**< Client callback */
	void *pw;			/**< Client data */
};

/** Entry in high-level cache */
struct hlcache_entry {
	struct content *content;	/**< Pointer to associated content */

	hlcache_entry *next;		/**< Next sibling */
	hlcache_entry *prev;		/**< Previous sibling */
};

/** List of cached content objects */
static hlcache_entry *hlcache_content_list;

/** Ring of retrieval contexts */
static hlcache_retrieval_ctx *hlcache_retrieval_ctx_ring;

static void hlcache_clean(void);
static nserror hlcache_llcache_callback(llcache_handle *handle,
		const llcache_event *event, void *pw);
static bool hlcache_type_is_acceptable(llcache_handle *llcache, 
		const content_type *accepted_types, 
		content_type *computed_type);
static nserror hlcache_find_content(hlcache_retrieval_ctx *ctx);
static void hlcache_content_callback(struct content *c,
		content_msg msg, union content_msg_data data, void *pw);

/******************************************************************************
 * Public API								      *
 ******************************************************************************/

/* See hlcache.h for documentation */
void hlcache_finalise(void)
{
	uint32_t num_contents, prev_contents;
	hlcache_entry *entry;
	hlcache_retrieval_ctx *ctx, *next;

	/* Obtain initial count of contents remaining */
	for (num_contents = 0, entry = hlcache_content_list; 
			entry != NULL; entry = entry->next) {
		num_contents++;
	}

	/* Drain cache */
	do {
		prev_contents = num_contents;

		hlcache_clean();

		for (num_contents = 0, entry = hlcache_content_list; 
				entry != NULL; entry = entry->next) {
			num_contents++;
		}
	} while (num_contents > 0 && num_contents != prev_contents);

	LOG(("%d contents remaining:", num_contents));
	for (entry = hlcache_content_list; entry != NULL; entry = entry->next) {
		hlcache_handle entry_handle = { entry, NULL, NULL };

		if (entry->content != NULL) {
			LOG(("  %p : %s", entry, 
					content_get_url(&entry_handle)));
		} else {
			LOG(("  %p", entry));
		}
	}

	/* Clean up retrieval contexts */
	if (hlcache_retrieval_ctx_ring != NULL) {
		do {
			ctx = hlcache_retrieval_ctx_ring;
			next = ctx->r_next;

			if (ctx->llcache != NULL)
				llcache_handle_release(ctx->llcache);

			if (ctx->handle != NULL)
				free(ctx->handle);

			if (ctx->child.charset != NULL)
				free((char *) ctx->child.charset);

			free(ctx);

			ctx = next;
		} while (ctx != hlcache_retrieval_ctx_ring);

		hlcache_retrieval_ctx_ring = NULL;
	}
}

/* See hlcache.h for documentation */
nserror hlcache_poll(void)
{
	static uint32_t last_clean_time;
	uint32_t now;

	llcache_poll();

	/* Only attempt to clean the cache every 5 seconds */
#define HLCACHE_CLEAN_INTERVAL_CS (500)
	now = wallclock();

	if (now > last_clean_time + HLCACHE_CLEAN_INTERVAL_CS) {
		/* Give the cache a clean */
		hlcache_clean();

		last_clean_time = now;
	}
#undef HLCACHE_CLEAN_INTERVAL_CS

	return NSERROR_OK;
}

/* See hlcache.h for documentation */
nserror hlcache_handle_retrieve(const char *url, uint32_t flags,
		const char *referer, llcache_post_data *post,
		hlcache_handle_callback cb, void *pw,
		hlcache_child_context *child, 
		const content_type *accepted_types, hlcache_handle **result)
{
	hlcache_retrieval_ctx *ctx;
	nserror error;

	assert(cb != NULL);

	ctx = calloc(1, sizeof(hlcache_retrieval_ctx));
	if (ctx == NULL)
		return NSERROR_NOMEM;

	ctx->handle = calloc(1, sizeof(hlcache_handle));
	if (ctx->handle == NULL) {
		free(ctx);
		return NSERROR_NOMEM;
	}

	if (child != NULL) {
		if (child->charset != NULL) {
			ctx->child.charset = strdup(child->charset);
			if (ctx->child.charset == NULL) {
				free(ctx->handle);
				free(ctx);
				return NSERROR_NOMEM;
			}
		}
		ctx->child.quirks = child->quirks;
	}

	ctx->flags = flags;
	ctx->accepted_types = accepted_types;

	ctx->handle->cb = cb;
	ctx->handle->pw = pw;

	error = llcache_handle_retrieve(url, flags, referer, post, 
			hlcache_llcache_callback, ctx, 
			&ctx->llcache);
	if (error != NSERROR_OK) {
		free((char *) ctx->child.charset);
		free(ctx->handle);
		free(ctx);
		return error;
	}
	
	RING_INSERT(hlcache_retrieval_ctx_ring, ctx);
	
	*result = ctx->handle;

	return NSERROR_OK;
}

/* See hlcache.h for documentation */
nserror hlcache_handle_release(hlcache_handle *handle)
{
	if (handle->entry != NULL) {
		content_remove_user(handle->entry->content, 
				hlcache_content_callback, handle);
	} else {
		RING_ITERATE_START(struct hlcache_retrieval_ctx,
				   hlcache_retrieval_ctx_ring,
				   ictx) {
			if (ictx->handle == handle) {
				/* This is the nascent context for us, 
				 * so abort the fetch */
				llcache_handle_abort(ictx->llcache);
				llcache_handle_release(ictx->llcache);
				/* Remove us from the ring */
				RING_REMOVE(hlcache_retrieval_ctx_ring, ictx);
				/* Throw us away */
				free((char *) ictx->child.charset);
				free(ictx);
				/* And stop */
				RING_ITERATE_STOP(hlcache_retrieval_ctx_ring, 
						ictx);
			}
		} RING_ITERATE_END(hlcache_retrieval_ctx_ring, ictx);
	}

	handle->cb = NULL;
	handle->pw = NULL;

	free(handle);

	return NSERROR_OK;
}

/* See hlcache.h for documentation */
struct content *hlcache_handle_get_content(const hlcache_handle *handle)
{
	assert(handle != NULL);

	if (handle->entry != NULL)
		return handle->entry->content;

	return NULL;
}

/* See hlcache.h for documentation */
nserror hlcache_handle_abort(hlcache_handle *handle)
{
	struct hlcache_entry *entry = handle->entry;
	struct content *c;
	
	if (entry == NULL) {
		/* This handle is not yet associated with a cache entry.
		 * The implication is that the fetch for the handle has
		 * not progressed to the point where the entry can be
		 * created. */
		
		RING_ITERATE_START(struct hlcache_retrieval_ctx,
				   hlcache_retrieval_ctx_ring,
				   ictx) {
			if (ictx->handle == handle) {
				/* This is the nascent context for us, 
				 * so abort the fetch */
				llcache_handle_abort(ictx->llcache);
				llcache_handle_release(ictx->llcache);
				/* Remove us from the ring */
				RING_REMOVE(hlcache_retrieval_ctx_ring, ictx);
				/* Throw us away */
				free((char *) ictx->child.charset);
				free(ictx);
				/* And stop */
				RING_ITERATE_STOP(hlcache_retrieval_ctx_ring, 
						ictx);
			}
		} RING_ITERATE_END(hlcache_retrieval_ctx_ring, ictx);
		
		return NSERROR_OK;
	}

	c = entry->content;
	
	if (content_count_users(c) > 1) {
		/* We are not the only user of 'c' so clone it. */
		struct content *clone = content_clone(c);
		
		if (clone == NULL)
			return NSERROR_NOMEM;
		
		entry = calloc(sizeof(struct hlcache_entry), 1);
		
		if (entry == NULL) {
			content_destroy(clone);
			return NSERROR_NOMEM;
		}
		
		if (content_add_user(clone, 
				hlcache_content_callback, handle) == false) {
			content_destroy(clone);
			free(entry);
			return NSERROR_NOMEM;
		}
		
		content_remove_user(c, hlcache_content_callback, handle);
		
		entry->content = clone;
		handle->entry = entry;
		entry->prev = NULL;
		entry->next = hlcache_content_list;
		if (hlcache_content_list != NULL)
			hlcache_content_list->prev = entry;
		hlcache_content_list = entry;
		
		c = clone;
	}
	
	return content_abort(c);
}

/* See hlcache.h for documentation */
nserror hlcache_handle_replace_callback(hlcache_handle *handle,
		hlcache_handle_callback cb, void *pw)
{
	handle->cb = cb;
	handle->pw = pw;

	return NSERROR_OK;
}

/******************************************************************************
 * High-level cache internals						      *
 ******************************************************************************/

/**
 * Attempt to clean the cache
 */
void hlcache_clean(void)
{
	hlcache_entry *entry, *next;

	for (entry = hlcache_content_list; entry != NULL; entry = next) {
		next = entry->next;

		if (entry->content == NULL)
			continue;

		if (content_count_users(entry->content) != 0)
			continue;

		/** \todo This is over-zealous: all unused contents 
		 * will be immediately destroyed. Ideally, we want to 
		 * purge all unused contents that are using stale 
		 * source data, and enough fresh contents such that 
		 * the cache fits in the configured cache size limit.
		 */

		/* Remove entry from cache */
		if (entry->prev == NULL)
			hlcache_content_list = entry->next;
		else
			entry->prev->next = entry->next;

		if (entry->next != NULL)
			entry->next->prev = entry->prev;

		/* Destroy content */
		content_destroy(entry->content);

		/* Destroy entry */
		free(entry);
	}
}

/**
 * Handler for low-level cache events
 *
 * \param handle  Handle for which event is issued
 * \param event	  Event data
 * \param pw	  Pointer to client-specific data
 * \return NSERROR_OK on success, appropriate error otherwise
 */
nserror hlcache_llcache_callback(llcache_handle *handle,
		const llcache_event *event, void *pw)
{
	hlcache_retrieval_ctx *ctx = pw;
	nserror error;

	assert(ctx->llcache == handle);

	switch (event->type) {
	case LLCACHE_EVENT_HAD_HEADERS:
	{
		content_type type = CONTENT_UNKNOWN;
		
		/* Unlink the context to prevent recursion */
		RING_REMOVE(hlcache_retrieval_ctx_ring, ctx);
		
		if (hlcache_type_is_acceptable(handle, 
				ctx->accepted_types, &type)) {
			error = hlcache_find_content(ctx);
			if (error != NSERROR_OK) {
				hlcache_event hlevent;

				hlevent.type = CONTENT_MSG_ERROR;
				hlevent.data.error = messages_get("MiscError");

				ctx->handle->cb(ctx->handle, &hlevent, 
						ctx->handle->pw);
				
				llcache_handle_abort(handle);
				llcache_handle_release(handle);
                                free((char *) ctx->child.charset);
				free(ctx);
				return error;
			}
		} else if (type == CONTENT_OTHER && 
				ctx->flags & HLCACHE_RETRIEVE_MAY_DOWNLOAD) {
			/* Unknown type, and we can download, so convert */
			llcache_handle_force_stream(handle);

			if (ctx->handle->cb != NULL) {
				hlcache_event hlevent;

				hlevent.type = CONTENT_MSG_DOWNLOAD;
				hlevent.data.download = handle;

				ctx->handle->cb(ctx->handle, &hlevent,
						ctx->handle->pw);
			}
		} else {
			/* Unacceptable type: abort fetch and report error */
			llcache_handle_abort(handle);
			llcache_handle_release(handle);

			if (ctx->handle->cb != NULL) {
				hlcache_event hlevent;

				hlevent.type = CONTENT_MSG_ERROR;
				hlevent.data.error = messages_get("BadType");

				ctx->handle->cb(ctx->handle, &hlevent, 
						ctx->handle->pw);
			}
		}

		/* No longer require retrieval context */
		free((char *) ctx->child.charset);
		free(ctx);
	}
		break;
	case LLCACHE_EVENT_HAD_DATA:
		/* fall through */
	case LLCACHE_EVENT_DONE:
		/* should never happen: the handler must be changed */
		assert(0 && "Unexpected llcache event");
		break;
	case LLCACHE_EVENT_ERROR:
		if (ctx->handle->cb != NULL) {
			hlcache_event hlevent;

			hlevent.type = CONTENT_MSG_ERROR;
			hlevent.data.error = event->data.msg;

			ctx->handle->cb(ctx->handle, &hlevent, ctx->handle->pw);
		}	
		break;
	case LLCACHE_EVENT_PROGRESS:
		break;
	}

	return NSERROR_OK;
}

/**
 * Determine if the type of a low-level cache object is acceptable
 *
 * \param llcache	  Low-level cache object to consider
 * \param accepted_types  Array of acceptable types, or NULL for any
 * \param computed_type	  Pointer to location to receive computed type of object
 * \return True if the type is acceptable, false otherwise
 */
bool hlcache_type_is_acceptable(llcache_handle *llcache, 
		const content_type *accepted_types, content_type *computed_type)
{
	const char *content_type_header;
	char *mime_type;
	http_parameter *params;
	content_type type;
	nserror error;
	bool acceptable;

	content_type_header = 
			llcache_handle_get_header(llcache, "Content-Type");
	if (content_type_header == NULL)
		content_type_header = "text/plain";

	error = http_parse_content_type(content_type_header, &mime_type, 
			&params);
	if (error != NSERROR_OK)
		return false;

	type = content_lookup(mime_type);

	free(mime_type);
	http_parameter_list_destroy(params);

	if (accepted_types == NULL) {
		acceptable = type != CONTENT_OTHER;
	} else {
		while (*accepted_types != CONTENT_UNKNOWN) {
			if (*accepted_types == type)
				break;

			accepted_types++;
		}

		acceptable = *accepted_types == type;
	}

	*computed_type = type;

	return acceptable;
}

/**
 * Find a content for the high-level cache handle
 *
 * \param ctx	High-level cache retrieval context
 * \return NSERROR_OK on success, appropriate error otherwise
 *
 * \pre handle::state == HLCACHE_HANDLE_NEW
 * \pre Headers must have been received for associated low-level handle
 * \post Low-level handle is either released, or associated with new content
 * \post High-level handle is registered with content
 */
nserror hlcache_find_content(hlcache_retrieval_ctx *ctx)
{
	hlcache_entry *entry;
	hlcache_event event;

	/* Search list of cached contents for a suitable one */
	for (entry = hlcache_content_list; entry != NULL; entry = entry->next) {
		hlcache_handle entry_handle = { entry, NULL, NULL };
		const llcache_handle *entry_llcache;

		if (entry->content == NULL)
			continue;

		/* Ignore contents in the error state */
		if (content_get_status(&entry_handle) == CONTENT_STATUS_ERROR)
			continue;

		/* Ensure that content is shareable */
		if (content_is_shareable(entry->content) == false)
			continue;

		/* Ensure that quirks mode is acceptable */
		if (content_matches_quirks(entry->content, 
				ctx->child.quirks) == false)
			continue;

		/* Ensure that content uses same low-level object as 
		 * low-level handle */
		entry_llcache = content_get_llcache_handle(entry->content);

		if (llcache_handle_references_same_object(entry_llcache, 
				ctx->llcache))
			break;
	}

	if (entry == NULL) {
		/* No existing entry, so need to create one */
		entry = malloc(sizeof(hlcache_entry));
		if (entry == NULL)
			return NSERROR_NOMEM;

		/* Create content using llhandle */
		entry->content = content_create(ctx->llcache, 
				ctx->child.charset, ctx->child.quirks);
		if (entry->content == NULL) {
			free(entry);
			return NSERROR_NOMEM;
		}

		/* Insert into cache */
		entry->prev = NULL;
		entry->next = hlcache_content_list;
		if (hlcache_content_list != NULL)
			hlcache_content_list->prev = entry;
		hlcache_content_list = entry;
	} else {
		/* Found a suitable content: no longer need low-level handle */
		llcache_handle_release(ctx->llcache);	
	}

	/* Associate handle with content */
	if (content_add_user(entry->content, 
			hlcache_content_callback, ctx->handle) == false)
		return NSERROR_NOMEM;

	/* Associate cache entry with handle */
	ctx->handle->entry = entry;

	/* Catch handle up with state of content */
	if (ctx->handle->cb != NULL) {
		content_status status = content_get_status(ctx->handle);

		if (status == CONTENT_STATUS_LOADING) {
			event.type = CONTENT_MSG_LOADING;
			ctx->handle->cb(ctx->handle, &event, ctx->handle->pw);
		} else if (status == CONTENT_STATUS_READY) {
			event.type = CONTENT_MSG_LOADING;
			ctx->handle->cb(ctx->handle, &event, ctx->handle->pw);

			if (ctx->handle->cb != NULL) {
				event.type = CONTENT_MSG_READY;
				ctx->handle->cb(ctx->handle, &event, 
						ctx->handle->pw);
			}
		} else if (status == CONTENT_STATUS_DONE) {
			event.type = CONTENT_MSG_LOADING;
			ctx->handle->cb(ctx->handle, &event, ctx->handle->pw);

			if (ctx->handle->cb != NULL) {
				event.type = CONTENT_MSG_READY;
				ctx->handle->cb(ctx->handle, &event, 
						ctx->handle->pw);
			}

			if (ctx->handle->cb != NULL) {
				event.type = CONTENT_MSG_DONE;
				ctx->handle->cb(ctx->handle, &event, 
						ctx->handle->pw);
			}
		}
	}

	return NSERROR_OK;
}

/**
 * Veneer between content callback API and hlcache callback API
 *
 * \param c	Content to emit message for
 * \param msg	Message to emit
 * \param data	Data for message
 * \param pw	Pointer to private data (hlcache_handle)
 */
void hlcache_content_callback(struct content *c, content_msg msg, 
		union content_msg_data data, void *pw)
{
	hlcache_handle *handle = pw;
	hlcache_event event;
	nserror error = NSERROR_OK;

	event.type = msg;
	event.data = data;

	if (handle->cb != NULL)
		error = handle->cb(handle, &event, handle->pw);

	if (error != NSERROR_OK)
		LOG(("Error in callback: %d", error));
}

