/*
 * Copyright (C) 2003-2011 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 "config.h"
#include "HttpdClient.hxx"
#include "HttpdInternal.hxx"
#include "util/fifo_buffer.h"
#include "page.h"
#include "icy_server.h"
#include "glib_socket.h"

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

#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "httpd_output"

HttpdClient::~HttpdClient()
{
	if (state == RESPONSE) {
		if (write_source_id != 0)
			g_source_remove(write_source_id);

		if (current_page != nullptr)
			page_unref(current_page);

		for (auto page : pages)
			page_unref(page);
	} else
		fifo_buffer_free(input);

	if (metadata)
		page_unref(metadata);

	g_source_remove(read_source_id);
	g_io_channel_unref(channel);
}

void
HttpdClient::Close()
{
	httpd_output_remove_client(httpd, this);
}

void
HttpdClient::LockClose()
{
	const ScopeLock protect(httpd->mutex);
	Close();
}

void
HttpdClient::BeginResponse()
{
	assert(state != RESPONSE);

	state = RESPONSE;
	write_source_id = 0;
	current_page = nullptr;

	httpd_output_send_header(httpd, this);
}

/**
 * Handle a line of the HTTP request.
 */
bool
HttpdClient::HandleLine(const char *line)
{
	assert(state != RESPONSE);

	if (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 == nullptr || strncmp(line + 1, "HTTP/", 5) != 0) {
			/* HTTP/0.9 without request headers */
			BeginResponse();
			return true;
		}

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

		if (g_ascii_strncasecmp(line, "Icy-MetaData: 1", 15) == 0) {
			/* Send icy metadata */
			metadata_requested = metadata_supported;
			return true;
		}

		if (g_ascii_strncasecmp(line, "transferMode.dlna.org: Streaming", 32) == 0) {
			/* Send as dlna */
			dlna_streaming_requested = true;
			/* metadata is not supported by dlna streaming, so disable it */
			metadata_supported = false;
			metadata_requested = false;
			return true;
		}

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

char *
HttpdClient::ReadLine()
{
	assert(state != RESPONSE);

	const ScopeLock protect(httpd->mutex);

	size_t length;
	const char *p = (const char *)fifo_buffer_read(input, &length);
	if (p == nullptr)
		/* empty input buffer */
		return nullptr;

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

	char *line = g_strndup(p, newline - p);
	fifo_buffer_consume(input, newline - p + 1);

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

/**
 * Sends the status line and response headers to the client.
 */
bool
HttpdClient::SendResponse()
{
	char buffer[1024];
	GError *error = nullptr;
	GIOStatus status;
	gsize bytes_written;

	assert(state == RESPONSE);

	if (dlna_streaming_requested) {
		g_snprintf(buffer, sizeof(buffer),
			   "HTTP/1.1 206 OK\r\n"
			   "Content-Type: %s\r\n"
			   "Content-Length: 10000\r\n"
			   "Content-RangeX: 0-1000000/1000000\r\n"
			   "transferMode.dlna.org: Streaming\r\n"
			   "Accept-Ranges: bytes\r\n"
			   "Connection: close\r\n"
			   "realTimeInfo.dlna.org: DLNA.ORG_TLAG=*\r\n"
			   "contentFeatures.dlna.org: DLNA.ORG_OP=01;DLNA.ORG_CI=0\r\n"
			   "\r\n",
			   httpd->content_type);

	} else if (metadata_requested) {
		gchar *metadata_header;

		metadata_header =
			icy_server_metadata_header(httpd->name, httpd->genre,
						   httpd->website,
						   httpd->content_type,
						   metaint);

		g_strlcpy(buffer, metadata_header, sizeof(buffer));

		g_free(metadata_header);

       } else { /* revert to a normal HTTP request */
		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",
			   httpd->content_type);
	}

	status = g_io_channel_write_chars(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 */

		Close();
		return false;

	case G_IO_STATUS_ERROR:
		/* I/O error */

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

		Close();
		return false;
	}

	/* unreachable */
	Close();
	return false;
}

bool
HttpdClient::Received()
{
	assert(state != RESPONSE);

	char *line;
	bool success;

	while ((line = ReadLine()) != nullptr) {
		success = HandleLine(line);
		g_free(line);
		if (!success) {
			assert(state != RESPONSE);
			return false;
		}

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

			fifo_buffer_free(input);

			return SendResponse();
		}
	}

	return true;
}

bool
HttpdClient::Read()
{
	size_t max_length;
	GError *error = nullptr;
	GIOStatus status;
	gsize bytes_read;

	if (state == RESPONSE) {
		/* the client has already sent the request, and he
		   must not send more */
		char buffer[1];

		status = g_io_channel_read_chars(channel, buffer,
						 sizeof(buffer), &bytes_read,
						 nullptr);
		if (status == G_IO_STATUS_NORMAL)
			g_warning("unexpected input from client");

		return false;
	}

	char *p = (char *)fifo_buffer_write(input, &max_length);
	if (p == nullptr) {
		g_warning("buffer overflow");
		return false;
	}

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

	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)
{
	HttpdClient *client = (HttpdClient *)data;

	if (condition == G_IO_IN && client->Read()) {
		return true;
	} else {
		client->LockClose();
		return false;
	}
}

