/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/* camel-nntp-newsrc.c - .newsrc parsing/regurgitating code */
/* 
 *
 * Copyright (C) 2000 Ximian, Inc. <toshok@ximian.com>
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

#include <config.h>

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <glib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include "camel-nntp-newsrc.h"
#include <camel/camel-folder-summary.h>

#ifdef ENABLE_THREADS
#include <pthread.h>

#define NEWSRC_LOCK(f, l) (g_mutex_lock(((CamelNNTPNewsrc *)f)->l))
#define NEWSRC_UNLOCK(f, l) (g_mutex_unlock(((CamelNNTPNewsrc *)f)->l))
#else
#define NEWSRC_LOCK(f, l)
#define NEWSRC_UNLOCK(f, l)
#endif

typedef struct {
	guint low;
	guint high;
} ArticleRange;

typedef struct {
	char *name;
	GArray *ranges;
	gboolean subscribed;
} NewsrcGroup;

struct CamelNNTPNewsrc {
	gchar *filename;
	GHashTable *groups;
	gboolean dirty;
#ifdef ENABLE_THREADS
	GMutex *lock;
#endif
} ;


static NewsrcGroup *
camel_nntp_newsrc_group_add (CamelNNTPNewsrc *newsrc, const char *group_name, gboolean subscribed)
{
	NewsrcGroup *new_group = g_malloc(sizeof(NewsrcGroup));

	new_group->name = g_strdup(group_name);
	new_group->subscribed = subscribed;
	new_group->ranges = g_array_new (FALSE, FALSE, sizeof (ArticleRange));

	g_hash_table_insert (newsrc->groups, new_group->name, new_group);

	newsrc->dirty = TRUE;

	return new_group;
}

static int
camel_nntp_newsrc_group_get_highest_article_read(CamelNNTPNewsrc *newsrc, NewsrcGroup *group)
{
	if (!group || group->ranges->len == 0)
		return 0;

	return g_array_index(group->ranges, ArticleRange, group->ranges->len - 1).high;
}

static int
camel_nntp_newsrc_group_get_num_articles_read(CamelNNTPNewsrc *newsrc, NewsrcGroup *group)
{
	int i;
	int count = 0;

	if (group == NULL)
		return 0;

	for (i = 0; i < group->ranges->len; i ++)
		count += (g_array_index(group->ranges, ArticleRange, i).high -
			  g_array_index(group->ranges, ArticleRange, i).low) + 1;

	return count;
}


static void
camel_nntp_newsrc_group_mark_range_read(CamelNNTPNewsrc *newsrc, NewsrcGroup *group, long low, long high)
{
	int i;

	if (group->ranges->len == 1
	    && g_array_index (group->ranges, ArticleRange, 0).low == 0
	    && g_array_index (group->ranges, ArticleRange, 0).high == 0) {
		g_array_index (group->ranges, ArticleRange, 0).low = low;
		g_array_index (group->ranges, ArticleRange, 0).high = high;

		newsrc->dirty = TRUE;
	}
	else  {
		ArticleRange tmp_range;

		for (i = 0; i < group->ranges->len; i ++) {
			guint range_low = g_array_index (group->ranges, ArticleRange, i).low;
			guint range_high = g_array_index (group->ranges, ArticleRange, i).high;
			
			/* if it's already part of a range, return immediately. */
			if (low >= range_low &&
			    low <= range_high &&
			    high >= range_low &&
			    high <= range_high) {
				return;
			}
			/* if we have a new lower bound for this range, set it. */
			else if (low <= range_low
				 && high >= range_low
				 && high <= range_high) {
				g_array_index (group->ranges, ArticleRange, i).low = low;
				newsrc->dirty = TRUE;
				return;
			}
			/* if we have a new upper bound for this range, set it. */
			else if (high >= range_high
				 && low >= range_low
				 && low <= range_high) {
				g_array_index (group->ranges, ArticleRange, i).high = high;
				newsrc->dirty = TRUE;
				return;
			}
			/* if we would be inserting another range that
                           starts one index higher than an existing
                           one, make the upper value of the existing
                           range the upper value of the new one. */
			else if (low == range_high + 1) {
				g_array_index (group->ranges, ArticleRange, i).high = high;
				newsrc->dirty = TRUE;
				return;
			}
			/* if we would be inserting another range that
                           ends one index lower than an existing one,
                           group the existing range by setting its low
                           to the new low */
			else if (high == range_low - 1) {
				g_array_index (group->ranges, ArticleRange, i).low = low;
				newsrc->dirty = TRUE;
				return;
			}
			/* if the range lies entirely outside another
                           range, doesn't coincide with it's
                           endpoints, and has lower values, insert it
                           into the middle of the list. */
			else if (low < range_low
				 && high < range_low) {
				tmp_range.low = low;
				tmp_range.high = high;

				group->ranges = g_array_insert_val (group->ranges, i, tmp_range);
				newsrc->dirty = TRUE;

				return;
			}
		}

		/* if we made it here, the range needs to go at the end */
		tmp_range.low = low;
		tmp_range.high = high;
		group->ranges = g_array_append_val (group->ranges, tmp_range);
		newsrc->dirty = TRUE;
	} 
}

