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

/*
 *  Copyright (C) 2003 Hiroyuki Ikezoe
 *
 *  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 <string.h>
#include <gtk/gtk.h>
#include <unistd.h>
#include <pthread.h>
#include <ghttp.h>

#include "prefs.h"
#include "kz-rss.h"
#include "gobject-utils.h"

enum {
   START_SIGNAL,
   UPDATE_START_SIGNAL,
   UPDATE_COMPLETED_SIGNAL,
   LAST_SIGNAL
};

static void kz_rss_class_init   (KzRSSClass *class);
static void kz_rss_init         (KzRSS *rss);
static void kz_rss_dispose      (GObject *object);

static gchar   *kz_rss_get_encoding(gchar *src);
static gchar   *kz_rss_convert_to_utf8(gchar *src, guint src_len, guint *dest_len);
static void     kz_rss_file_fetch(KzRSS *rss);
static gboolean kz_rss_update(KzRSS *rss);
static void     kz_rss_items_update(KzRSS *rss);
static void     kz_rss_items_dispose(KzRSS *rss);

static GObjectClass *parent_class = NULL;
static gint kz_rss_signals[LAST_SIGNAL] = {0};

KZ_OBJECT_GET_TYPE(kz_rss, "KzRSS", KzRSS,
		   kz_rss_class_init, kz_rss_init,
		   G_TYPE_OBJECT)

static void
kz_rss_class_init (KzRSSClass *class)
{
	GObjectClass *object_class;

	parent_class = g_type_class_peek_parent (class);
	object_class = (GObjectClass *) class;

	kz_rss_signals[UPDATE_COMPLETED_SIGNAL]
		= g_signal_new ("update_start",
				G_TYPE_FROM_CLASS (class),
				G_SIGNAL_RUN_LAST,
				G_STRUCT_OFFSET (KzRSSClass, update_start),
				NULL, NULL,
				g_cclosure_marshal_VOID__VOID,
				G_TYPE_NONE, 0);

	kz_rss_signals[UPDATE_COMPLETED_SIGNAL]
		= g_signal_new ("update_completed",
				G_TYPE_FROM_CLASS (class),
				G_SIGNAL_RUN_LAST,
				G_STRUCT_OFFSET (KzRSSClass, update_completed),
				NULL, NULL,
				g_cclosure_marshal_VOID__VOID,
				G_TYPE_NONE, 0);

	object_class->dispose = kz_rss_dispose;

	class->update_start     = NULL;
	class->update_completed = NULL;
}

static void
kz_rss_init (KzRSS *rss)
{
	rss->items = NULL;
	rss->title = NULL;
	rss->uri   = NULL;
	rss->rss_title       = NULL;
	rss->rss_link        = NULL;
	rss->rss_description = NULL;
	rss->rss_date        = NULL;

	rss->state = KZ_RSS_NORMAL;
}

static void
kz_rss_dispose (GObject *object)
{
	KzRSS *rss;

	g_return_if_fail(KZ_IS_RSS(object));

	rss = KZ_RSS(object);

	kz_rss_items_dispose(rss);
	if (rss->uri)
		g_free(rss->uri);
	if (rss->title)
		g_free(rss->title);

	rss->uri = NULL;
	rss->title = NULL;

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

KzRSS *
kz_rss_new(gchar *uri, gchar *title, guint update_interval)
{
	KzRSS *rss = KZ_RSS(g_object_new(KZ_TYPE_RSS, NULL));

	rss->uri             = g_strdup(uri);
	rss->title           = g_strdup(title);
	
	rss->update_interval = update_interval;
	kz_rss_update(rss);
	if (rss->update_interval != 0)
	{
		gtk_timeout_add(60000 * rss->update_interval, (void *)&kz_rss_update, rss);
	}
	return rss;
}


typedef enum {
	STATE_START   = (1 << 0),
	STATE_ROOT    = (1 << 1),
	STATE_CHANNEL = (1 << 2),
	STATE_ITEM    = (1 << 3),
	STATE_TITLE   = (1 << 4),
	STATE_LINK    = (1 << 5),
	STATE_DESC    = (1 << 6),
	STATE_END     = (1 << 7),
	STATE_ERROR   = (1 << 8)
} ParseState;

typedef struct _ParseContext ParseContext;
struct _ParseContext
{
	/* parser state information */
	ParseState state;
	ParseState current_state;

	KzRSS *rss;
};

