/*
 * Copyright (c) 1999 Sun Microsystems, Inc.
 * Copyright (c) 1999 Nihon Sun Microsystems K.K.
 * All rights reserved.
 */

/*
 * "$Id: iconv_relay.c,v 1.1.1.1 2000/10/29 16:44:24 himi Exp $"
 */

#pragma ident	"@(#)iconv_relay.c 1.9	99/05/20 SMI"


#include <stdlib.h>
#include <string.h>
#include <iconv.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/param.h>

#if defined(CSC_WC_MB) || defined(CSC_MB_WC)
#include <locale.h>
#endif /* CSC_WC_MB || CSC_MB_WC */

#include "csconv.h"


#if defined(CSC_MB_WC)
#  define iconv_relay_open	iconv_relay_mb_wc_open
#  define iconv_relay_close	iconv_relay_mb_wc_close
#  define iconv_relay_conv	iconv_relay_mb_wc_conv
#endif /* CSC_MB_WC */

#if defined(CSC_WC_MB)
#  define iconv_relay_open	iconv_relay_wc_mb_open
#  define iconv_relay_close	iconv_relay_wc_mb_close
#  define iconv_relay_conv	iconv_relay_wc_mb_conv
#endif /* CSC_WC_MB */

#if !defined(INTERIM_ENCODING)
#define INTERIM_ENCODING	"UTF-8"
#endif /* !INTERIM_ENCODING */
#if !defined(INTERIM_ENCODING_DELIM)
#define INTERIM_ENCODING_DELIM '%'
#endif /* !INTERIM_ENCODING_DELIM */
#if !defined(INTERIM_ENCODING_RENAME_DELIM)
#define INTERIM_ENCODING_RENAME_DELIM '|'
#endif /* !INTERIM_ENCODING_RENAME_DELIM */


typedef struct _csc_state {
	iconv_t		cd0;
	iconv_t		cd1;
#if defined(CSC_MB_WC) || defined(CSC_WC_MB)
	char *		locale;
#endif /* CSC_MB_WC || CSC_WC_MB */
} csc_state_t;


csc_state_t *
iconv_relay_open(
	const char *	locale,
	const char *	tocode,
	const char *	fromcode)
{
	csc_state_t *	csc_state;
	iconv_t		cd0;
	iconv_t		cd1;
#if defined(CSC_MB_WC) || defined(CSC_WC_MB)
	char *		locale_dup;
#endif /* CSC_MB_WC || CSC_WC_MB */
	char		interim_encoding_0_buf[MAXPATHLEN];
	char *		interim_encoding_0;
	char		interim_encoding_1_buf[MAXPATHLEN];
	char *		interim_encoding_1;
	char *		p0;
	char *		p1;
	int		n0;
	int		n1;
	int		len;

	csc_state = NULL;
#if defined(CSC_MB_WC) || defined(CSC_WC_MB)
	locale_dup = NULL;
#endif /* CSC_MB_WC || CSC_WC_MB */

	p1 = strchr(tocode, INTERIM_ENCODING_DELIM);
	if (NULL == p1) {
		interim_encoding_0 = INTERIM_ENCODING;
		interim_encoding_1 = interim_encoding_0;
	} else {
		len = strlen(tocode);
		p0 = strchr(tocode, INTERIM_ENCODING_RENAME_DELIM);
		if (NULL == p0) {
			n0 = 0;
			n1 = (p1 - tocode);

			if (((sizeof (interim_encoding_1_buf)) <= n1) ||
			    (n1 <= 0) ||
			    (strlen(tocode) <= (n1 + 1))) {
				errno = EINVAL;
				return NULL;
			}

			interim_encoding_0 = interim_encoding_1_buf;
			memcpy(interim_encoding_1_buf, tocode, n1);
			tocode = tocode + n1 + 1;
		} else {
			n0 = (p0 - tocode);
			n1 = ((p1 - tocode) - n0 - 1);

			if (((sizeof (interim_encoding_0_buf)) <= n0) ||
			    ((sizeof (interim_encoding_1_buf)) <= n1) ||
			    (n0 <= 0) ||
			    (n1 <= 0) ||
			    (strlen(tocode) <= (n0 + 1 + n1 + 1))) {
				errno = EINVAL;
				return NULL;
			}

			memcpy(interim_encoding_0_buf, tocode, n0);
			interim_encoding_0_buf[n0] = '\0';
			interim_encoding_0 = interim_encoding_0_buf;
			memcpy(interim_encoding_1_buf, tocode + n0 + 1, n1);
			tocode = tocode + n0 + 1 + n1 + 1;
		}

		interim_encoding_1_buf[n1] = '\0';
		interim_encoding_1 = interim_encoding_1_buf;
	}

	do {
		cd0 = iconv_open(tocode, fromcode);
		if ((iconv_t)(-1) == cd0) {
			cd0 = iconv_open(interim_encoding_0, fromcode);
			if ((iconv_t)(-1) == cd0) {
				continue;
			}

			cd1 = iconv_open(tocode, interim_encoding_1);
			if ((iconv_t)(-1) == cd1) {
				continue;
			}
		} else {
			cd1 = (iconv_t)(-1);
		}

#if defined(CSC_MB_WC) || defined(CSC_WC_MB)
		locale_dup = strdup(locale);
		if (NULL == locale_dup) {
			continue;
		}
#endif /* CSC_MB_WC || CSC_WC_MB */

		csc_state = malloc(sizeof (*csc_state));
		if (NULL == csc_state) {
			continue;
		}

		csc_state->cd0 = cd0;
		csc_state->cd1 = cd1;
#if defined(CSC_MB_WC) || defined(CSC_WC_MB)
		csc_state->locale = locale_dup;
#endif /* CSC_MB_WC || CSC_WC_MB */

		return csc_state;
	} while (0);

	if ((iconv_t)(-1) != cd0) {
		iconv_close(cd0);
		if ((iconv_t)(-1) != cd1) {
			iconv_close(cd1);
		}
	}

#if defined(CSC_MB_WC) || defined(CSC_WC_MB)
	free(locale_dup);
#endif /* CSC_MB_WC || CSC_WC_MB */
	free(csc_state);

	return NULL;
}


