/*
 * Copyright (C) 2003-2009 The Music Player Daemon Project
 * http://www.musicpd.org
 *
 * This program 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "httpd_client.h"
#include "httpd_internal.h"
#include "fifo_buffer.h"
#include "page.h"

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

struct httpd_client {
	/**
	 * The httpd output object this client is connected to.
	 */
	struct httpd_output *httpd;

	/**
	 * The TCP socket.
	 */
	GIOChannel *channel;

	/**
	 * The GLib main loop source id for reading from the socket,
	 * and to detect errors.
	 */
	guint read_source_id;

	/**
	 * The GLib main loop source id for writing to the socket.  If
	 * 0, then there is no event source currently (because there
	 * are no queued pages).
	 */
	guint write_source_id;

	/**
	 * For buffered reading.  This pointer is only valid while the
	 * HTTP request is read.
	 */
	struct fifo_buffer *input;

	/**
	 * The current state of the client.
	 */
	enum {
		/** reading the request line */
		REQUEST,

		/** reading the request headers */
		HEADERS,

		/** sending the HTTP response */
		RESPONSE,
	} state;

	/**
	 * A queue of #page objects to be sent to the client.
	 */
	GQueue *pages;

	/**
	 * The #page which is currently being sent to the client.
	 */
	struct page *current_page;

	/**
	 * The amount of bytes which were already sent from
	 * #current_page.
	 */
	size_t current_position;
};

static void
httpd_client_unref_page(gpointer data, G_GNUC_UNUSED gpointer user_data)
{
	struct page *page = data;

	page_unref(page);
}

void
httpd_client_free(struct httpd_client *client)
{
	if (client->state == RESPONSE) {
		if (client->write_source_id != 0)
			g_source_remove(client->write_source_id);

		if (client->current_page != NULL)
			page_unref(client->current_page);

		g_queue_foreach(client->pages, httpd_client_unref_page, NULL);
		g_queue_free(client->pages);
	} else
		fifo_buffer_free(client->input);

	g_source_remove(client->read_source_id);
	g_io_channel_unref(client->channel);
	g_free(client);
}

/**
 * Frees the client and removes it from the server's client list.
 */
static void
httpd_client_close(struct httpd_client *client)
{
	httpd_output_remove_client(client->httpd, client);
	httpd_client_free(client);
}

/**
 * Switch the client to the "RESPONSE" state.
 */
static void
httpd_client_begin_response(struct httpd_client *client)
{
	client->state = RESPONSE;
	client->write_source_id = 0;
	client->pages = g_queue_new();
	client->current_page = NULL;

	httpd_output_send_header(client->httpd, client);
}

/**
 * Handle a line of the HTTP request.
 */
static bool
httpd_client_handle_line(struct httpd_client *client, const char *line)
{
	assert(client->state != RESPONSE);

	if (client->state == REQUEST) {
		if (strncmp(line, "GET /", 5) != 0) {
			/* only GET is supported */
			g_warning("malformed request line from client");
			return false;
		}

		line = strchr(line + 5, ' ');
		if (line == NULL || strncmp(line + 1, "HTTP/", 5) != 0) {
			/* HTTP/0.9 without request headers */
			httpd_client_begin_response(client);
			return true;
		}

		/* after the request line, request headers follow */
		client->state = HEADERS;
		return true;
	} else {
		if (*line == 0) {
			/* empty line: request is finished */
			httpd_client_begin_response(client);
			return true;
		}

		/* expect more request headers */
		return true;
	}
}

/**
 * Check if a complete line of input is present in the input buffer,
 * and duplicates it.  It is removed from the input buffer.  The
 * return value has to be freed with g_free().
 */
static char *
httpd_client_read_line(struct httpd_client *client)
{
	const char *p, *newline;
	size_t length;
	char *line;

	p = fifo_buffer_read(client->input, &length);
	if (p == NULL)
		/* empty input buffer */
		return NULL;

	newline = memchr(p, '\n', length);
	if (newline == NULL)
		/* incomplete line */
		return NULL;

	line = g_strndup(p, newline - p);
	fifo_buffer_consume(client->input, newline - p + 1);

	/* remove trailing whitespace (e.g. '\r') */
	return g_strchomp(line);
}

