/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 *  Authors: Jeffrey Stedfast <fejj@ximian.com>
 *
 *  Copyright 2002 Ximian, Inc. (www.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 Street #330, Boston, MA 02111-1307, USA.
 *
 */


#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <glib.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>

#ifdef HAVE_ALLOCA_H
#include <alloca.h>
#endif

#include "gmime-charset.h"
#include "gmime-iconv.h"
#include "memchunk.h"


#define ICONV_CACHE_SIZE   (10)


struct _iconv_node {
	struct _iconv_node *next;
	struct _iconv_node *prev;
	
	struct _iconv_cache_bucket *bucket;
	
	gboolean used;
	iconv_t cd;
};

struct _iconv_cache_bucket {
	struct _iconv_cache_bucket *next;
	struct _iconv_cache_bucket *prev;
	
	struct _iconv_node *unused;
	struct _iconv_node *used;
	
	char *key;
};



static struct _iconv_cache_bucket *iconv_cache_buckets;
static struct _iconv_cache_bucket *iconv_cache_tail;
static unsigned int iconv_cache_size = 0;
static GHashTable *iconv_cache;
static GHashTable *iconv_open_hash;
static MemChunk *node_chunk;


static struct _iconv_node *
iconv_node_new (struct _iconv_cache_bucket *bucket)
{
	struct _iconv_node *node;
	
	node = memchunk_alloc (node_chunk);
	node->next = NULL;
	node->prev = NULL;
	node->bucket = bucket;
	node->used = FALSE;
	node->cd = (iconv_t) -1;
	
	return node;
}

static void
iconv_node_set_used (struct _iconv_node *node, gboolean used)
{
	if (node->used == used)
		return;
	
	node->used = used;
	
	if (used) {
		/* this should be a lone unused node, so prepend it to the used list */
		node->prev = NULL;
		node->next = node->bucket->used;
		if (node->bucket->used)
			node->bucket->used->prev = node;
		node->bucket->used = node;
		
		/* add to the open hash */
		g_hash_table_insert (iconv_open_hash, node->cd, node);
	} else {
		/* this could be anywhere in the used node list... */
		if (node->prev) {
			node->prev->next = node->next;
			if (node->next)
				node->next->prev = node->prev;
		} else {
			node->bucket->used = node->next;
			if (node->next)
				node->next->prev = NULL;
		}
		
		/* remove from the iconv open hash */
		g_hash_table_remove (iconv_open_hash, node->cd);
		
		/* prepend the node to the unused list */
		node->prev = NULL;
		node->next = node->bucket->unused;
		if (node->bucket->unused)
			node->bucket->unused->prev = node;
		node->bucket->unused = node;
	}
}

static void
iconv_node_destroy (struct _iconv_node *node)
{
	if (node) {
		if (node->cd != (iconv_t) -1)
			iconv_close (node->cd);
		
		memchunk_free (node_chunk, node);
	}
}



static struct _iconv_cache_bucket *
iconv_cache_bucket_new (const char *key)
{
	struct _iconv_cache_bucket *bucket;
	
	bucket = g_new (struct _iconv_cache_bucket, 1);
	bucket->next = NULL;
	bucket->prev = NULL;
	bucket->unused = NULL;
	bucket->used = NULL;
	bucket->key = g_strdup (key);
	
	return bucket;
}

static void
iconv_cache_bucket_add (struct _iconv_cache_bucket *bucket)
{
	if (iconv_cache_buckets)
		bucket->prev = iconv_cache_tail;
	
	iconv_cache_tail->next = bucket;
	iconv_cache_tail = bucket;
	
	g_hash_table_insert (iconv_cache, bucket->key, bucket);
}

static void
iconv_cache_bucket_add_node (struct _iconv_cache_bucket *bucket, struct _iconv_node *node)
{
	node->prev = NULL;
	node->next = bucket->unused;
	if (bucket->unused)
		bucket->unused->prev = node;
	bucket->unused = node;
}

static void
iconv_cache_bucket_remove (struct _iconv_cache_bucket *bucket)
{
	if (bucket->prev) {
		bucket->prev->next = bucket->next;
		if (bucket->next)
			bucket->next->prev = bucket->prev;
		else
			iconv_cache_tail = bucket->prev;
	} else {
		bucket->next->prev = NULL;
		iconv_cache_buckets = bucket->next;
		if (!iconv_cache_buckets)
			iconv_cache_tail = (struct _iconv_cache_bucket *) &iconv_cache_buckets;
	}
}

static struct _iconv_node *
iconv_cache_bucket_get_first_unused (struct _iconv_cache_bucket *bucket)
{
	struct _iconv_node *node = NULL;
	
	if (bucket->unused) {
		node = bucket->unused;
		bucket->unused = node->next;
		if (bucket->unused)
			bucket->unused->prev = NULL;
		node->next = NULL;
	}
	
	return node;
}

static void
iconv_cache_bucket_destroy (struct _iconv_cache_bucket *bucket)
{
	struct _iconv_node *node, *next;
	
	node = bucket->unused;
	while (node) {
		next = node->next;
		iconv_node_destroy (node);
		node = next;
	}
	
	node = bucket->used;
	while (node) {
		next = node->next;
		iconv_node_destroy (node);
		node = next;
	}
	
	g_free (bucket->key);
	g_free (bucket);
}

static void
iconv_cache_bucket_flush_unused (struct _iconv_cache_bucket *bucket)
{
	struct _iconv_node *node, *next;
	
	node = bucket->unused;
	while (node && iconv_cache_size >= ICONV_CACHE_SIZE) {
		next = node->next;
		iconv_node_destroy (node);
		iconv_cache_size--;
		node = next;
	}
	
	bucket->unused = node;
	
	if (!bucket->unused && !bucket->used) {
		/* expire this cache bucket... */
		iconv_cache_bucket_remove (bucket);
	}
}


static gboolean initialized = FALSE;


/**
 * g_mime_iconv_shutdown:
 *
 * Frees internal iconv caches created in #g_mime_iconv_init().
 **/
void
g_mime_iconv_shutdown (void)
{
	struct _iconv_cache_bucket *bucket, *next;
	
	if (!initialized)
		return;
	
	bucket = iconv_cache_buckets;
	while (bucket) {
		next = bucket->next;
		iconv_cache_bucket_destroy (bucket);
		bucket = next;
	}
	
	g_hash_table_destroy (iconv_cache);
	g_hash_table_destroy (iconv_open_hash);
	
	memchunk_destroy (node_chunk);
	
	initialized = FALSE;
}


/**
 * g_mime_iconv_init:
 *
 * Initialize GMime's iconv cache. This *MUST* be called before any
 * gmime-iconv interfaces will work correctly.
 **/
void
g_mime_iconv_init (void)
{
	if (initialized)
		return;
	
	g_mime_charset_init ();
	
	node_chunk = memchunk_new (sizeof (struct _iconv_node),
				   ICONV_CACHE_SIZE, FALSE);
	
	iconv_cache_buckets = NULL;
	iconv_cache_tail = (struct _iconv_cache_bucket *) &iconv_cache_buckets;
	iconv_cache = g_hash_table_new (g_str_hash, g_str_equal);
	iconv_open_hash = g_hash_table_new (g_direct_hash, g_direct_equal);
	
	initialized = TRUE;
}


/**
 * g_mime_iconv_open:
 * @to: charset to convert to
 * @from: charset to convert from
 *
 * Allocates a coversion descriptor suitable for converting byte
 * sequences from charset @from to charset @to. The resulting
 * descriptor can be used with iconv (or the g_mime_iconv wrapper) any
 * number of times until closed using g_mime_iconv_close.
 *
 * Returns a new conversion descriptor for use with iconv on success
 * or (iconv_t) -1 on fail as well as setting an appropriate errno
 * value.
 **/
iconv_t
g_mime_iconv_open (const char *to, const char *from)
{
	struct _iconv_cache_bucket *bucket, *prev;
	struct _iconv_node *node;
	iconv_t cd;
	char *key;
	
	if (from == NULL || to == NULL) {
		errno = EINVAL;
		return (iconv_t) -1;
	}
	
	from = g_mime_charset_name (from);
	to = g_mime_charset_name (to);
	key = alloca (strlen (from) + strlen (to) + 2);
	sprintf (key, "%s:%s", from, to);
	
	bucket = g_hash_table_lookup (iconv_cache, key);
	if (bucket) {
		node = iconv_cache_bucket_get_first_unused (bucket);
	} else {
		/* make room for another cache bucket */
		bucket = iconv_cache_tail;
		while (bucket && iconv_cache_size >= ICONV_CACHE_SIZE) {
			prev = bucket->prev;
			iconv_cache_bucket_flush_unused (bucket);
			bucket = prev;
		}
		
		bucket = iconv_cache_bucket_new (key);
		iconv_cache_bucket_add (bucket);
		
		node = NULL;
	}
	
	if (node == NULL) {
		node = iconv_node_new (bucket);
		
		/* make room for this node */
		bucket = iconv_cache_tail;
		while (bucket && iconv_cache_size >= ICONV_CACHE_SIZE) {
			prev = bucket->prev;
			iconv_cache_bucket_flush_unused (bucket);
			bucket = prev;
		}
		
		cd = iconv_open (to, from);
		if (cd == (iconv_t) -1) {
			iconv_node_destroy (node);
			return cd;
		}
		
		node->cd = cd;
		
		iconv_cache_bucket_add_node (node->bucket, node);
	} else {
		cd = node->cd;
		
		/* reset the iconv descriptor */
		iconv (cd, NULL, NULL, NULL, NULL);
	}
	
	iconv_node_set_used (node, TRUE);
	
	return cd;
}


/**
 * g_mime_iconv_close:
 * @cd: iconv conversion descriptor
 *
 * Closes the iconv descriptor @cd.
 *
 * Returns 0 on success or -1 on fail as well as setting an
 * appropriate errno value.
 **/
int
g_mime_iconv_close (iconv_t cd)
{
	struct _iconv_node *node;
	
	if (cd == (iconv_t) -1)
		return 0;
	
	node = g_hash_table_lookup (iconv_open_hash, cd);
	if (node) {
		iconv_node_set_used (node, FALSE);
	} else {
		g_warning ("This iconv context wasn't opened using g_mime_iconv_open()!");
		return iconv_close (cd);
	}
	
	return 0;
}


syntax highlighted by Code2HTML, v. 0.9.1