/* iconv implementation - see iconv.h for docs */

#ifdef DEBUG
int errno;
#define errno errno
#endif

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

#include <sys/errno.h>

#include "swis.h"

#include "unicode/charsets.h"
#include "unicode/encoding.h"

#include "errors.h"
#include "header.h"
#include "iconv.h"
#include "internal.h"

static struct encoding_context *context_list;

#ifndef DEBUG
static _kernel_oserror ErrorGeneric = { 0x0, "" };
#endif

static int character_callback(void *handle, UCS4 c);
static size_t iconv_convert(_kernel_swi_regs *r);

#ifndef DEBUG

/* Module initialisation */
_kernel_oserror *mod_init(const char *tail, int podule_base, void *pw)
{
	UNUSED(tail);
	UNUSED(podule_base);
	UNUSED(pw);

	/* ensure the !Unicode resource exists */
	if (!getenv("Unicode$Path")) {
		strcpy(ErrorGeneric.errmess, "Unicode resources not found.");
		return &ErrorGeneric;
	}

	/* initialise encoding library */
	encoding_initialise();

	return NULL;
}

/* Module finalisation */
_kernel_oserror *mod_fini(int fatal, int podule_base, void *pw)
{
	struct encoding_context *a, *b;

	UNUSED(fatal);
	UNUSED(podule_base);
	UNUSED(pw);

	/* clients may quit / die without cleaning up. */
	for (a = context_list; a; a = b) {
		b = a->next;
		if (a->in)
			encoding_delete(a->in);
		if (a->out)
			encoding_delete(a->out);
		iconv_eightbit_delete(a);
		free(a);
	}

	/* finalise the library */
	encoding_tidyup();

	return NULL;
}

/* SWI handler */
_kernel_oserror *swi_handler(int swi_off, _kernel_swi_regs *regs, void *pw)
{
	unsigned int ret;

	UNUSED(pw);

	if (swi_off > 5)
		return error_BAD_SWI;

	switch (swi_off) {
		case 0: /* Iconv_Open */
			if ((ret = (unsigned int)
				iconv_open((const char*)regs->r[0],
					(const char*)regs->r[1])) == -1) {
				ErrorGeneric.errnum = errno;
				return &ErrorGeneric;
			}
			regs->r[0] = ret;
			break;
		case 1: /* Iconv_Iconv */
			if ((ret = (unsigned int)
				iconv((iconv_t)regs->r[0],
					(char**)regs->r[1],
					(size_t*)regs->r[2],
					(char**)regs->r[3],
					(size_t*)regs->r[4])) == -1) {
				ErrorGeneric.errnum = errno;
				return &ErrorGeneric;
			}
			regs->r[0] = ret;
			break;
		case 2: /* Iconv_Close */
			if ((ret = (unsigned int)
				iconv_close((iconv_t)regs->r[0])) == -1) {
				ErrorGeneric.errnum = errno;
				return &ErrorGeneric;
			}
			regs->r[0] = ret;
			break;
		case 3: /* Iconv_Convert */
			if ((ret = (unsigned int)
				iconv_convert(regs)) == -1) {
				ErrorGeneric.errnum = errno;
				return &ErrorGeneric;
			}
			regs->r[0] = ret;
			break;
		case 4: /* Iconv_CreateMenu */
			regs->r[2] = iconv_createmenu(regs->r[0],
					(char *)regs->r[1],
					regs->r[2],
					(const char *)regs->r[3]);
			break;
		case 5: /* Iconv_DecodeMenu */
			regs->r[4] = iconv_decodemenu(regs->r[0],
					(void *)regs->r[1],
					(int *)regs->r[2],
					(char *)regs->r[3], regs->r[4]);
			break;
	}

	return NULL;
}
#endif

iconv_t iconv_open(const char *tocode, const char *fromcode)
{
	int to = 0, from = 0;
	struct encoding_context *e;

	/* can't do anything without these */
	if (!tocode || !fromcode) {
		errno = ICONV_INVAL;
		return (iconv_t)(-1);
	}

	e = calloc(1, sizeof(*e));
	if (!e) {
		LOG(("malloc failed"));
		errno = ICONV_NOMEM;
		return (iconv_t)(-1);
	}

	/* try our own 8bit charset code first */
	to = iconv_eightbit_number_from_name(tocode);
	from = iconv_eightbit_number_from_name(fromcode);

	/* if that failed, try the UnicodeLib functionality */
	if (!to)
		to = iconv_encoding_number_from_name(tocode);

	if (!from)
		from = iconv_encoding_number_from_name(fromcode);

	LOG(("to: %d(%s) from: %d(%s)", to, tocode, from, fromcode));

	/* ensure both encodings are recognised */
	if (to == 0 || from == 0) {
		free(e);
		errno = ICONV_INVAL;
		return (iconv_t)(-1);
	}

	/* bit 30 set indicates that this is an 8bit encoding */
	if (from & (1<<30))
		e->intab = iconv_eightbit_new(from & ~(1<<30));
	else
		e->in = encoding_new(from, encoding_READ);

	/* neither created => memory error or somesuch. assume ENOMEM */
	/* no table is ever generated for ASCII */
	if (!e->in && !e->intab && (from & ~(1<<30)) != csASCII) {
		free(e);
		errno = ICONV_NOMEM;
		return (iconv_t)(-1);
	}

	if (to & (1<<30))
		e->outtab = iconv_eightbit_new(to & ~(1<<30));
	else
		e->out = encoding_new(to, encoding_WRITE_STRICT);

	/* neither created => ENOMEM */
	if (!e->out && !e->outtab && (to & ~(1<<30)) != csASCII) {
		if (e->in)
			encoding_delete(e->in);
		iconv_eightbit_delete(e);
		free(e);
		errno = ICONV_NOMEM;
		return (iconv_t)(-1);
	}

	/* add to list */
	e->prev = 0;
	e->next = context_list;
	if (context_list)
		context_list->prev = e;
	context_list = e;

	return (iconv_t)e;
}

