/*$
mlk in apdtool
Copyright (c) 2020 Azel

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
$*/

/*****************************************
 * ロケールの文字セット関連
 *****************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <locale.h>
#include <langinfo.h>
#include <iconv.h>
#include <errno.h>

#include "mlk.h"
#include "mlk_charset.h"


//----------------

typedef struct
{
	const char *name;
	mlkbool is_utf8;
}mCharset;

static mCharset g_charset = {0, 1};

//----------------


/**@ ロケールを初期化
 *
 * @d:setlocale(LC_ALL, "") を実行し、文字セット情報を取得する。\
 * ASCII 文字以外の文字列を扱う場合、アプリ開始時に実行すること。\
 * \
 * 文字セット情報は静的変数に保存される。\
 * 一度設定したら変更することはほぼないと思われるので、
 * 読み込み専用データとして扱うため、スレッドの対策はしていない。 */

void mInitLocale(void)
{
	char *m;

	setlocale(LC_ALL, "");

	//文字セット

	m = nl_langinfo(CODESET);

	g_charset.is_utf8 = (!m || m[0] == 0 || strcmp(m, "UTF-8") == 0);

	if(g_charset.is_utf8)
		g_charset.name = "UTF-8";
	else
		g_charset.name = m;
}

/**@ ロケールの文字セット名を取得
 *
 * @d:デフォルトで "UTF-8" となる */

const char *mGetLocaleCharset(void)
{
	return g_charset.name;
}

/**@ ロケールの文字セットが UTF-8 かどうか
 *
 * @d:mInitLocale() が実行されていない場合、UTF-8 とみなす。 */

mlkbool mLocaleCharsetIsUTF8(void)
{
	return g_charset.is_utf8;
}


//==============================
// iconv
//==============================


/**@ iconv 開く
 *
 * @g:iconv
 *
 * @p:p 値が格納される
 * @p:from 変換元の文字コード
 * @p:to 変換先の文字コード */

mlkbool mIconvOpen(mIconv *p,const char *from,const char *to)
{
	*p = iconv_open(to, from);

	return (*p != (iconv_t)-1);
}

/**@ iconv 閉じる */

void mIconvClose(mIconv p)
{
	if(p != (iconv_t)-1)
		iconv_close((iconv_t)p);
}

/**@ 文字コード変換
 *
 * @p:str 変換元文字列
 * @p:len 変換元文字列の長さ (負の値でヌル文字まで)
 * @p:dstlen NULL 以外で、変換後の長さが格納される (ヌル文字を含まない)
 * @r:変換後の、確保された文字列。\
 * 終端にはヌル文字がセットされる。\
 * NULL で失敗。 */

char *mIconvConvert(mIconv p,const char *str,int len,int *dstlen)
{
	const char *ps;
	char *dstbuf,*pd;
	size_t ssize,dsize,ret,bufsize,pos;
	int freset = FALSE,ferr = FALSE;

	if(p == (mIconv)-1) return NULL;

	if(len < 0)
		len = strlen(str);

	bufsize = len + 1;
	if(bufsize < 256) bufsize = 256;

	dstbuf = (char *)mMalloc(bufsize);
	if(!dstbuf) return NULL;

	ps = str;
	pd = dstbuf;
	ssize = len;
	dsize = bufsize - 1;

	//

	while(1)
	{
		ret = iconv((iconv_t)p, (freset)? NULL: (char **)&ps, &ssize, &pd, &dsize);

		if(ret == (size_t)-1)
		{
			//エラー
			
			if(errno == E2BIG)
			{
				//出力バッファが足りない

				pos = pd - dstbuf;
				bufsize *= 2;

				pd = (char *)mRealloc(dstbuf, bufsize);
				if(!pd)
				{
					ferr = TRUE;
					break;
				}

				dstbuf = pd;
				pd += pos;
				dsize = bufsize - 1 - pos;
			}
			else
			{
				//入力に不完全なもの・無効な文字があった場合など
				ferr = TRUE;
				break;
			}
		}
		else
		{
			//完了した
			
			if(freset)
				break;
			else
			{
				//最後に状態をリセット
				freset = TRUE;
				ssize = 0;
			}
		}
	}

	//エラー時

	if(ferr)
	{
		mFree(dstbuf);
		return NULL;
	}

	//成功

	pos = pd - dstbuf;

	dstbuf[pos] = 0;

	if(dstlen) *dstlen = pos;

	return mRealloc(dstbuf, pos + 1);
}

/**@ コールバック関数を使って文字コード変換
 *
 * @d:各バッファを確保し、in 関数で入力文字列をセット、
 * out 関数で出力を処理する。\
 * \
 * コールバック関数は、負の値を返すとエラー終了する。\
 * in 関数では、セットした文字列の長さを返す。0 でデータの終了となる。\
 * out 関数では、すべてのデータを一度で処理すること。0 以上の値を返すと成功とする。\
 *
 * @p:inbufsize 入力バッファサイズ
 * @p:outbufsize 出力バッファサイズ
 * @p:param コールバック関数に渡されるユーザー定義値
 * @r:エラーがあった場合、FALSE */