HttpdClient::HttpdClient(httpd_output *_httpd, int _fd,
			 bool _metadata_supported)
	:httpd(_httpd),
	 channel(g_io_channel_new_socket(_fd)),
	 input(fifo_buffer_new(4096)),
	 state(REQUEST),
	 dlna_streaming_requested(false),
	 metadata_supported(_metadata_supported),
	 metadata_requested(false), metadata_sent(true),
	 metaint(8192), /*TODO: just a std value */
	 metadata(nullptr),
	 metadata_current_position(0), metadata_fill(0)
{
	/* GLib is responsible for closing the file descriptor */
	g_io_channel_set_close_on_unref(channel, true);
	/* NULL encoding means the stream is binary safe */
	g_io_channel_set_encoding(channel, nullptr, nullptr);
	/* we prefer to do buffering */
	g_io_channel_set_buffered(channel, false);

	read_source_id = g_io_add_watch(channel,
					GIOCondition(G_IO_IN|G_IO_ERR|G_IO_HUP),
					httpd_client_in_event, this);
}

size_t
HttpdClient::GetQueueSize() const
{
	if (state != RESPONSE)
		return 0;

	size_t size = 0;
	for (auto page : pages)
		size += page->size;
	return size;
}

void
HttpdClient::CancelQueue()
{
	if (state != RESPONSE)
		return;

	for (auto page : pages)
		page_unref(page);
	pages.clear();

	if (write_source_id != 0 && current_page == nullptr) {
		g_source_remove(write_source_id);
		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 != nullptr);
	assert(page != nullptr);
	assert(position < page->size);

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

static GIOStatus
write_n_bytes_to_channel(GIOChannel *channel, const struct page *page,
			 size_t position, gint n,
			 gsize *bytes_written_r, GError **error)
{
	GIOStatus status;

	assert(channel != nullptr);
	assert(page != nullptr);
	assert(position < page->size);

	if (n == -1) {
		status =  write_page_to_channel (channel, page, position,
						 bytes_written_r, error);
	} else {
		status = g_io_channel_write_chars(channel,
						  (const gchar*)page->data + position,
						  n, bytes_written_r, error);
	}

	return status;
}

int
HttpdClient::GetBytesTillMetaData() const
{
	if (metadata_requested &&
	    current_page->size - current_position > metaint - metadata_fill)
		return metaint - metadata_fill;

	return -1;
}

inline bool
HttpdClient::Write()
{
	GError *error = nullptr;
	GIOStatus status;
	gsize bytes_written;

	const ScopeLock protect(httpd->mutex);

	assert(state == RESPONSE);

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

	if (current_page == nullptr) {
		current_page = pages.front();
		pages.pop_front();
		current_position = 0;
	}

	const gint bytes_to_write = GetBytesTillMetaData();
	if (bytes_to_write == 0) {
		gint metadata_to_write;

		metadata_to_write = metadata_current_position;

		if (!metadata_sent) {
			status = write_page_to_channel(channel,
						       metadata,
						       metadata_to_write,
						       &bytes_written, &error);

			metadata_current_position += bytes_written;

			if (metadata->size - metadata_current_position == 0) {
				metadata_fill = 0;
				metadata_current_position = 0;
				metadata_sent = true;
			}
		} else {
			struct page *empty_meta;
			guchar empty_data = 0;

			empty_meta = page_new_copy(&empty_data, 1);

			status = write_page_to_channel(channel,
						       empty_meta,
						       metadata_to_write,
						       &bytes_written, &error);

			metadata_current_position += bytes_written;

			if (empty_meta->size - metadata_current_position == 0) {
				metadata_fill = 0;
				metadata_current_position = 0;
			}
		}

		bytes_written = 0;
	} else {
		status = write_n_bytes_to_channel(channel, current_page,
						  current_position, bytes_to_write,
						  &bytes_written, &error);
	}

	switch (status) {
	case G_IO_STATUS_NORMAL:
		current_position += bytes_written;
		assert(current_position <= current_page->size);

		if (metadata_requested)
			metadata_fill += bytes_written;

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

			if (pages.empty()) {
				/* all pages are sent: remove the
				   event source */
				write_source_id = 0;

				return false;
			}
		}

		return true;

	case G_IO_STATUS_AGAIN:
		return true;

	case G_IO_STATUS_EOF:
		/* client has disconnected */

		Close();
		return false;

	case G_IO_STATUS_ERROR:
		/* I/O error */

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

		Close();
		return false;
	}

	/* unreachable */
	Close();
	return false;
}

static gboolean
httpd_client_out_event(gcc_unused GIOChannel *source,
		       gcc_unused GIOCondition condition, gpointer data)
{
	assert(condition == G_IO_OUT);

	HttpdClient *client = (HttpdClient *)data;
	return client->Write();
}

void
HttpdClient::PushPage(struct page *page)
{
	if (state != RESPONSE)
		/* the client is still writing the HTTP request */
		return;

	page_ref(page);
	pages.push_back(page);

	if (write_source_id == 0)
		write_source_id = g_io_add_watch(channel, G_IO_OUT,
						 httpd_client_out_event,
						 this);
}

void
HttpdClient::PushMetaData(struct page *page)
{
	if (metadata) {
		page_unref(metadata);
		metadata = nullptr;
	}

	g_return_if_fail (page);

	page_ref(page);
	metadata = page;
	metadata_sent = false;
}
