/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */

/*
 *  Copyright (C) 2003 Hiroyuki Ikezoe
 *  Copyright (C) 2003 Takuro Ashie
 *
 *  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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <stdlib.h>
#define __USE_XOPEN
#include <time.h>

#include "gobject-utils.h"
#include "intl.h"
#include "kz-http.h"
#include "kazehakase.h"
#include "kz-profile.h"

#define BUFFER_SIZE 1024

enum {
	PROP_0,
	PROP_METHOD,
	PROP_URI,
	PROP_HOSTNAME,
	PROP_PATH,
};

const gchar *methods[] = {
	"GET",
	"HEAD",
};
static guint n_methods = G_N_ELEMENTS(methods);

static void kz_http_class_init      (KzHTTPClass *klass);
static void kz_http_init            (KzHTTP *http);
static void kz_http_set_property    (GObject *object,
				     guint prop_id,
				     const GValue *value,
				     GParamSpec *pspec);
static void kz_http_get_property    (GObject *object,
				     guint prop_id,
				     GValue *value,
				     GParamSpec *pspec);

static void      kz_http_connect         (KzHTTP *http);
static GIOStatus kz_http_in_header       (KzHTTP *http,
					  GIOChannel *iochannel);
static GIOStatus kz_http_in_body         (KzHTTP *http,
					  GIOChannel *iochannel);
static GIOStatus kz_http_in_chunked_body (KzHTTP *http,
					  GIOChannel *iochannel);

static void     cb_http_connect  (GTcpSocket *socket, 
				  GTcpSocketConnectAsyncStatus status,
				  gpointer data);

static void kz_http_error (KzHTTP *http);
static void kz_http_start (KzIO *io);

static GIOStatus kz_http_in    (KzIO *io, GIOChannel *iochannel);


GType
kz_http_method_type_get_type (void)
{
	static GType etype = 0;
	if (etype == 0) {
		static const GEnumValue values[] = {
			{ KZ_HTTP_METHOD_GET,  "KZ_HTTP_METHOD_GET",  "GET" },
			{ KZ_HTTP_METHOD_HEAD, "KZ_HTTP_METHOD_HEAD", "HEAD" },
			{ 0, NULL, NULL }
		};
		etype = g_enum_register_static ("KzHTTPMethodType", values);
	}
	return etype;
}


static KzIOClass *parent_class = NULL;

KZ_OBJECT_GET_TYPE(kz_http, "KzHTTP", KzHTTP,
		   kz_http_class_init, kz_http_init,
		   KZ_TYPE_IO)

static void
kz_http_class_init (KzHTTPClass *klass)
{
	GObjectClass *object_class;
	KzIOClass *io_class;

	parent_class = g_type_class_peek_parent (klass);
	object_class = (GObjectClass *) klass;
	io_class     = (KzIOClass *) klass;

	object_class->dispose      = kz_http_dispose;
	object_class->set_property = kz_http_set_property;
	object_class->get_property = kz_http_get_property;
	
	io_class->kz_io_in    = kz_http_in;
	io_class->kz_io_start = kz_http_start;

	g_object_class_install_property(
		object_class,
		PROP_METHOD,
		g_param_spec_enum(
			"method",
			_("Method"),
			_("Request Method"),
			KZ_TYPE_HTTP_METHOD_TYPE,
			KZ_HTTP_METHOD_GET,
			G_PARAM_READWRITE));
	g_object_class_install_property(
		object_class,
		PROP_URI,
		g_param_spec_string(
			"uri",
			_("URI"),
			_("The URI of Fetch file"),
			NULL,
			G_PARAM_READWRITE));
	g_object_class_install_property(
		object_class,
		PROP_HOSTNAME,
		g_param_spec_string(
			"hostname",
			_("Hostname"),
			_("The Hostname of the URI"),
			NULL,
			G_PARAM_READWRITE));
	g_object_class_install_property(
		object_class,
		PROP_PATH,
		g_param_spec_string(
			"path",
			_("Path"),
			_("The Path of the URI"),
			NULL,
			G_PARAM_READWRITE));
}


static void
kz_http_init (KzHTTP *http)
{
	http->sockID          = NULL;

	http->hostname        = NULL;
	http->path            = NULL;
	
	http->header          = TRUE;
	http->chunked         = FALSE;
	http->redirection     = FALSE;
	
	http->chunk_size      = 0;
}


void
kz_http_dispose (GObject *object)
{
	KzHTTP *http;

	g_return_if_fail(KZ_IS_HTTP(object));

	http = KZ_HTTP(object);

	if (http->hostname)
		g_free(http->hostname);
	if (http->path)
		g_free(http->path);
	
	http->hostname  = NULL;
	http->path      = NULL;

	if (G_OBJECT_CLASS (parent_class)->dispose)
		G_OBJECT_CLASS (parent_class)->dispose(object);
}


static void
kz_http_set_property (GObject *object,
		      guint prop_id,
		      const GValue *value,
		      GParamSpec *pspec)
{
	KzHTTP *http = KZ_HTTP(object);

	switch (prop_id)
	{
	case PROP_METHOD:
		http->method = g_value_get_enum(value);
		break;
	case PROP_URI:
		g_free(KZ_IO(http)->uri);
		KZ_IO(http)->uri = g_value_dup_string(value);
		break;
	case PROP_HOSTNAME:
		g_free(http->hostname);
		http->hostname = g_value_dup_string(value);
		break;
	case PROP_PATH:
		g_free(http->path);
		http->path = g_value_dup_string(value);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}
}


static void
kz_http_get_property (GObject *object,
		      guint prop_id,
		      GValue *value,
		      GParamSpec *pspec)
{
	KzHTTP *http = KZ_HTTP(object);

	switch (prop_id)
	{
	case PROP_METHOD:
		g_value_get_enum(value);
		break;
	case PROP_URI:
		g_value_set_string(value, KZ_IO(http)->uri);
		break;
	case PROP_HOSTNAME:
		g_value_set_string(value, http->hostname);
		break;
	case PROP_PATH:
		g_value_set_string(value, http->path);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}
}


KzHTTP *
kz_http_new (const gchar *uri)
{
	KzHTTP *http;
	GURI *guri;
	gchar *path = NULL, *hostname = NULL;

	guri = gnet_uri_new(uri);

	if (guri)
	{
		hostname = guri->hostname;
		if (guri->query)
			path = g_strdup_printf("%s?%s",
					       guri->path, guri->query);
		else
			path = g_strdup(guri->path);
	}

	
	http = KZ_HTTP(g_object_new(KZ_TYPE_HTTP,
				    "uri",      uri,
				    "hostname", hostname,
				    "path",     path,
				    NULL));
	
	if (guri)
		gnet_uri_delete(guri);
	g_free(path);

	return http;
}


static void
kz_http_connect (KzHTTP *http)
{
	GURI *uri = NULL;
	gchar proxy[1024];
	gboolean exist;
	guint port = 80;
	gchar *host;

	/* proxy check */
	exist = KZ_CONF_GET("Global", "proxy", proxy, STRING);
	if (exist)
	{
		uri = gnet_uri_new(proxy);
		http->use_proxy = TRUE;
		port = uri->port;
		host = uri->hostname;
	}
	else 
	{
		host = http->hostname;
	}

	http->sockID = gnet_tcp_socket_connect_async(host, port,
						     cb_http_connect, http);
	
	if(uri)
		gnet_uri_delete(uri);
}