int
camel_nntp_newsrc_get_highest_article_read (CamelNNTPNewsrc *newsrc, const char *group_name)
{
	NewsrcGroup *group;
	int ret;

	NEWSRC_LOCK(newsrc, lock);

	group = g_hash_table_lookup (newsrc->groups, group_name);
	ret = camel_nntp_newsrc_group_get_highest_article_read (newsrc, group);

	NEWSRC_UNLOCK(newsrc, lock);

	return ret;
}

int
camel_nntp_newsrc_get_num_articles_read (CamelNNTPNewsrc *newsrc, const char *group_name)
{
	NewsrcGroup *group;
	int ret;

	NEWSRC_LOCK(newsrc, lock);

	group = g_hash_table_lookup (newsrc->groups, group_name);
	ret = camel_nntp_newsrc_group_get_num_articles_read (newsrc, group);

	NEWSRC_UNLOCK(newsrc, lock);

	return ret;
}

void
camel_nntp_newsrc_mark_article_read (CamelNNTPNewsrc *newsrc, const char *group_name, int num)
{
	camel_nntp_newsrc_mark_range_read (newsrc, group_name, num, num);
}

void
camel_nntp_newsrc_mark_range_read(CamelNNTPNewsrc *newsrc, const char *group_name, long low, long high)
{
	NewsrcGroup *group;

	/* swap them if they're in the wrong order. */
	if (low > high) {
		long tmp;

		tmp = high;
		high = low;
		low = tmp;
	}

	NEWSRC_LOCK(newsrc, lock);
	group = g_hash_table_lookup (newsrc->groups, group_name);

	camel_nntp_newsrc_group_mark_range_read (newsrc, group, low, high);
	NEWSRC_UNLOCK(newsrc, lock);
}

gboolean
camel_nntp_newsrc_article_is_read (CamelNNTPNewsrc *newsrc, const char *group_name, long num)
{
	int i;
	NewsrcGroup *group;
	int ret = FALSE;

	NEWSRC_LOCK(newsrc, lock);
	group = g_hash_table_lookup (newsrc->groups, group_name);
	
	for (i = 0; i < group->ranges->len; i++) {
		if (num >= g_array_index (group->ranges, ArticleRange, i).low && 
		    num <= g_array_index (group->ranges, ArticleRange, i).high) {
			ret = TRUE;
			break;
		}
	}

	NEWSRC_UNLOCK(newsrc, lock);

	return FALSE;
}

gboolean  
camel_nntp_newsrc_group_is_subscribed (CamelNNTPNewsrc *newsrc, const char *group_name)
{
	NewsrcGroup *group;
	int ret = FALSE;

	NEWSRC_LOCK(newsrc, lock);

	group = g_hash_table_lookup (newsrc->groups, group_name);

	if (group) {
		ret = group->subscribed;
	}

	NEWSRC_UNLOCK(newsrc, lock);

	return ret;
}

void
camel_nntp_newsrc_subscribe_group (CamelNNTPNewsrc *newsrc, const char *group_name)
{
	NewsrcGroup *group;

	NEWSRC_LOCK(newsrc, lock);

	group = g_hash_table_lookup (newsrc->groups, group_name);

	if (group) {
		if (!group->subscribed)
			newsrc->dirty = TRUE;
		group->subscribed = TRUE;
	}
	else {
		camel_nntp_newsrc_group_add (newsrc, group_name, TRUE);
	}

	NEWSRC_UNLOCK(newsrc, lock);
}