/**
 * Sends the status line and response headers to the client.
 */
static bool
httpd_client_send_response(struct httpd_client *client)
{
	char buffer[1024];
	GError *error = NULL;
	GIOStatus status;
	gsize bytes_written;

	assert(client->state == RESPONSE);

	g_snprintf(buffer, sizeof(buffer),
		   "HTTP/1.1 200 OK\r\n"
		   "Content-Type: %s\r\n"
		   "Connection: close\r\n"
		   "Pragma: no-cache\r\n"
		   "Cache-Control: no-cache, no-store\r\n"
		   "\r\n",
		   client->httpd->content_type);

	status = g_io_channel_write_chars(client->channel,
					  buffer, strlen(buffer),
					  &bytes_written, &error);
	switch (status) {
	case G_IO_STATUS_NORMAL:
	case G_IO_STATUS_AGAIN:
		return true;

	case G_IO_STATUS_EOF:
		/* client has disconnected */

		httpd_client_close(client);
		return false;

	case G_IO_STATUS_ERROR:
		/* I/O error */

		g_warning("failed to write to client: %s", error->message);
		g_error_free(error);

		httpd_client_close(client);
		return false;
	}

	/* unreachable */
	httpd_client_close(client);
	return false;
}

/**
 * Data has been received from the client and it is appended to the
 * input buffer.
 */
static bool
httpd_client_received(struct httpd_client *client)
{
	char *line;
	bool success;

	while ((line = httpd_client_read_line(client)) != NULL) {
		success = httpd_client_handle_line(client, line);
		g_free(line);
		if (!success)
			return false;

		if (client->state == RESPONSE) {
			if (!fifo_buffer_is_empty(client->input)) {
				g_warning("unexpected input from client");
				return false;
			}

			fifo_buffer_free(client->input);

			return httpd_client_send_response(client);
		}
	}

	return true;
}

static bool
httpd_client_read(struct httpd_client *client)
{
	char *p;
	size_t max_length;
	GError *error = NULL;
	GIOStatus status;
	gsize bytes_read;

	if (client->state == RESPONSE) {
		/* the client has already sent the request, and he
		   must not send more */
		g_warning("unexpected input from client");
		return false;
	}

	p = fifo_buffer_write(client->input, &max_length);
	if (p == NULL) {
		g_warning("buffer overflow");
		return false;
	}

	status = g_io_channel_read_chars(client->channel, p, max_length,
					 &bytes_read, &error);
	switch (status) {
	case G_IO_STATUS_NORMAL:
		fifo_buffer_append(client->input, bytes_read);
		return httpd_client_received(client);

	case G_IO_STATUS_AGAIN:
		/* try again later, after select() */
		return true;

	case G_IO_STATUS_EOF:
		/* peer disconnected */
		return false;

	case G_IO_STATUS_ERROR:
		/* I/O error */
		g_warning("failed to read from client: %s",
			  error->message);
		g_error_free(error);
		return false;
	}

	/* unreachable */
	return false;
}

static gboolean
httpd_client_in_event(G_GNUC_UNUSED GIOChannel *source, GIOCondition condition,
		      gpointer data)
{
	struct httpd_client *client = data;
	struct httpd_output *httpd = client->httpd;
	bool ret;

	g_mutex_lock(httpd->mutex);

	if (condition == G_IO_IN && httpd_client_read(client)) {
		ret = true;
	} else {
		httpd_client_close(client);
		ret = false;
	}

	g_mutex_unlock(httpd->mutex);

	return ret;
}

struct httpd_client *
httpd_client_new(struct httpd_output *httpd, int fd)
{
	struct httpd_client *client = g_new(struct httpd_client, 1);

	client->httpd = httpd;

#ifndef G_OS_WIN32
	client->channel = g_io_channel_unix_new(fd);
#else
	client->channel = g_io_channel_win32_new_socket(fd);
#endif

	/* GLib is responsible for closing the file descriptor */
	g_io_channel_set_close_on_unref(client->channel, true);
	/* NULL encoding means the stream is binary safe */
	g_io_channel_set_encoding(client->channel, NULL, NULL);
	/* we prefer to do buffering */
	g_io_channel_set_buffered(client->channel, false);

	client->read_source_id = g_io_add_watch(client->channel,
						G_IO_IN|G_IO_ERR|G_IO_HUP,
						httpd_client_in_event, client);

	client->input = fifo_buffer_new(4096);
	client->state = REQUEST;

	return client;
}