static void
start_element_handler (GMarkupParseContext *context,
		       const gchar         *element_name,
		       const gchar        **attribute_names,
		       const gchar        **attribute_values,
		       gpointer             user_data,
		       GError             **error)
{
	ParseContext *ctx = user_data;

	ctx->current_state = STATE_ERROR;
	
	switch (element_name[0])
	{
	 case 'r':
		if (ctx->state == STATE_START && 
		    (!strcmp(element_name, "rdf:RDF") ||
		     !strcmp(element_name, "rss")))
		{
			ctx->state = STATE_ROOT;
			ctx->current_state = STATE_ROOT;
		}
		break;
	 case 'i':
		if ((ctx->state & STATE_ROOT) && !strcmp(element_name, "item")
		    && strcmp(element_name, "items"))
		{
			ctx->state |= STATE_ITEM;
			ctx->current_state = STATE_ITEM;
			/* append new KzRSSItem */
			KzRSSItem *item = g_new0(KzRSSItem, 1);
			item->rss = ctx->rss;
			item->title = NULL;
			item->link = NULL;
			item->description = NULL;

			if (!ctx->rss->items)
				ctx->rss->items = g_slist_append(ctx->rss->items, item);
			else
				g_slist_append(ctx->rss->items,item);
		}
		break;
	 case 'c':
		if ((ctx->state & STATE_ROOT) && !strcmp(element_name, "channel"))
		{
			ctx->state |= STATE_CHANNEL;
			ctx->current_state = STATE_CHANNEL;
		}
		break;
	 case 't':
		if (!strcmp(element_name, "title"))
		{
			ctx->state |= STATE_TITLE;
			ctx->current_state = STATE_TITLE;
		}
		break;
	 case 'l':
		if (!strcmp(element_name, "link"))
		{
			ctx->state |= STATE_LINK;
			ctx->current_state = STATE_LINK;
		}
		break;
	 case 'd':
		if (!strcmp(element_name, "description"))
		{
			ctx->state |= STATE_DESC;
			ctx->current_state = STATE_DESC;
		}
		break;
	}
}

static void
end_element_handler (GMarkupParseContext *context,
		     const gchar         *element_name,
		     gpointer             user_data,
		     GError             **error)
{
	ParseContext *ctx = user_data;
	
	ctx->state = ctx->state & ~(ctx->current_state);
	ctx->current_state = STATE_ERROR;
}

static void 
text(GMarkupParseContext *context,
     const gchar          *text,
     gsize                 text_len,
     gpointer              user_data,
     GError              **error)
{
	ParseContext *ctx = user_data;

	if (ctx->current_state == STATE_ERROR)
		return;
	if (ctx->state & STATE_ITEM)
	{
		guint pos = g_slist_length(ctx->rss->items);
		KzRSSItem *item = g_slist_nth_data(ctx->rss->items, pos - 1);
		switch (ctx->current_state)
		{
		 case STATE_TITLE:
			item->title = g_strndup(text, text_len);
			break;
		 case STATE_LINK:
			item->link = g_strndup(text, text_len);
			break;
		 case STATE_DESC:
			item->description = g_strndup(text, text_len);
			break;
		default:
			break;
		}
			
	}
	else if (ctx->state & STATE_CHANNEL)
	{
		switch (ctx->current_state)
		{
		 case STATE_TITLE:
			if (ctx->rss->rss_title)
				g_free(ctx->rss->rss_title);
			ctx->rss->rss_title = g_strndup(text, text_len);
			break;
		 case STATE_LINK:
			if (ctx->rss->rss_link)
				g_free(ctx->rss->rss_link);
			ctx->rss->rss_link = g_strndup(text, text_len);
			break;
		 case STATE_DESC:
			if (ctx->rss->rss_description)
				g_free(ctx->rss->rss_description);
			ctx->rss->rss_description = g_strndup(text, text_len);
			break;
		default:
			break;
		}
	}
}

static GMarkupParser ui_parser = {
	start_element_handler,
	end_element_handler,
	text,
	NULL,
	NULL
};

static gboolean
kz_rss_parse_from_string (KzRSS *rss, gpointer user_data,
			  const gchar *buffer, guint length,
			  GError **error)
{
	ParseContext ctx = { 0 };
	GMarkupParseContext *context;
	gboolean res = TRUE;

	g_return_val_if_fail(buffer != NULL, FALSE);

	ctx.state = STATE_START;
	ctx.current_state = STATE_START;
	ctx.rss = rss;

	context = g_markup_parse_context_new(&ui_parser, 0, &ctx, NULL);
	if (length < 0)
		length = strlen(buffer);

	if (g_markup_parse_context_parse(context, buffer, length, error))
	{
		if (!g_markup_parse_context_end_parse(context, error))
			res = FALSE;
	}
	else
		res = FALSE;

	g_markup_parse_context_free (context);

	return res;
}