void
camel_nntp_newsrc_unsubscribe_group (CamelNNTPNewsrc *newsrc, const char *group_name)
{
	NewsrcGroup *group;

	NEWSRC_LOCK(newsrc, lock);

	group = g_hash_table_lookup (newsrc->groups, group_name);
	if (group) {
		if (group->subscribed)
			newsrc->dirty = TRUE;
		group->subscribed = FALSE;
	}
	else {
		camel_nntp_newsrc_group_add (newsrc, group_name, FALSE);
	}

	NEWSRC_UNLOCK(newsrc, lock);
}

struct newsrc_ptr_array {
	GPtrArray *ptr_array;
	gboolean subscribed_only;
};

/* this needs to strdup the grup_name, if the group array is likely to change */
static void
get_group_foreach (char *group_name, NewsrcGroup *group, struct newsrc_ptr_array *npa)
{
	if (group->subscribed || !npa->subscribed_only) {
		g_ptr_array_add (npa->ptr_array, group_name);
	}
}

GPtrArray *
camel_nntp_newsrc_get_subscribed_group_names (CamelNNTPNewsrc *newsrc)
{
	struct newsrc_ptr_array npa;

	g_return_val_if_fail (newsrc, NULL);

	NEWSRC_LOCK(newsrc, lock);

	npa.ptr_array = g_ptr_array_new();
	npa.subscribed_only = TRUE;

	g_hash_table_foreach (newsrc->groups,
			      (GHFunc)get_group_foreach, &npa);

	NEWSRC_UNLOCK(newsrc, lock);

	return npa.ptr_array;
}

GPtrArray *
camel_nntp_newsrc_get_all_group_names (CamelNNTPNewsrc *newsrc)
{
	struct newsrc_ptr_array npa;

	g_return_val_if_fail (newsrc, NULL);

	NEWSRC_LOCK(newsrc, lock);

	npa.ptr_array = g_ptr_array_new();
	npa.subscribed_only = FALSE;

	g_hash_table_foreach (newsrc->groups,
			      (GHFunc)get_group_foreach, &npa);

	NEWSRC_UNLOCK(newsrc, lock);

	return npa.ptr_array;
}

void
camel_nntp_newsrc_free_group_names (CamelNNTPNewsrc *newsrc, GPtrArray *group_names)
{
	g_ptr_array_free (group_names, TRUE);
}

struct newsrc_fp {
	CamelNNTPNewsrc *newsrc;
	FILE *fp;
};

static void
camel_nntp_newsrc_write_group_line(gpointer key, NewsrcGroup *group, struct newsrc_fp *newsrc_fp)
{
	CamelNNTPNewsrc *newsrc;
	FILE *fp;
	int i;

	fp = newsrc_fp->fp;
	newsrc = newsrc_fp->newsrc;

	fprintf (fp, "%s%c", group->name, group->subscribed ? ':' : '!');

	if (group->ranges->len == 1
	    && g_array_index (group->ranges, ArticleRange, 0).low == 0
	    && g_array_index (group->ranges, ArticleRange, 0).high == 0) {
		fprintf (fp, "\n");

		return; /* special case since our parsing code will insert this
			   bogus range if there were no read articles.  The code
			   to add a range is smart enough to remove this one if we
			   ever mark an article read, but we still need to deal with
			   it if that code doesn't get hit. */
	}

	fprintf (fp, " ");

	for (i = 0; i < group->ranges->len; i ++) {
		char range_buffer[100];
		guint low = g_array_index (group->ranges, ArticleRange, i).low;
		guint high = g_array_index (group->ranges, ArticleRange, i).high;

		if (low == high)
			sprintf(range_buffer, "%d", low);
		else if (low == high - 1)
			sprintf(range_buffer, "%d,%d", low, high);
		else
			sprintf(range_buffer, "%d-%d", low, high);

		if (i != group->ranges->len - 1)
			strcat(range_buffer, ",");

		fprintf (fp, range_buffer);
	}

	fprintf (fp, "\n");
}

void 
camel_nntp_newsrc_write_to_file(CamelNNTPNewsrc *newsrc, FILE *fp)
{
	struct newsrc_fp newsrc_fp;

	g_return_if_fail (newsrc);

	newsrc_fp.newsrc = newsrc;
	newsrc_fp.fp = fp;

	NEWSRC_LOCK(newsrc, lock);

	g_hash_table_foreach (newsrc->groups,
			      (GHFunc)camel_nntp_newsrc_write_group_line,
			      &newsrc_fp);

	NEWSRC_UNLOCK(newsrc, lock);
}