static GIOStatus
kz_http_in_header(KzHTTP *http, GIOChannel *iochannel)
{
	GIOStatus iostatus;
	GString *buffer = NULL;
	struct tm t;

	buffer = g_string_sized_new(0);
	/* Read the data into our buffer */
	iostatus = g_io_channel_read_line_string(iochannel,
						 buffer,
						 NULL,
						 NULL);
	
	if (iostatus == G_IO_STATUS_ERROR)
		return iostatus;
	
	if (strncmp(buffer->str, "HTTP/1.1", 8) == 0)
	{
		switch (buffer->str[9])
		{
		 case '2': /* 2xx Succeccful */
			break;
		 case '3': /* 3xx Redirection   */
			http->redirection = TRUE;
			break;
		 case '1': /* 1xx Informational */
		 case '4': /* 4xx Client Error  */
		 case '5': /* 5xx Server Error  */
		 default:
			{
				g_warning("%s", buffer->str);
				iostatus = G_IO_STATUS_ERROR;
				break;
			}
		}
	}
	else if (strncmp(buffer->str, "Content-Length:", 15) == 0)
	{
		KZ_IO(http)->file_size = strtol(buffer->str + 15, NULL, 10);
	}
	else if (strncmp(buffer->str, "Transfer-Encoding:", 18) == 0)
	{
		if(strncasecmp(buffer->str + 19, "chunked", 7) == 0)  
			http->chunked = TRUE;
	}
	else if (strncmp(buffer->str, "Location:", 9) == 0)
	{
		http->location = g_strdup(buffer->str + 10);
	}
	else if (strncmp(buffer->str, "Last-Modified:", 15) == 0)
	{
		strptime(buffer->str + 15,
			 "%a, %d %b %Y %H:%M:%S %z", &t);
		KZ_IO(http)->last_modified = (guint)mktime(&t);
	}
	else if (strncmp(buffer->str, "\r\n", 2) == 0) /* End of Header*/ 
	{
		http->header = FALSE;
	}

	g_string_free(buffer, TRUE);
	
	return iostatus;
}