void
iconv_relay_close(csc_state_t * csc_state)
{
	if (NULL == csc_state) {
		return;
	}

	iconv_close(csc_state->cd0);
	if ((iconv_t)(-1) != csc_state->cd1) {
		iconv_close(csc_state->cd1);
	}

#if defined(CSC_MB_WC) || defined(CSC_WC_MB)
	free(csc_state->locale);
#endif /* CSC_MB_WC || CSC_WC_MB */

	free(csc_state);

	return;
}


size_t
iconv_relay_conv(
	csc_state_t *	csc_state,
	const char **	inbuf,
	size_t *	inbytesleft,
	char **		outbuf,
	size_t *	outbytesleft)
{
	size_t		ret_val;
	size_t		ret0;
	size_t		ret1;
	int		ret_val0;
	char		buf[1024];
	char *		p_interim;
	size_t		l_interim;
	char *		p_interim_save;
	size_t		l_interim_save;
	int		errno_save;

#if defined(CSC_WC_MB) || defined(CSC_MB_WC)
	char *		locale;
	const char *	ip;
	size_t		ileft;
	char *		op;
	size_t		oleft;
#endif /* CSC_WC_MB || CSC_MB_WC */
#if defined(CSC_WC_MB)
	char		interim_buf_in[1024];
	char *		interim_in;
	size_t		interim_in_len;
	wchar_t		wchar;
#endif /* CSC_WC_MB */
#if defined(CSC_MB_WC)
	char		interim_buf_out[1024];
	char *		interim_out;
	size_t		interim_out_len;
#endif /* CSC_MB_WC */

#if defined(CSC_WC_MB)
#  define inbuf0	&ip
#  define inbytesleft0	&ileft
#else /* !CSC_WC_MB */
#  define inbuf0	inbuf
#  define inbytesleft0	inbytesleft
#endif /* !CSC_WC_MB */
#if defined(CSC_MB_WC)
#  define outbuf0	&op
#  define outbytesleft0	&oleft
#else /* !CSC_MB_WC */
#  define outbuf0	outbuf
#  define outbytesleft0	outbytesleft
#endif /* !CSC_MB_WC */


	ret_val = 0;
	ret_val0 = 0;
	errno_save = 0;

	p_interim_save = NULL;

#if defined(CSC_WC_MB) || defined(CSC_MB_WC)
	locale = NULL;
#endif /* CSC_WC_MB || CSC_MB_WC */

#if defined(CSC_WC_MB)
	interim_in = NULL;
#endif /* CSC_WC_MB */

#if defined(CSC_MB_WC)
	interim_out = NULL;
#endif /* CSC_MB_WC */


	do {

#if defined(CSC_WC_MB) || defined(CSC_MB_WC)
		/*
		 * enter the locale
		 */
		if (NULL == (locale = setlocale(LC_CTYPE, NULL))) {
			errno_save = errno;
			ret_val = (size_t)(-1);
			break;
		}

		if (0 != strcmp(csc_state->locale, locale)) {
			if (NULL == (locale = strdup(locale))) {
				errno_save = errno;
				ret_val = (size_t)(-1);
				break;
			}

			if (NULL == setlocale(LC_CTYPE, csc_state->locale)) {
				errno_save = errno;
				ret_val = (size_t)(-1);
				break;
			}
		} else {
			locale = NULL;
		}
#endif /* CSC_WC_MB || CSC_MB_WC */


#if defined(CSC_MB_WC)
		/*
		 * allocate a buffer to hold MultiByte characters,
		 * which will be converted to WideChar characters.
		 *
		 * use (*inbytesleft) to adjust interim_out_len.
		 * do not use (*outbytesleft).  it might be huge.
		 */
		if ((NULL != inbuf) && (NULL != *inbuf)) {
			interim_out_len = ((*inbytesleft) * 4);
			if ((sizeof (interim_buf_out)) < interim_out_len) {
				interim_out = malloc(interim_out_len);
				if (NULL == interim_out) {
					errno_save = ENOMEM;
					ret_val = (size_t)(-1);
					break;
				}
			} else {
				interim_out = interim_buf_out;
				interim_out_len = (sizeof (interim_buf_out));
			}
		} else {
			interim_out = interim_buf_out;
			interim_out_len = (sizeof (interim_buf_out));
		}

		op = interim_out;
		oleft = interim_out_len;
#endif /* CSC_MB_WC */


#if defined(CSC_WC_MB)
		/*
		 * allocate a buffer to hold MultiByte characters,
		 * into which incoming WideChar characters are converted.
		 */
		if ((NULL != inbuf) && (NULL != *inbuf)) {
			interim_in_len = ((*inbytesleft) * 4);
			if ((sizeof (interim_buf_in)) < interim_in_len) {
				interim_in = malloc(interim_in_len);
				if (NULL == interim_in) {
					errno_save = ENOMEM;
					ret_val = (size_t)(-1);
					break;
				}
			} else {
				interim_in = interim_buf_in;
				interim_in_len = (sizeof (interim_buf_in));
			}

			ip = *inbuf;
			ileft = *inbytesleft;
			op = interim_in;
			oleft = interim_in_len;

			while ((0 < ileft) && (MB_CUR_MAX <= oleft)) {
				ret_val0 = wctomb(op, *((wchar_t *)ip));
				ip += (sizeof (wchar_t));
				ileft -= (sizeof (wchar_t));

				/* skip invalid data */
				if ((-1) != ret_val0) {
					op += ret_val0;
					oleft -= ret_val0;
				}
			}

			*inbuf = ip;
			*inbytesleft = ileft;

			ip = interim_in;
			ileft = (interim_in_len - oleft);

		} else {
			interim_in = NULL;
			interim_in_len = 0;
			ip = NULL;
			ileft = 0;
		}
#endif /* CSC_WC_MB */


		if ((iconv_t)(-1) == csc_state->cd1) {

			ret_val = iconv(csc_state->cd0,
					inbuf0, inbytesleft0,
					outbuf0, outbytesleft0);

			if ((size_t)(-1) == ret_val) {
				errno_save = errno;
			}

#if defined(CSC_MB_WC)
			if ((NULL != outbuf) && (NULL != *outbuf)) {
				ip = interim_out;
				ileft = interim_out_len - oleft;
				op = *outbuf;
				oleft = *outbytesleft;
				while ((0 < ileft) &&
				       ((sizeof (wchar_t)) < oleft)) {
					ret_val0 = mbtowc((wchar_t *)op,
							 ip, ileft);
					if (0 < ret_val0) {
						ip += ret_val0;
						ileft -= ret_val0;
						op += (sizeof (wchar_t));
						oleft -= (sizeof (wchar_t));
					} else {
						ip += 1;
						ileft -= 1;
					}
				}
				*outbuf = op;
				*outbytesleft = oleft;
			}
#endif /* CSC_MB_WC */

			break;
		}


		/* Initialize Only */
		if ((NULL == inbuf) || (NULL == *inbuf)) {
			char *	op;
			size_t	ol;
			size_t	il;
			op = NULL;
			ol = 0;
			il = 0;
			ret0 = iconv(csc_state->cd0, NULL, &il, &op, &ol);
			ret1 = iconv(csc_state->cd1,
				     inbuf0, inbytesleft0, outbuf0, outbytesleft0);
#if defined(CSC_MB_WC)
			if ((NULL != outbuf) && (NULL != *outbuf)) {
				ip = interim_out;
				ileft = interim_out_len - oleft;
				op = *outbuf;
				oleft = *outbytesleft;
				while ((0 < ileft) &&
				       ((sizeof (wchar_t)) < oleft)) {
					ret_val0 = mbtowc((wchar_t *)op,
							 ip, ileft);
					if (0 < ret_val0) {
						ip += ret_val0;
						ileft -= ret_val0;
						op += (sizeof (wchar_t));
						oleft -= (sizeof (wchar_t));
					} else {
						ip += 1;
						ileft -= 1;
					}
				}
				*outbuf = op;
				*outbytesleft = oleft;
			}
#endif /* CSC_MB_WC */
			ret_val = (((size_t)(-1) == ret0) ? ret0 : ret1);
			break;
		}

		p_interim = buf;
		l_interim = (sizeof (buf));
		p_interim_save = p_interim;
		l_interim_save = l_interim;

		ret0 = iconv(csc_state->cd0,
			     inbuf0, inbytesleft0, &p_interim, &l_interim);

		if ((size_t)(-1) == ret0) {
			errno_save = errno;
		}

		/*
		 * allocate interimediate encoding storage,
		 * and convert all input data into the storage
		 */
		if (((size_t)(-1) == ret0) && (E2BIG == errno)) {
			size_t	l;
			char *	p;

			l = (l_interim_save * 2);

			if (NULL == (p = malloc(l))) {
				errno_save = errno;
				ret_val = (size_t)(-1);
				break;
			}

			memcpy(p, p_interim_save, l_interim_save - l_interim);

			do {
				l_interim = (l - (l_interim_save - l_interim));
				l_interim_save = l;
				p_interim = (p + (l_interim_save - l_interim));
				p_interim_save = p;

				ret0 = iconv(csc_state->cd0,
					     inbuf0, inbytesleft0,
					     &p_interim, &l_interim);

				if ((size_t)(-1) != ret0) {
					break;
				} else if (E2BIG != errno) {
					errno_save = errno;
					break;
				}

				l = (l_interim_save * 2);

				if (NULL == (p = realloc(p_interim_save, l))) {
					errno_save = errno;
					ret0 = (size_t)(-1);
					break;
				}
			} while (1);

			if ((size_t)(-1) == ret0) {
				break;
			}
		}

		p_interim = p_interim_save;
		l_interim = (l_interim_save - l_interim);

		ret1 = iconv(csc_state->cd1,
			     (const char **)(&p_interim), &l_interim,
			     outbuf0, outbytesleft0);

		if (((size_t)(-1) != ret0) && ((size_t)(-1) == ret1)) {
			errno_save = errno;
		}

#if defined(CSC_MB_WC)
		if ((NULL != outbuf) && (NULL != *outbuf)) {
			ip = interim_out;
			ileft = interim_out_len - oleft;
			op = *outbuf;
			oleft = *outbytesleft;
			while ((0 < ileft) && ((sizeof (wchar_t)) < oleft)) {
				ret_val0 = mbtowc((wchar_t *)op, ip, ileft);
				if (0 < ret_val0) {
					ip += ret_val0;
					ileft -= ret_val0;
					op += (sizeof (wchar_t));
					oleft -= (sizeof (wchar_t));
				} else {
					ip += 1;
					ileft -= 1;
				}
			}
			*outbuf = op;
			*outbytesleft = oleft;
		}
#endif /* CSC_MB_WC */

	} while (0);

	if (buf != p_interim_save) {
		free(p_interim_save);
	}

#if defined(CSC_WC_MB) || defined(CSC_MB_WC)
	/*
	 * revert to previous locale
	 */
	if (NULL != locale) {
		if (NULL == setlocale(LC_CTYPE, locale)) {
			if ((size_t)(-1) != ret_val) {
				ret_val = (size_t)(-1);
				errno_save = errno;
			}
		}
		free(locale);
	}
#endif /* CSC_WC_MB || CSC_MB_WC */

#if defined(CSC_WC_MB)
	if (interim_in != interim_buf_in) {
		free(interim_in);
	}
#endif /* CSC_WC_MB */

#if defined(CSC_MB_WC)
	if (interim_out != interim_buf_out) {
		free(interim_out);
	}
#endif /* CSC_MB_WC */

	if (0 != errno_save) {
		errno = errno_save;
	}

	return ret_val;
}