static gboolean
kz_rss_parse_from_file (KzRSS *rss, gpointer user_data,
			const gchar *filename, GError **error)
{
	gchar *buffer;
	gint length;
	gboolean res;

	if (!g_file_get_contents (filename, &buffer, &length, error))
		return FALSE;

	res = kz_rss_parse_from_string(rss, user_data, buffer, length, error);
	g_free(buffer);

	return res;
}

static gchar *
kz_rss_get_encoding(gchar *src)
{
	gchar *encoding = NULL;
	gchar **lines = NULL;
	gchar *prev_pos, *next_pos;
	
	g_return_val_if_fail(src, NULL);

	lines = g_strsplit(src, "\n", 1);
	prev_pos =  strstr(lines[0], "encoding");
	if (prev_pos)
	{
		while (*prev_pos != '"' && *prev_pos != '\0')
		{
			prev_pos++;
		}
		if (prev_pos != '\0')
		{
			prev_pos++;
			next_pos = strchr(prev_pos, '"');
			if (next_pos)
				encoding = g_strndup(prev_pos, 
						     next_pos - prev_pos);
			if (!strcasecmp(encoding, "UTF-8"))
			{
				g_free(encoding);
				encoding = NULL;
			}
		}
	}

	g_strfreev(lines);
	return encoding;
}

static gchar *
kz_rss_convert_to_utf8(gchar *src, guint src_len, guint *dest_len)
{
	gchar *dest = NULL;
	gchar *encoding = NULL;
	guint read_len;

	encoding = kz_rss_get_encoding(src);

	if(encoding)
	{
		dest = g_convert(src, src_len, "UTF-8", encoding,
				 &read_len, dest_len, NULL);
		g_free(encoding);
	}

	return dest;
}

static void
kz_rss_file_fetch(KzRSS *rss)
{
	gchar *body = NULL;
	gchar *utf8_body = NULL;
	guint len, utf8_len;

	ghttp_request *request= NULL;

	request = ghttp_request_new();
	
	ghttp_set_uri(request, rss->uri);
	if (prefs.proxy)
		ghttp_set_proxy(request, prefs.proxy);
	ghttp_set_header(request, http_hdr_Connection, "close");
	ghttp_prepare(request);
	
	ghttp_process(request);

	len = ghttp_get_body_len(request);
	body = ghttp_get_body(request);

	/* check encoding and convert if need */
	utf8_body = kz_rss_convert_to_utf8(body, len, &utf8_len);
	if(utf8_body)
	{
		kz_rss_parse_from_string(rss, NULL, utf8_body, utf8_len, NULL);
		g_free(utf8_body);
	}
	else
		kz_rss_parse_from_string(rss, NULL, body, len, NULL);
	
	ghttp_request_destroy(request);
}

static void 
kz_rss_items_update(KzRSS *rss)
{
	g_return_if_fail(KZ_IS_RSS(rss));

	kz_rss_items_dispose(rss);
	kz_rss_file_fetch(rss);

	rss->state = KZ_RSS_NORMAL;
	g_signal_emit(G_OBJECT (rss),
		      kz_rss_signals[UPDATE_COMPLETED_SIGNAL],
		      0);
}

static void 
kz_rss_items_dispose(KzRSS *rss)
{
	if (rss->items)
	{
		GSList *node = rss->items;
		for(; node; node = g_slist_next(node))
		{
			KzRSSItem *item = node->data;
			if(item->title)
				g_free(item->title);
			if(item->link)
				g_free(item->link);
			if(item->description)
				g_free(item->description);
			if(item->date)
				g_free(item->date);
		}
		g_slist_free(rss->items);
		rss->items = NULL;
	}
}

static gboolean
kz_rss_update(KzRSS *rss)
{
	if (rss->state == KZ_RSS_LOADING)
		return TRUE;
	rss->state = KZ_RSS_LOADING;
	pthread_t t;
	pthread_create(&t, NULL, (void *)kz_rss_items_update, (void *)rss);
	pthread_detach(t);
	return TRUE;
}

void
kz_rss_force_update(KzRSS *rss)
{
	if (rss->state == KZ_RSS_LOADING)
		return;

	kz_rss_update(rss);
}