void
camel_nntp_newsrc_write(CamelNNTPNewsrc *newsrc)
{
	FILE *fp;

	g_return_if_fail (newsrc);

	NEWSRC_LOCK(newsrc, lock);

	if (!newsrc->dirty) {
		NEWSRC_UNLOCK(newsrc, lock);
		return;
	}

	if ((fp = fopen(newsrc->filename, "w")) == NULL) {
		g_warning ("Couldn't open newsrc file '%s'.\n", newsrc->filename);
		NEWSRC_UNLOCK(newsrc, lock);
		return;
	}

	newsrc->dirty = FALSE;
	NEWSRC_UNLOCK(newsrc, lock);

	camel_nntp_newsrc_write_to_file(newsrc, fp);

	fclose(fp);
}

static void
camel_nntp_newsrc_parse_line(CamelNNTPNewsrc *newsrc, char *line)
{
	char *p, *comma, *dash;
	gboolean is_subscribed;
	NewsrcGroup *group;

	p = strchr(line, ':');

	if (p) {
		is_subscribed = TRUE;
	}
	else {
		p = strchr(line, '!');
		if (p)
			is_subscribed = FALSE;
		else
			return; /* bogus line. */
	}

	*p++ = '\0';

	group = camel_nntp_newsrc_group_add (newsrc, line, is_subscribed);

	do {
		guint high, low;

		comma = strchr(p, ',');

		if (comma)
			*comma = '\0';

		dash = strchr(p, '-');

		if (!dash) { /* there wasn't a dash.  must be just one number */
			high = low = atol(p);
		}
		else { /* there was a dash. */
			*dash = '\0';
			low = atol(p);
			*dash = '-';
			p = dash + 1;
			high = atol(p);
		}

		camel_nntp_newsrc_group_mark_range_read (newsrc, group, low, high);

		if (comma) {
			*comma = ',';
			p = comma + 1;
		}

	} while(comma);
}

static char*
get_line (char *buf, char **p)
{
	char *l;
	char *line;

	g_assert (*p == NULL || **p == '\n' || **p == '\0');

	if (*p == NULL) {
		*p = buf;

		if (**p == '\0')
			return NULL;
	}
	else {
		if (**p == '\0')
			return NULL;

		(*p) ++;
	
		/* if we just incremented to the end of the buffer, return NULL */
		if (**p == '\0')
			return NULL;
	}

	l = strchr (*p, '\n');
	if (l) {
		*l = '\0';
		line = g_strdup (*p);
		*l = '\n';
		*p = l;
	}
	else {
		/* we're at the last line (which isn't terminated by a \n, btw) */
		line = g_strdup (*p);
		(*p) += strlen (*p);
	}

	return line;
}

CamelNNTPNewsrc *
camel_nntp_newsrc_read_for_server (const char *server)
{
	int fd;
	char buf[1024];
	char *file_contents, *line, *p;
	char *filename;
	CamelNNTPNewsrc *newsrc;
	int newsrc_len;
	int len_read = 0;
	struct stat sb;

	filename = g_strdup_printf ("%s/.newsrc-%s", g_get_home_dir(), server);

	newsrc = g_new0(CamelNNTPNewsrc, 1);
	newsrc->filename = filename;
	newsrc->groups = g_hash_table_new (g_str_hash, g_str_equal);
#ifdef ENABLE_THREADS
	newsrc->lock = g_mutex_new();
#endif

	if ((fd = open(filename, O_RDONLY)) == -1) {
		g_warning ("~/.newsrc-%s not present.\n", server);
		return newsrc;
	}

	if (fstat (fd, &sb) == -1) {
		g_warning ("failed fstat on ~/.newsrc-%s: %s\n", server, strerror(errno));
		return newsrc;
	}
	newsrc_len = sb.st_size;

	file_contents = g_malloc (newsrc_len + 1);

	while (len_read < newsrc_len) {
		int c = read (fd, buf, sizeof (buf));

		if (c == -1)
			break;

		memcpy (&file_contents[len_read], buf, c);
		len_read += c;
	}
	file_contents [len_read] = 0;

	p = NULL;
	while ((line = get_line (file_contents, &p))) {
		camel_nntp_newsrc_parse_line(newsrc, line);
		g_free (line);
	}

	close (fd);
	g_free (file_contents);

	return newsrc;
}
