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

/*
 *  Copyright (C) 2003 Hiroyuki Ikezoe
 *  Copyright (C) 2003 Takuro Ashie
 *  Copyright (C) 2004 Hidetaka Iwai
 *
 *  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 <glib/gi18n.h>

#include "gobject-utils.h"
#include "kz-http.h"
#include "gnet.h"
#include "kazehakase.h"
#include "kz-profile.h"
#include "kz-proxy-item.h"

#define BUFFER_SIZE 256

enum {
	PROP_0,
	PROP_METHOD,
	PROP_HOSTNAME,
	PROP_PATH
};

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

struct _KzHTTPPrivate 
{
	GIOChannel *iochannel;

	GTcpSocket *socket;

	KzHTTPMethodType method; /* Access Method */
	
	gchar *hostname;
	gchar *path;

	gboolean header;
	gboolean use_proxy;

	gboolean chunked; /* Transfer-Encoding is chunked or not */
	gsize chunk_size;

	gboolean redirection; /* for Redirection 3xx */
	gchar    *location;   /* Redirect-URI */

	gchar *post_data;
};

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_dispose         (GObject *object);
static void kz_http_finalize        (GObject *object);

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 GIOStatus kz_http_read_from_io    (KzIO *io,
					  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 void     kz_http_set_chunked_mode (KzHTTP *http);
static void     kz_http_set_redirection  (KzHTTP *http);
static gboolean kz_http_use_proxy        (KzHTTP *http);
static gboolean kz_http_is_in_header     (KzHTTP *http);
static gboolean kz_http_is_chunked_mode  (KzHTTP *http);
static gboolean kz_http_is_redirection   (KzHTTP *http);

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" },
			{ KZ_HTTP_METHOD_POST, "KZ_HTTP_METHOD_POST", "POST" },
			{ 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->finalize     = kz_http_finalize;
	object_class->set_property = kz_http_set_property;
	object_class->get_property = kz_http_get_property;
	
	io_class->read_from_io  = kz_http_read_from_io;
	io_class->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_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->priv = g_new0(KzHTTPPrivate, 1);

	http->priv->socket          = NULL;

	http->priv->hostname        = NULL;
	http->priv->path            = NULL;
	
	http->priv->header          = TRUE;
	http->priv->chunked         = FALSE;
	http->priv->redirection     = FALSE;

	http->priv->chunk_size      = 0;

	http->priv->post_data       = NULL;
}


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

	g_return_if_fail(KZ_IS_HTTP(object));

	http = KZ_HTTP(object);

	if (http->priv->socket)
		gnet_tcp_socket_unref(http->priv->socket);
	if (http->priv->hostname)
		g_free(http->priv->hostname);
	if (http->priv->path)
		g_free(http->priv->path);
	if (http->priv->post_data)
		g_free(http->priv->post_data);
	
	http->priv->socket    = NULL;
	http->priv->hostname  = NULL;
	http->priv->path      = NULL;
	http->priv->post_data = NULL;

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


KZ_OBJECT_FINALIZE(kz_http, KzHTTP)


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->priv->method = g_value_get_enum(value);
		break;
	case PROP_HOSTNAME:
		g_free(http->priv->hostname);
		http->priv->hostname = g_value_dup_string(value);
		break;
	case PROP_PATH:
		g_free(http->priv->path);
		http->priv->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_HOSTNAME:
		g_value_set_string(value, http->priv->hostname);
		break;
	case PROP_PATH:
		g_value_set_string(value, http->priv->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;
}


KzHTTP *
kz_http_post_new (const gchar *uri, const gchar *post_data)
{
	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,
				    "method",   KZ_HTTP_METHOD_POST,
				    "uri",      uri,
				    "hostname", hostname,
				    "path",     path,
				    NULL));
	
	http->priv->post_data = g_strdup(post_data);
	
	if (guri)
		gnet_uri_delete(guri);
	g_free(path);

	return http;
}


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

	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   */
			kz_http_set_redirection(http);
			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)
	{
		guint size = (guint)strtol(buffer->str + 15, NULL, 10);
		g_object_set(G_OBJECT(KZ_IO(http)),
			     "file_size", size, NULL);
	}
	else if (strncmp(buffer->str, "Transfer-Encoding:", 18) == 0)
	{
		if(strncasecmp(buffer->str + 19, "chunked", 7) == 0)  
			kz_http_set_chunked_mode(http);
	}
	else if (strncmp(buffer->str, "Location:", 9) == 0)
	{
		http->priv->location = g_strdup(buffer->str + 10);
	}
	else if (strncmp(buffer->str, "Last-Modified:", 15) == 0)
	{
		struct tm t;
		strptime(buffer->str + 15,
			 "%a, %d %b %Y %H:%M:%S %z", &t);
		g_object_set(G_OBJECT(KZ_IO(http)),
			     "last_modified", (guint)mktime(&t), NULL);
	}
	else if (strncmp(buffer->str, "\r\n", 2) == 0) /* End of Header*/ 
	{
		http->priv->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;

	/* These codes are quite silly! FIX!! */
	/* Get chunk size */
	if (http->priv->chunk_size <= 0)
	{
		iostatus = g_io_channel_read_line(iochannel,
						  &buffer,
						  &bytes_read,
						  NULL,
						  &e);
		if (iostatus == G_IO_STATUS_NORMAL)
		{
			http->priv->chunk_size = strtol(buffer, NULL, 16);
			if (buffer)
			{
				g_free(buffer);
				buffer = NULL;
			}
			/* check EOF */
			if (http->priv->chunk_size == 0)
				iostatus = G_IO_STATUS_EOF;
		}
	}
	if (iostatus == G_IO_STATUS_NORMAL)
	{
		/* Get a chunked body */
		buffer = g_new0(gchar, http->priv->chunk_size);
		iostatus = g_io_channel_read_chars(iochannel, buffer,
						   http->priv->chunk_size,
						   &bytes_read,
						   &e);
		if (iostatus == G_IO_STATUS_NORMAL)
		{
			KZ_IO_CLASS (parent_class)->io_to_buffer(KZ_IO(http), bytes_read, buffer);

			http->priv->chunk_size -= bytes_read;
		}
		if (buffer)
		{
			g_free(buffer);
			buffer = NULL;
		}
	}
	if (iostatus == G_IO_STATUS_NORMAL)
	{
		/* Get CRLF */
		if (http->priv->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);

	return iostatus;
}


static GIOStatus
kz_http_in_body(KzHTTP *http, GIOChannel *iochannel)
{
	GIOStatus iostatus;
	gsize bytes_read;
	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)
	{	
		KZ_IO_CLASS (parent_class)->io_to_buffer(KZ_IO(http), bytes_read, buffer);
		if (bytes_read == 0)
			iostatus = G_IO_STATUS_EOF;
	}

	return iostatus;
}