static GIOStatus
kz_http_in_chunked_body(KzHTTP *http, GIOChannel *iochannel)
{
	GIOStatus iostatus = G_IO_STATUS_NORMAL;
	GError *e = NULL;
	gchar *buffer = NULL;
	gsize bytes_read, bytes_written;

	/* These codes are quite silly! FIX!! */
	/* Get chunk size */
	if (http->chunk_size <= 0)
	{
		iostatus = g_io_channel_read_line(iochannel,
						  &buffer,
						  &bytes_read,
						  NULL,
						  &e);
		if (iostatus == G_IO_STATUS_NORMAL)
		{
			http->chunk_size = strtol(buffer, NULL, 16);
			if (buffer)
			{
				g_free(buffer);
				buffer = NULL;
			}
			/* check EOF */
			if (http->chunk_size == 0)
				iostatus = G_IO_STATUS_EOF;
		}
	}
	if (iostatus == G_IO_STATUS_NORMAL)
	{
		/* Get a chunked body */
		buffer = g_new0(gchar, http->chunk_size);
		iostatus = g_io_channel_read_chars(iochannel, buffer,
						   http->chunk_size,
						   &bytes_read,
						   &e);
		if (iostatus == G_IO_STATUS_NORMAL)
		{
			if(KZ_IO(http)->inmemory)
				g_string_append_len(KZ_IO(http)->memory_buffer, buffer, bytes_read);
			else
			{
				iostatus = g_io_channel_write_chars(KZ_IO(http)->tempio,
								    buffer,
								    bytes_read,
								    &bytes_written,
								    NULL);
			}
			KZ_IO(http)->bytes_loaded += bytes_read;
			http->chunk_size   -= bytes_read;
		}
		if (buffer)
		{
			g_free(buffer);
			buffer = NULL;
		}
	}
	if (iostatus == G_IO_STATUS_NORMAL)
	{
		/* Get CRLF */
		if (http->chunk_size <= 0)
		{
			iostatus = g_io_channel_read_line(iochannel,
							  &buffer,
							  &bytes_read,
							  NULL,
							  &e);
			if (buffer)
				g_free(buffer);
		}
	}

	if (e)
		g_error_free(e);

	/* emit io_in signal */
	KZ_IO_CLASS (parent_class)->emit_io_in_signal(KZ_IO(http), bytes_read, buffer);

	return iostatus;
}