static void
httpd_client_add_page_size(gpointer data, gpointer user_data)
{
	struct page *page = data;
	size_t *size = user_data;

	*size += page->size;
}

size_t
httpd_client_queue_size(const struct httpd_client *client)
{
	size_t size = 0;

	if (client->state != RESPONSE)
		return 0;

	g_queue_foreach(client->pages, httpd_client_add_page_size, &size);
	return size;
}

/* g_queue_clear() was introduced in GLib 2.14 */
#if !GLIB_CHECK_VERSION(2,14,0)
#define g_queue_clear(q) do { g_queue_free(q); q = g_queue_new(); } while (0)
#endif

void
httpd_client_cancel(struct httpd_client *client)
{
	if (client->state != RESPONSE)
		return;

	g_queue_foreach(client->pages, httpd_client_unref_page, NULL);
	g_queue_clear(client->pages);

	if (client->write_source_id != 0 && client->current_page == NULL) {
		g_source_remove(client->write_source_id);
		client->write_source_id = 0;
	}
}

static GIOStatus
write_page_to_channel(GIOChannel *channel,
		      const struct page *page, size_t position,
		      gsize *bytes_written_r, GError **error)
{
	assert(channel != NULL);
	assert(page != NULL);
	assert(position < page->size);

	return g_io_channel_write_chars(channel,
					(const gchar*)page->data + position,
					page->size - position,
					bytes_written_r, error);
}

static gboolean
httpd_client_out_event(GIOChannel *source,
		       G_GNUC_UNUSED GIOCondition condition, gpointer data)
{
	struct httpd_client *client = data;
	struct httpd_output *httpd = client->httpd;
	GError *error = NULL;
	GIOStatus status;
	gsize bytes_written;

	g_mutex_lock(httpd->mutex);

	assert(condition == G_IO_OUT);
	assert(client->state == RESPONSE);

	if (client->write_source_id == 0) {
		/* another thread has removed the event source while
		   this thread was waiting for httpd->mutex */
		g_mutex_unlock(httpd->mutex);
		return false;
	}

	if (client->current_page == NULL) {
		client->current_page = g_queue_pop_head(client->pages);
		client->current_position = 0;
	}

	status = write_page_to_channel(source, client->current_page,
				       client->current_position,
				       &bytes_written, &error);
	switch (status) {
	case G_IO_STATUS_NORMAL:
		client->current_position += bytes_written;
		assert(client->current_position <= client->current_page->size);

		if (client->current_position >= client->current_page->size) {
			page_unref(client->current_page);
			client->current_page = NULL;

			if (g_queue_is_empty(client->pages)) {
				/* all pages are sent: remove the
				   event source */
				client->write_source_id = 0;

				g_mutex_unlock(httpd->mutex);
				return false;
			}
		}

		g_mutex_unlock(httpd->mutex);
		return true;

	case G_IO_STATUS_AGAIN:
		g_mutex_unlock(httpd->mutex);
		return true;

	case G_IO_STATUS_EOF:
		/* client has disconnected */

		httpd_client_close(client);
		g_mutex_unlock(httpd->mutex);
		return false;

	case G_IO_STATUS_ERROR:
		/* I/O error */

		g_warning("failed to write to client: %s", error->message);
		g_error_free(error);

		httpd_client_close(client);
		g_mutex_unlock(httpd->mutex);
		return false;
	}

	/* unreachable */
	httpd_client_close(client);
	g_mutex_unlock(httpd->mutex);
	return false;
}

void
httpd_client_send(struct httpd_client *client, struct page *page)
{
	if (client->state != RESPONSE)
		/* the client is still writing the HTTP request */
		return;

	page_ref(page);
	g_queue_push_tail(client->pages, page);

	if (client->write_source_id == 0)
		client->write_source_id =
			g_io_add_watch(client->channel, G_IO_OUT,
				       httpd_client_out_event, client);
}