static GIOStatus
kz_http_read_from_io (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 (kz_http_is_in_header(http)) /* Header Section */
		iostatus = kz_http_in_header(http, iochannel);
	else if (kz_http_is_chunked_mode(http)) /* 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 && kz_http_is_redirection(http))
	{
		g_object_set(G_OBJECT(http),
			     "uri", g_strchomp(http->priv->location), NULL);
		iostatus = G_IO_STATUS_AGAIN;
	}

	return iostatus;
}


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

	http = KZ_HTTP(data);

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

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

	http->priv->socket = socket;
	g_io_channel_ref(iochannel);
	KZ_IO(http)->iochannel = iochannel;

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

	/* proxy */
	if (kz_http_use_proxy(http))
	{
		URL = g_strdup_printf("http://%s%s",
				      http->priv->hostname,
				      http->priv->path);
	}
	else
	{
		URL = g_strdup(http->priv->path);
	}

	/* Set method */
	if (http->priv->method >= 0 && http->priv->method < n_methods)
		method = methods[http->priv->method];
	else
		g_warning("KzHTTP: Invalid method type was specified!");
		
	/* Send GET command */
	if (http->priv->method == KZ_HTTP_METHOD_POST)
	{
		gchar *c_len;
		if (http->priv->post_data)
			c_len = g_strdup_printf("%d", strlen(http->priv->post_data));
		else
			c_len = g_strdup("0");

		command = g_strconcat(method, " ",
				      URL, " HTTP/1.1\r\n",
				      "Host: ", http->priv->hostname, "\r\n",
				      "User-Agent: Kazehakase/"VERSION "\r\n",
				      "Content-Type: text/xml\r\n",
				      "Content-Length: ", c_len, "\r\n\r\n", 
				      http->priv->post_data, "\r\n", NULL);
		g_free(c_len);
	}
	else
	{
		command = g_strconcat(method, " ",
				      URL, " HTTP/1.1\r\n",
				      "Host: ", http->priv->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);
		return;
	}

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

static void
kz_http_start (KzIO *io)
{
	gchar proxy_name[1024];
	gboolean exist, use_proxy;
	gchar *http_host = NULL;
	guint http_port;
	KzHTTP *http;
	KzProxyItem *item = NULL;
	
	g_return_if_fail(KZ_IS_HTTP(io));
	http = KZ_HTTP(io);
	
	/* proxy check */
	KZ_CONF_GET("Global", "use_proxy", use_proxy, BOOL);
	if (!use_proxy)
		goto NO_PROXY;
	
	exist = KZ_CONF_GET("Global", "proxy_name", proxy_name, STRING);
	if (!exist)
	{
		/* We can find no proxy setting, so connect without proxy. */
		goto NO_PROXY;
	}

	item = kz_proxy_find(proxy_name);
	if(!item)
	{
		/* There is no proxy object whose name is proxy_name, 
		   so connect without proxy */
		goto NO_PROXY;
	}

	/* There is a valid proxy setting, so connect with this proxy*/
	http->priv->use_proxy = TRUE;
	gnet_tcp_socket_connect_async(item->http_host, item->http_port,
			 	      cb_http_connect, http);

	g_object_unref(G_OBJECT(item));
	return;
	
NO_PROXY:
	http_host = http->priv->hostname;
	http_port = 80;
	gnet_tcp_socket_connect_async(http_host, http_port,
				      cb_http_connect, http);
	return;
}



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

	KZ_IO_CLASS (parent_class)->io_error(KZ_IO(http));
}


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

	http->priv->chunked = TRUE;
}

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

	http->priv->redirection = TRUE;
}

static gboolean
kz_http_use_proxy (KzHTTP *http)
{
	return http->priv->use_proxy;
}

static gboolean
kz_http_is_in_header (KzHTTP *http)
{
	return http->priv->header;
}

static gboolean
kz_http_is_chunked_mode (KzHTTP *http)
{
	return http->priv->chunked;
}

static gboolean
kz_http_is_redirection (KzHTTP *http)
{
	return http->priv->redirection;
}