mlkbool mIconvConvert_callback(mIconv p,int inbufsize,int outbufsize,mIconvCallback *cb,void *param)
{
	char *inbuf,*outbuf,*pin,*pout;
	size_t insize,outsize,reterr;
	int ret, flags = 0;
	mlkbool retval = FALSE;

	if(p == (mIconv)-1) return FALSE;

	//バッファ確保

	inbuf = (char *)mMalloc(inbufsize);
	outbuf = (char *)mMalloc(outbufsize);

	if(!inbuf || !outbuf) goto END;

	//

	pout = outbuf;
	insize = 0;
	outsize = outbufsize;

	/* flags: 1=リセット, 2=入力に不完全な文字列があった */

	while(1)
	{
		//入力文字列取得
		
		if(insize == 0 || (flags & 2))
		{
			ret = (cb->in)(inbuf + insize, inbufsize - insize, param);
			if(ret < 0) break;

			if(ret == 0)
				flags |= 1;

			insize += ret;
			pin = inbuf;
		}

		//変換

		reterr = iconv((iconv_t)p, (flags & 1)? NULL: &pin, &insize, &pout, &outsize);

		//前回 EINVAL で、再変換しても EINVAL の場合は、エラーとする

		if(flags & 2)
		{
			if(reterr == (size_t)-1 && errno == EINVAL)
				break;

			flags &= ~2;
		}

		//

		if(reterr == (size_t)-1)
		{
			//エラー

			if(errno == EINVAL)
			{
				//入力に不完全な文字列があった
				//(入力データが足りていない場合があるため、追加取得する)

				flags |= 2;

				memmove(inbuf, pin, insize);
			}
			else if(errno == E2BIG)
			{
				//出力バッファが足りない

				ret = (cb->out)(outbuf, pout - outbuf, param);
				if(ret < 0) break;

				pout = outbuf;
				outsize = outbufsize;
			}
			else
				break;
		}
		else
		{
			//入力をすべて処理した

			if(flags & 1)
			{
				retval = TRUE;
				break;
			}

			insize = 0;
		}
	}

	//残った出力を処理

	if(retval && pout != outbuf)
	{
		ret = (cb->out)(outbuf, pout - outbuf, param);
		if(ret < 0) retval = FALSE;
	}

	//
	
END:
	mFree(inbuf);
	mFree(outbuf);

	return retval;
}


//==============================
// 変換
//==============================


/**@ 文字列の文字コードを変換
 *
 * @g:変換
 * 
 * @p:str 変換元文字列
 * @p:len 変換元文字列の長さ (負の値でヌル文字まで)
 * @p:from 変換元の文字コード
 * @p:to 変換先の文字コード
 * @p:dstlen NULL 以外で、変換後の長さ
 * @r:変換後の、確保された文字列。\
 * NULL で失敗。 */

char *mConvertCharset(const char *str,int len,const char *from,const char *to,int *dstlen)
{
	mIconv ic;
	char *dst;

	if(!mIconvOpen(&ic, from, to))
		return NULL;
	else
	{
		dst = mIconvConvert(ic, str, len, dstlen);

		mIconvClose(ic);

		return dst;
	}
}

/**@ UTF-8 文字列をロケール文字列に変換
 *
 * @d:ロケールが UTF-8 の場合、そのままコピーされる。
 * @p:len 文字列の長さ。負の値でヌル文字まで。
 * @p:dstlen NULL 以外で、変換された文字列の長さが格納される
 * @r:確保された文字列。NULL でエラー。 */

char *mUTF8toLocale(const char *str,int len,int *dstlen)
{
	if(g_charset.is_utf8)
	{
		if(len < 0) len = strlen(str);

		if(dstlen) *dstlen = len;
		
		return mStrndup(str, len);
	}
	else
		return mConvertCharset(str, len, "UTF-8", g_charset.name, dstlen);
}

/**@ ロケール文字列を UTF-8 文字列に変換
 *
 * @d:ロケールが UTF-8 の場合、そのままコピーされる。
 * @p:len 文字列の長さ。負の値でヌル文字まで。
 * @p:dstlen NULL 以外で、変換された文字列の長さが格納される
 * @r:確保された文字列。NULL でエラー。 */

char *mLocaletoUTF8(const char *str,int len,int *dstlen)
{
	if(g_charset.is_utf8)
	{
		if(len < 0) len = strlen(str);

		if(dstlen) *dstlen = len;

		return mStrndup(str, len);
	}
	else
		return mConvertCharset(str, len, g_charset.name, "UTF-8", dstlen);
}


//==============================
// 出力
//==============================


/**@ UTF-8 文字列を標準出力に出力
 *
 * @d:ロケールの文字セットが UTF-8 以外の場合は、
 * ロケール文字列に変換して出力される。
 *
 * @p:str NULL で何もしない */

void mPutUTF8_stdout(const char *str)
{
	if(!str) return;

	if(g_charset.is_utf8)
		fputs(str, stdout);
	else
	{
		char *buf;

		buf = mUTF8toLocale(str, -1, NULL);

		if(buf)
		{
			fputs(buf, stdout);
			mFree(buf);
		}
	}
}

/**@ UTF-8 文字列を標準エラー出力に出力 */

void mPutUTF8_stderr(const char *str)
{
	if(!str) return;

	if(g_charset.is_utf8)
		fputs(str, stderr);
	else
	{
		char *buf;

		buf = mUTF8toLocale(str, -1, NULL);

		if(buf)
		{
			fputs(buf, stderr);
			mFree(buf);
		}
	}
}

/**@ UTF-8 文字列を標準出力に出力
 *
 * @p:format '%s' の部分に str が挿入される */

void mPutUTF8_format_stdout(const char *format,const char *str)
{
	char *pc;

	pc = strstr(format, "%s");
	if(!pc)
		fputs(format, stdout);
	else
	{
		fwrite(format, 1, pc - format, stdout);
		mPutUTF8_stdout(str);
		fputs(pc + 2, stdout);
	}
}