static GIOStatus
kz_http_in_body(KzHTTP *http, GIOChannel *iochannel)
{
	GIOStatus iostatus;
	gsize bytes_read, bytes_written;
	gchar buffer[BUFFER_SIZE];

	/* Read the data into our buffer */
	iostatus = g_io_channel_read_chars(iochannel, buffer, 
					   sizeof(buffer),
					   &bytes_read,
					   NULL);

	if (iostatus == G_IO_STATUS_NORMAL)
	{	
		if(KZ_IO(http)->inmemory)
			g_string_append_len(KZ_IO(http)->memory_buffer, buffer, bytes_read);
		else
		{
			iostatus = g_io_channel_write_chars(KZ_IO(http)->tempio,
							    buffer,
							    bytes_read,
							    &bytes_written,
							    NULL);
		}
		KZ_IO(http)->bytes_loaded += bytes_read;
		/* Check for EOF */
		if (bytes_read == 0)
			iostatus = G_IO_STATUS_EOF;
	}
	/* emit io_in signal */
	KZ_IO_CLASS (parent_class)->emit_io_in_signal(KZ_IO(http), bytes_read, buffer);

	return iostatus;
}


static GIOStatus
kz_http_in (KzIO *io, GIOChannel *iochannel)
{
	KzHTTP *http;
	GIOStatus iostatus;

	g_return_val_if_fail(KZ_IS_HTTP(io), G_IO_STATUS_ERROR);

	http = KZ_HTTP(io);

	if (http->header) /* Header Section */
		iostatus = kz_http_in_header(http, iochannel);
	else if (http->chunked) /* Chunked Body */
		iostatus = kz_http_in_chunked_body(http, iochannel);
	else 		/* Entity-Body Section */
		iostatus = kz_http_in_body(http, iochannel);
	
	if (iostatus == G_IO_STATUS_EOF && http->redirection)
	{
		g_object_set(G_OBJECT(http),
			     "uri", g_strchomp(http->location), NULL);
		iostatus = G_IO_STATUS_AGAIN;
	}

	return iostatus;
}


static void 
cb_http_connect(GTcpSocket *socket,
		GTcpSocketConnectAsyncStatus status, gpointer data)
{
	KzHTTP *http;
	GIOStatus iostatus;
	gchar *command;
	const gchar *method = methods[0];
	guint n;
	gchar *URL;

	http = KZ_HTTP(data);

	if ( status != GTCP_SOCKET_CONNECT_ASYNC_STATUS_OK )
	{
		kz_http_error(http);
		return;
	}

	/* Get the IOChannel */
	KZ_IO(http)->iochannel = gnet_tcp_socket_get_io_channel(socket);
	if (KZ_IO(http)->iochannel == NULL)
	{
		kz_http_error(http);
		return;
	}

	g_io_channel_set_flags(KZ_IO(http)->iochannel,
			       G_IO_FLAG_NONBLOCK, NULL);

	/* proxy */
	if (http->use_proxy)
	{
		URL = g_strdup_printf("http://%s%s",
				      http->hostname,
				      http->path);
	}
	else
	{
		URL = g_strdup(http->path);
	}
	
	/* Set method */
	if (http->method >= 0 && http->method < n_methods)
		method = methods[http->method];
	else
		g_warning("KzHTTP: Invalid method type was specified!");
		
	/* Send GET command */
	command = g_strconcat(method, " ",
			      URL, " HTTP/1.1\r\n",
			      "Host: ", http->hostname, "\r\n",
			      "User-Agent: Kazehakase/"VERSION "\r\n",
			      "Connection: close\r\n\r\n", NULL);

	iostatus = g_io_channel_write_chars(KZ_IO(http)->iochannel,
					    command,
					    strlen(command),
					    &n,
					    NULL);

	g_free(command);
	g_free(URL);

	if (iostatus != G_IO_STATUS_NORMAL)
		kz_http_error(http);

	/* call kz_io_set_iochannel, start to loading */
	KZ_IO_CLASS (parent_class)->set_io_channel(KZ_IO(http));
}


static void
kz_http_error (KzHTTP *http)
{
	g_return_if_fail(KZ_IS_HTTP(http));

	/* emit error signal */
	KZ_IO_CLASS (parent_class)->emit_error_signal(KZ_IO(http));
}


static void 
kz_http_start (KzIO *io)
{
	KzHTTP *http;
	
	g_return_if_fail(KZ_IS_HTTP(io));

	http = KZ_HTTP(io);

	kz_http_connect(http);
}