size_t iconv(iconv_t cd, char **inbuf, size_t *inbytesleft, char **outbuf,
		size_t *outbytesleft)
{
	struct encoding_context *e;
	unsigned read;

	/* search for cd in list */
	for (e = context_list; e; e = e->next)
		if (e == (struct encoding_context *)cd)
			break;

	/* not found => invalid */
	if (!e) {
		errno = ICONV_INVAL;
		return (size_t)(-1);
	}

	if (inbuf == NULL || *inbuf == NULL) {
		if (e->in)
			encoding_reset(e->in);
		return 0;
	}

	/* Is there any point doing anything? */
	if (!outbuf || !(*outbuf) || !outbytesleft) {
		errno = ICONV_INVAL;
		return (size_t)(-1);
	}

	e->outbuf = outbuf;
	e->outbytesleft = outbytesleft;

	LOG(("reading"));

	if (e->in)
		read = encoding_read(e->in, character_callback, *inbuf,
				*inbytesleft, e);
	else
		read = iconv_eightbit_read(e, character_callback, *inbuf,
				*inbytesleft, e);

	LOG(("done"));

	LOG(("read: %d, ibl: %d, obl: %d", read, *inbytesleft, *outbytesleft));

	/* 2 */
	if (read == *inbytesleft) {
		*inbuf += (int)inbytesleft;
		*inbytesleft = 0;
		return 0;
	}
	/* 4 */
	else if ((int)*outbytesleft < 0) {
		LOG(("e2big"));
		errno = ICONV_2BIG;
	}
	/** \todo find a mechanism for distinguishing between 1 & 3 */
	/* 1 */
	else if (read != *inbytesleft) {
		*inbuf += read;
		*inbytesleft -= read;
		LOG(("eilseq"));
		errno = ICONV_ILSEQ;
	}
	/* 3 */
	else if ((int)*outbytesleft >= 0) {
		*inbuf += read;
		*inbytesleft -= read;
		LOG(("einval"));
		errno = ICONV_INVAL;
	}

	LOG(("errno: %d", errno));

	return (size_t)(-1);
}

int iconv_close(iconv_t cd)
{
	struct encoding_context *e;

	/* search for cd in list */
	for (e = context_list; e; e = e->next)
		if (e == (struct encoding_context *)cd)
			break;

	/* not found => invalid */
	if (!e)
		return 0;

	if (e->in)
		encoding_delete(e->in);
	if (e->out)
		encoding_delete(e->out);
	iconv_eightbit_delete(e);

	/* remove from list */
	if (e->next)
		e->next->prev = e->prev;
	if (e->prev)
		e->prev->next = e->next;
	else
		context_list = e->next;

	free(e);

	/* reduce our memory usage somewhat */
	encoding_table_remove_unused(8 /* recommended value */);

	return 0;
}

/* this is called for each converted character */
int character_callback(void *handle, UCS4 c)
{
	struct encoding_context *e;
	int ret;

	e = (struct encoding_context*)handle;

	LOG(("outbuf: %p, free: %d", e->outbuf, *e->outbytesleft));
	LOG(("writing: %d", c));

	if (e->out)
		ret = encoding_write(e->out, c, e->outbuf,
				(int*)e->outbytesleft);
	else
		ret = iconv_eightbit_write(e, c, e->outbuf,
				(int*)e->outbytesleft);

	return (!ret);
}

size_t iconv_convert(_kernel_swi_regs *regs)
{
	char *inbuf, *outbuf;
	size_t inbytesleft, outbytesleft;
	size_t ret;

	inbuf = (char *)regs->r[1];
	inbytesleft = (size_t)regs->r[2];
	outbuf = (char *)regs->r[3];
	outbytesleft = (size_t)regs->r[4];

	ret = iconv((iconv_t)regs->r[0], &inbuf, &inbytesleft,
			&outbuf, &outbytesleft);

	regs->r[1] = (int)inbuf;
	regs->r[2] = (int)inbytesleft;
	regs->r[3] = (int)outbuf;
	regs->r[4] = (int)outbytesleft;

	return ret;
}
