/***************************************************************************

  gbx_local.c

  (c) 2000-2017 Benoît Minisini <g4mba5@gmail.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, 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., 51 Franklin Street, Fifth Floor, Boston,
  MA 02110-1301, USA.

***************************************************************************/

#define __GBX_LOCAL_C

#include "gb_common.h"
#include "gb_common_buffer.h"
#include "gb_common_case.h"

#include "gb_error.h"
#include "gbx_value.h"
#include "gb_limit.h"

#include <locale.h>
#include <langinfo.h>

#include <time.h>
#include <ctype.h>
#include <float.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <libintl.h>

#include "gbx_math.h"
#include "gbx_date.h"
#include "gbx_string.h"
#include "gbx_c_string.h"
#include "gbx_api.h"
#include "gb_file.h"
#include "gbx_component.h"
#include "gbx_exec.h"
#include "gbx_archive.h"

#include "gbx_local.h"

//#define DEBUG_LANG

#define buffer_init COMMON_buffer_init
#define look_char COMMON_look_char
#define put_char COMMON_put_char
#define get_current COMMON_get_current
#define buffer_pos COMMON_pos
#define get_size_left COMMON_get_size_left

static void add_string(const char *src, int len, int *before);

/* System encoding*/
char *LOCAL_encoding = NULL;

/* If system encoding is UTF-8 */
bool LOCAL_is_UTF8;

/* Default 'C' localization */
LOCAL_INFO LOCAL_default = {
	'.', '.',
	NULL, 0, NULL, 0,
	3, 3,
	0,
	'/', ':',
	"",
	"",
	{ LO_MONTH, LO_DAY, LO_YEAR },
	{ LO_HOUR, LO_MINUTE, LO_SECOND },
	"dddd mmmm d yyyy",
	"mmm d yyyy",
	"mm/dd/yyyy",
	"hh:nn:ss",
	"hh:nn AM/PM",
	"hh:nn",
	"mm/dd/yyyy hh:nn:ss",
	"(#,##0.##)",
	"(#,##0.##)",
	"True", 4,
	"False", 5,
	0
	};

/* User language localization */
LOCAL_INFO LOCAL_local = { 0 };

// First day of weekday
char LOCAL_first_day_of_week = -1;

static char *_rtl_lang[] = { "ar", "fa", "he", NULL };

static bool _translation_loaded = FALSE;

static LOCAL_INFO *local_current;

static char env_LC_ALL[MAX_LANG + sizeof "LC_ALL" + 1];
static char env_LANGUAGE[MAX_LANG + sizeof "LANGUAGE" + 1];
static char env_LANG[MAX_LANG + sizeof "LANG" + 1];

static bool _currency;

static char *_lang = NULL;

extern char **environ;
static char **_environ;

#define add_currency_flag(_flag) (LOCAL_local.currency_flag <<= 1, LOCAL_local.currency_flag |= (!!(_flag)))
#define test_currency_flag(_negative, _space, _before, _intl) (!!(LOCAL_local.currency_flag & (1 << ((!!_negative) + ((!!_before) << 1) + ((!!_intl) << 2)))))

static void init_currency_flag(struct lconv *info)
{
#ifndef OS_BSD
	add_currency_flag(info->int_n_cs_precedes); // 7
	add_currency_flag(info->int_p_cs_precedes); // 6
#else
	add_currency_flag(info->n_cs_precedes); // 7
	add_currency_flag(info->p_cs_precedes); // 6
#endif
	add_currency_flag(1); // 5
	add_currency_flag(1); // 4
	add_currency_flag(info->n_cs_precedes); // 3
	add_currency_flag(info->p_cs_precedes); // 2
	add_currency_flag(info->n_sep_by_space); // 1
	add_currency_flag(info->p_sep_by_space); // 0
}

static bool is_currency_before(bool negative, bool intl)
{
	return test_currency_flag(negative, FALSE, TRUE, intl);
}

static bool is_currency_space(bool negative, bool intl)
{
	//fprintf(stderr, "%02X & %02X\n", LOCAL_local.currency_flag, (1 << ((!!negative) + ((!!0) << 1) + ((!!intl) << 2))));
	return test_currency_flag(negative, TRUE, FALSE, intl);
}

static void my_setenv(const char *name, const char *value, char *ptr)
{
	snprintf(ptr, MAX_LANG + strlen(name) + 1, "%s=%s", name, value);
	putenv(ptr);
}

static void begin(void)
{
	buffer_init(COMMON_buffer, COMMON_BUF_MAX - 4);
}

static void end(char **str, int *len)
{
	*(get_current()) = 0;
	*str = COMMON_buffer;
	*len = buffer_pos;
}


static void stradd_sep(char *dst, const char *src, const char *sep)
{
	if (*dst)
		strcat(dst, sep);

	strcat(dst, src);
}


static void add_thousand_sep(int *before)
{
	int group;
	const char *thsep;
	int lthsep;

	if (before == NULL)
		return;

	thsep = _currency ? local_current->thousand_sep : local_current->currency_thousand_sep;
	lthsep = _currency ? local_current->len_thousand_sep : local_current->len_currency_thousand_sep;
	
	if (thsep && thsep)
	{
		group = _currency ? local_current->currency_group_size : local_current->group_size;

		if (group > 0 && (*before > 1) && ((*before - 1) == (((*before - 1) / group) * group)))
		{
			if (buffer_pos > 0 && (get_current()[-1] == ' '))
				put_char(' ');
			else
				add_string(thsep, lthsep, NULL);
		}
	}

	(*before)--;
}


static void add_string(const char *src, int len, int *before)
{
	if (len <= 0)
		len = strlen(src);

	while (len > 0)
	{
		put_char(*src++);
		len--;

		if (before)
			add_thousand_sep(before);
	}
}

static void add_unicode(uint unicode)
{
	char str[8];
	STRING_utf8_from_unicode(unicode, str);
	add_string(str, STRING_utf8_get_char_length(*str), NULL);
}

static void add_currency(const char *sym)
{
	const char *p = sym;
	char c;

	for(;;)
	{
		c = *p++;
		if (c == 0)
			return;
		if (c != ' ')
			put_char(c);
	}
}


static void add_char(char c, int count, int *before)
{
	while (count > 0)
	{
		put_char(c);
		count--;

		add_thousand_sep(before);
	}
}

static void add_zero(int count, int *before)
{
	add_char('0', count, before);
}


static void add_sign(char mode, int sign, bool after)
{
	if (after && mode != '(')
		return;

	if (sign < 0)
	{
		if (mode == '(')
			put_char(after ? ')' : '(');
		else
			put_char('-');
	}
	else if (mode != 0 && mode != '(')
	{
		if (sign > 0)
			put_char(mode);
		else
			put_char(' ');
	}
}


static char *get_languages(void)
{
	char *lang;
	char *lang_list = NULL;
	char *src;
	char *p;

	lang = STRING_new_temp_zero(LOCAL_get_lang());
	p = index(lang, '.');
	if (p) *p = 0;

	lang_list = STRING_add(lang_list, lang, 0);
	lang_list = STRING_add_char(lang_list, ':');

	src = index(lang, '_');
	if (src)
	{
		*src = 0;
		lang_list = STRING_add(lang_list, lang, 0);
		lang_list = STRING_add_char(lang_list, ':');
	}

	lang_list = STRING_add(lang_list, lang, 0);
	lang_list = STRING_add(lang_list, "_*", 2);

	#ifdef DEBUG_LANG
	fprintf(stderr, "Languages = %s\n", lang_list);
	#endif

	return lang_list;
}


static void free_local_info(void)
{
	STRING_free(&LOCAL_local.true_str);
	STRING_free(&LOCAL_local.false_str);
	CLEAR(&LOCAL_local);
}

static const char *fix_separator(const char *str)
{
	if (!str || !*str)
		return " ";
	
	if ((uchar)str[0] == 0xC2 && (uchar)str[1] == 0xA0 && str[2] == 0)
		return " ";

	if ((uchar)str[0] == 0xE2 && (uchar)str[1] == 0x80 && (uchar)str[2] == 0xAF && str[3] == 0)
		return " ";

	return str[1] ? "_" : str;
}

static void fill_local_info(void)
{
	struct lconv *info;
	char *p;
	char c;
	char *dp;
	char *tp;
	char *codeset;
	const char *lang;
	char *am_pm;
	bool got_second;

	free_local_info();

	/* local encoding */

	if (!LOCAL_is_UTF8)
		STRING_free(&LOCAL_encoding);

	codeset = nl_langinfo(CODESET);
	if (!codeset || !*codeset)
		codeset = "US-ASCII";

	LOCAL_is_UTF8 = (strcasecmp(codeset, "UTF-8") == 0);
	if (LOCAL_is_UTF8)
		LOCAL_encoding = SC_UTF8;
	else
		LOCAL_encoding = STRING_new_zero(codeset);

	#ifdef DEBUG_LANG
	fprintf(stderr, "LOCAL_encoding = %s\n", LOCAL_encoding == SC_UTF8 ? "UTF-8" : LOCAL_encoding);
	#endif

	/* Numeric information */

	info = localeconv();

	//fprintf(stderr, "'%s' '%s' %d %d\n", info->thousands_sep, info->mon_thousands_sep, *info->thousands_sep, *info->grouping);
	//fprintf(stderr, "'%s' '%s'\n", nl_langinfo(THOUSANDS_SEP), nl_langinfo(MON_THOUSANDS_SEP));

	LOCAL_local.decimal_point = *(info->decimal_point);
	LOCAL_local.thousand_sep = fix_separator(info->thousands_sep);
	LOCAL_local.len_thousand_sep = strlen(LOCAL_local.thousand_sep);
	
	LOCAL_local.group_size = *(info->grouping);
	if (LOCAL_local.group_size == 0)
		LOCAL_local.group_size = 3;

	/*LOCAL_local.currency_symbol = STRING_conv_to_UTF8(info->currency_symbol, 0);
	STRING_ref(LOCAL_local.currency_symbol);
	LOCAL_local.intl_currency_symbol = STRING_conv_to_UTF8(info->int_curr_symbol, 0);
	STRING_ref(LOCAL_local.intl_currency_symbol);*/

	// Date format

	p = nl_langinfo(D_FMT);
	//fprintf(stderr, "date format: %s\n", p);
	dp = LOCAL_local.date_order;

	if (strcmp(p, "%D") == 0)
		p = "%m/%d/%y";
	else if (strcmp(p, "%F") == 0)
		p = "%Y-%m-%d";

	for (;;)
	{
		c = *p++;
		if (!c)
			break;

		if (c == '%')
		{
			c = *p++;
			if (c != '%')
			{
				if (c == 'E' || c == 'O')
					c = *p++;

				switch (c)
				{
					case 'y': case 'Y':
						*dp++ = LO_YEAR;
						stradd_sep(LOCAL_local.long_date, "yyyy", " ");
						stradd_sep(LOCAL_local.medium_date, "yyyy", " ");
						stradd_sep(LOCAL_local.short_date, "yyyy", "/");
						stradd_sep(LOCAL_local.general_date, "yyyy", "/");
						break;

					case 'b': case 'B': case 'h': case 'm':
						*dp++ = LO_MONTH;
						stradd_sep(LOCAL_local.long_date, "mmmm", " ");
						stradd_sep(LOCAL_local.medium_date, "mmm", " ");
						stradd_sep(LOCAL_local.short_date, "mm", "/");
						stradd_sep(LOCAL_local.general_date, "mm", "/");
						break;

					case 'd': case 'e':
						*dp++ = LO_DAY;
						stradd_sep(LOCAL_local.long_date, "dddd d", " ");
						stradd_sep(LOCAL_local.medium_date, "dd", " ");
						stradd_sep(LOCAL_local.short_date, "dd", "/");
						stradd_sep(LOCAL_local.general_date, "dd", "/");
						break;
				}
				continue;
			}
		}

		if (dp != LOCAL_local.date_order && LOCAL_local.date_sep == 0)
			LOCAL_local.date_sep = STRING_utf8_to_unicode(p - 1, STRING_utf8_get_char_length(c));
	}

	// Time format

	p = nl_langinfo(T_FMT);
	//fprintf(stderr, "time format: %s\n", p);
	tp = LOCAL_local.time_order;

	if (strcmp(p, "%T") == 0 || strcmp(p, "%R") == 0 || strcmp(p, "%r") == 0)
		p = "%H:%M:%S";

	got_second = FALSE;

	for (;;)
	{
		c = *p++;
		if (!c)
			break;

		if (c == '%')
		{
			c = *p++;
			if (c != '%')
			{
				if (c == 'E' || c == 'O')
					c = *p++;

				switch(c)
				{
					case 'H': case 'I': case 'k': case 'l':
						*tp++ = LO_HOUR;
						stradd_sep(LOCAL_local.long_time, "hh", ":");
						stradd_sep(LOCAL_local.medium_time, "hh", ":");
						stradd_sep(LOCAL_local.short_time, "hh", ":");
						break;

					case 'M':
						*tp++ = LO_MINUTE;
						stradd_sep(LOCAL_local.long_time, "nn", ":");
						stradd_sep(LOCAL_local.medium_time, "nn", ":");
						stradd_sep(LOCAL_local.short_time, "nn", ":");
						break;

					case 'S':
						*tp++ = LO_SECOND;
						stradd_sep(LOCAL_local.long_time, "ss", ":");
						got_second = TRUE;
						break;
				}
				continue;
			}
		}

		if (tp != LOCAL_local.time_order && LOCAL_local.time_sep == 0)
			LOCAL_local.time_sep = STRING_utf8_to_unicode(p - 1, STRING_utf8_get_char_length(c));
	}

	// Fix missing seconds

	if (!got_second)
	{
		*tp++ = LO_SECOND;
		stradd_sep(LOCAL_local.long_time, "ss", ":");
	}

	// Fix the french date separator

	lang = LOCAL_get_lang();
	if (strcmp(lang, "fr") == 0 || strncmp(lang, "fr_", 3) == 0)
		LOCAL_local.date_sep = '/';

	stradd_sep(LOCAL_local.general_date, LOCAL_local.long_time, " ");
	am_pm = nl_langinfo(AM_STR);
	if (am_pm && *am_pm)
	{
		am_pm = nl_langinfo(PM_STR);
		if (am_pm && *am_pm)
		{
			stradd_sep(LOCAL_local.medium_time, "AM/PM", " ");
		}
	}

	// Currency format

	LOCAL_local.currency_thousand_sep = fix_separator(info->mon_thousands_sep);
	LOCAL_local.len_currency_thousand_sep = strlen(LOCAL_local.currency_thousand_sep);
	
	LOCAL_local.currency_group_size = *(info->mon_grouping);
	if (LOCAL_local.currency_group_size == 0)
		LOCAL_local.currency_group_size = 3;

	LOCAL_local.currency_decimal_point = *(info->mon_decimal_point);
	LOCAL_local.currency_symbol = info->currency_symbol;
	LOCAL_local.intl_currency_symbol = info->int_curr_symbol;

	strcpy(LOCAL_local.general_currency, "($,0.");
	strncat(LOCAL_local.general_currency, "########", Min(8, info->frac_digits));
	strcat(LOCAL_local.general_currency, ")");

	strcpy(LOCAL_local.intl_currency, "($$,0.");
	strncat(LOCAL_local.intl_currency, "########", Min(8, info->int_frac_digits));
	strcat(LOCAL_local.intl_currency, ")");

	init_currency_flag(info);

	LOCAL_local.true_str = STRING_new_zero(LOCAL_gettext(LOCAL_default.true_str));
	LOCAL_local.len_true_str = STRING_length(LOCAL_local.true_str);
	LOCAL_local.false_str = STRING_new_zero(LOCAL_gettext(LOCAL_default.false_str));
	LOCAL_local.len_false_str = STRING_length(LOCAL_local.false_str);
}


void LOCAL_init(void)
{
	_environ = environ;
	LOCAL_set_lang(NULL);
}

void LOCAL_exit(void)
{
	if (environ == _environ)
	{
		unsetenv("LANG");
		unsetenv("LC_ALL");
		unsetenv("LANGUAGE");
	}
	
	if (!LOCAL_is_UTF8)
		STRING_free(&LOCAL_encoding);
	
	STRING_free(&_lang);
	free_local_info();
}


const char *LOCAL_get_lang(void)
{
	char *lang;

	if (!_lang)
	{
		lang = getenv("LC_ALL");
		if (!lang)
			lang = getenv("LANG");
		if (!lang || !*lang)
			lang = "en_US";
		_lang = STRING_new_zero(lang);
	}

	return _lang;
}

void LOCAL_set_lang(const char *lang)
{
	char **l;
	int rtl;
	char *var;

	if (lang && (strlen(lang) > MAX_LANG))
		THROW(E_ARG);
	
	#ifdef DEBUG_LANG
	fprintf(stderr, "******** LOCAL_set_lang: %s ********\n", lang);
	#endif

	if (lang && *lang)
	{
		my_setenv("LANG", lang, env_LANG);
		my_setenv("LC_ALL", lang, env_LC_ALL);
	}
	
	STRING_free(&_lang);
	lang = LOCAL_get_lang();

	if (getenv("LANGUAGE"))
		my_setenv("LANGUAGE", lang, env_LANGUAGE);
	
	if (setlocale(LC_ALL, ""))
	{
		_translation_loaded = FALSE;
		COMPONENT_translation_must_be_reloaded();
	}
	else
	{
		char *err = strerror(errno);
		ERROR_warning("cannot switch to language '%s': %s. Did you install the corresponding locale packages?", lang ? lang : LOCAL_get_lang(), err);
		setlocale(LC_ALL, "C");
	}

	DATE_init_local();
	fill_local_info();

	/* If language is right to left written */

	rtl = FALSE;
	for (l = _rtl_lang; *l; l++)
	{
		if (strncmp(*l, lang, 2) == 0)
		{
			rtl = TRUE;
			break;
		}
	}

	var = getenv("GB_REVERSE");
	if (var && !(var[0] == '0' && var[1] == 0))
		rtl = !rtl;

	HOOK(lang)(lang, rtl);
	LOCAL_local.rtl = rtl;
}


static int int_to_string(uint64_t nbr, char **addr)
{
	static char buf[32];
	char *ptr;
	int digit, len;
	bool neg;

	len = 0;
	ptr = &buf[sizeof(buf)];

	if (nbr == 0)
	{
		ptr -= 2;
		*addr = ptr;
		ptr[0] = '0';
		ptr[1] = 0;
		return 1;
	}
	
	neg = (nbr & (1LL << 63)) != 0;
	
	if (neg)
		nbr = 1 + ~nbr;
	
	while (nbr > 0)
	{
		digit = nbr % 10;
		nbr /= 10;

		ptr--;
		len++;

		*ptr = '0' + digit;
	}

	if (neg)
	{
		len++;
		ptr--;
		*ptr = '-';
	}
	
	*addr = ptr;
	return len;
}


bool LOCAL_format_number(double number, int fmt_type, const char *fmt, int len_fmt, char **str, int *len_str, bool local)
{
	int i;
	unsigned char c;
	int n;
	char *buf_addr;
	int pos;
	int thousand;
	int *thousand_ptr;
	char sign;
	bool comma;
	bool point;
	int before, before_zero;
	int after, after_zero;
	char exponent;
	int exp_zero;
	bool exp_sign;

	int number_sign;
	uint64_t mantisse;
	uint64_t power;
	double number_mant;
	int number_exp;
	int number_real_exp;
	int ndigit;
	int pos_first_digit;

	bool intl_currency;

	if (local)
		local_current = &LOCAL_local;
	else
		local_current = &LOCAL_default;

	switch(fmt_type)
	{
		case LF_USER:
			break;

		case LF_STANDARD:
		case LF_GENERAL_NUMBER:
			if ((number != 0.0) && ((fabs(number) < 1E-4) || (fabs(number) >= 1E10)))
				fmt = "0.###############E+#";
			else
				fmt = "0.###############";
			break;

		case LF_SHORT_NUMBER:
			if ((number != 0.0) && ((fabs(number) < 1E-4) || (fabs(number) >= 1E10)))
				fmt = "0.#######E+#";
			else
				fmt = "0.#######";
			break;

		case LF_FIXED:
			fmt = "0.00";
			break;

		case LF_PERCENT:
			fmt = "###%";
			break;

		case LF_SCIENTIFIC:
			fmt = "0.################E+0";
			break;

		case LF_CURRENCY:
			fmt = local_current->general_currency;
			break;

		case LF_INTERNATIONAL:
			fmt = local_current->intl_currency;
			break;

		default:
			return TRUE;
	}

	if (len_fmt == 0)
		len_fmt = strlen(fmt);

	if (len_fmt >= COMMON_BUF_MAX)
		return TRUE;

	sign = 0;
	comma = FALSE;
	before = 0;
	before_zero = 0;
	point = FALSE;
	after = 0;
	after_zero = 0;
	exponent = 0;
	exp_sign = FALSE;
	exp_zero = 0;
	_currency = FALSE;
	intl_currency = FALSE;

	begin();

	// Search for the first formatting character
	
	for (i = 0; i < len_fmt; i++)
	{
		c = fmt[i];
		if (c == '\\')
		{
			i++;
			continue;
		}
		
		if (c == '-' || c == '+' || c == '#' || c == '0' || c == '.' || c == ',' || c == '(' || c == '$')
			break;
	}
	
	if (i >= len_fmt)
		return TRUE;

	pos = i;
	if (pos > 0)
		add_string(fmt, pos, NULL);

	// Search if there is a percent format character
	
	for (i = pos; i < len_fmt; i++)
	{
		c = fmt[i];
		if (c == '\\')
		{
			i++;
			continue;
		}
		
		if (c == '%')
		{
			number *= 100;
			break;
		}
	}
	
	/* specify the sign */

	if (fmt[pos] == '-')
	{
		sign = ' ';
		pos++;
	}
	else if (fmt[pos] == '+')
	{
		sign = '+';
		pos++;
	}
	else if (fmt[pos] == '(')
	{
		sign = '(';
		pos++;
	}

	if (pos >= len_fmt)
		return TRUE;

	/* currency */

	if (fmt[pos] == '$')
	{
		_currency = TRUE;
		pos++;

		if (fmt[pos] == '$')
		{
			intl_currency = TRUE;
			pos++;
		}
	}

	/* decimal digits */

	for(; pos < len_fmt; pos++)
	{
		c = fmt[pos];

		if (c == ',')
		{
			comma = TRUE;
			continue;
		}

		if (c == '#' || c == '0')
		{
			before++;
			if (c == '0' || before_zero > 0)
				before_zero++;
			continue;
		}

		break;
	}

	if (pos >= len_fmt)
		goto _FORMAT;

	/* the point */

	if (fmt[pos] != '.')
		goto _FORMAT;

	pos++;
	point = TRUE;

	if (pos >= len_fmt)
		goto _FORMAT;

	/* digits after point */

	for(; pos < len_fmt; pos++)
	{
		c = fmt[pos];

		if (c == '#' || c == '0')
		{
			after++;
			if (c == '0')
				after_zero = after;
			continue;
		}

		break;
	}

	if (pos >= len_fmt)
		goto _FORMAT;

	/* exponent */

	if (fmt[pos] == 'e' || fmt[pos] == 'E')
	{
		exponent = fmt[pos];

		pos++;
		if (pos >= len_fmt)
			return TRUE;

		if (fmt[pos] == '-' || fmt[pos] == '+')
		{
			exp_sign = TRUE;
			pos++;
		}

		if (pos >= len_fmt)
			return TRUE;

		for(; pos < len_fmt; pos++)
		{
			c = fmt[pos];

			if (c == '#' || c == '0')
			{
				if (c == '0' || exp_zero > 0)
					exp_zero++;
				continue;
			}

			break;
		}
	}

_FORMAT:

	if (before == 0 && after == 0)
		return TRUE;

	/* sign */

	number_sign = fsgn(number);

	add_sign(sign, number_sign, FALSE);

	/* currency (before) */

	if (_currency && is_currency_before(number_sign < 0, intl_currency))
	{
		add_currency(intl_currency ? local_current->intl_currency_symbol : local_current->currency_symbol);
		if (is_currency_space(number_sign < 0, intl_currency))
			put_char(' ');
	}

	/* We note where the first digit will be printed */

	pos_first_digit = buffer_pos;

	/* the number */

	if (isfinite(number))
	{
		number_mant = frexp10(fabs(number), &number_exp);
		ndigit = after;
		
		if (!exponent) 
			ndigit += number_exp;
		else
			ndigit++;
		
		ndigit = MinMax(ndigit, 0, MAX_FLOAT_DIGIT);
		//fprintf(stderr, "number_mant = %.24g  number_exp = %d  ndigit = %d\n", number_mant, number_exp, ndigit);

		power = pow10_uint64_p(ndigit + 1);

		mantisse = number_mant * power;
		if ((mantisse % 10) >= 5)
			mantisse += 10;

		//fprintf(stderr, "-> power = %" PRId64 " mantisse = %" PRId64 "\n", power, mantisse);

		if (mantisse >= power)
		{
			ndigit = int_to_string(mantisse, &buf_addr);
			buf_addr--;
			buf_addr[0] = buf_addr[1];
			buf_addr[1] = '.';
			ndigit++;
		}
		else
		{
			ndigit = int_to_string(mantisse, &buf_addr);
			buf_addr -= 2;
			buf_addr[0] = '0';
			buf_addr[1] = '.';
			ndigit += 2;
		}

		ndigit--;
		buf_addr[ndigit] = 0;

		/* 0.0 <= number_mant < 1.0 */

		//number_exp++; /* simplifie les choses */

		number_real_exp = number_exp;
		if (exponent)
			number_exp = number != 0.0;

		// should return "0[.]...", or "1[.]..." if the number is rounded up.

		if (buf_addr[0] == '1') // the number has been rounded up.
		{
			if (exponent)
				number_real_exp++;
			else
				number_exp++;
		}

		if (ndigit > 1) // so there is a point
		{
			if (buf_addr[0] == '0')
			{
				buf_addr += 2;
				ndigit -= 2;
			}
			else
			{
				buf_addr[1] = buf_addr[0];
				ndigit--;
				buf_addr++;
			}

			while (ndigit > 0 && buf_addr[ndigit - 1] == '0')
				ndigit--;
		}

		/* digits before point */

		thousand = Max(before, Max(before_zero, number_exp));
		thousand_ptr = comma ? &thousand : NULL;

		if (number_exp > 0)
		{
			add_char(' ', before - Max(before_zero, number_exp), thousand_ptr);
			add_zero(before_zero - number_exp, thousand_ptr);

			add_string(buf_addr, Min(number_exp, ndigit), thousand_ptr);

			if (number_exp > ndigit)
				add_zero(number_exp - ndigit, thousand_ptr);
		}
		else
		{
			add_char(' ', before - before_zero, thousand_ptr);
			add_zero(before_zero, thousand_ptr);
		}

		/* decimal point */

		if (point)
			put_char(local_current->decimal_point);

		/* digits after the decimal point */

		if ((ndigit - number_exp) > 0)
		{
			if (number_exp < 0)
			{
				n = Min(after, (- number_exp));
				if (n == after)
				{
					add_zero(after_zero, NULL);
					goto _EXPOSANT;
				}
				else
				{
					add_zero(n, NULL);
					after -= n;
					after_zero -= n;
				}
			}

			if (number_exp > 0)
			{
				buf_addr += number_exp;
				ndigit -= number_exp;
			}

			n = Min(ndigit, after);
			if (n > 0)
			{
				add_string(buf_addr, n, NULL);
				after -= n;
				after_zero -= n;
			}

			if (after_zero > 0)
				add_zero(after_zero, NULL);
		}
		else
			add_zero(after_zero, NULL);

	_EXPOSANT:

		/* The decimal point is removed if it is located at the end */

		buffer_pos--;
		if (look_char() != local_current->decimal_point)
			buffer_pos++;

		/* exponent */

		if (exponent != 0) // && number != 0.0)
		{
			put_char(exponent);
			if (exp_sign && number_real_exp >= 1)
				put_char('+');
			n = int_to_string(number_real_exp - 1, &buf_addr);
			while (exp_zero > n)
			{
				put_char('0');
				exp_zero--;
			}
			add_string(buf_addr, n, NULL);
		}
	}
	else // isfinite
	{
		if (isnan(number))
			add_string("NaN", 3, NULL);
		else if (isinf(number))
			add_string("Inf", 3, NULL);
	}

	/* currency (after) */

	if (_currency && !is_currency_before(number_sign < 0, intl_currency))
	{
		if (is_currency_space(number_sign < 0, intl_currency))
			put_char(' ');
		add_currency(intl_currency ? local_current->intl_currency_symbol : local_current->currency_symbol);
	}

	/* The last format brace is ignored */

	if (sign == '(' && fmt[pos] == ')')
		pos++;

	/* The sign after */

	add_sign(sign, number_sign, TRUE);

	/* print at least a zero */

	if (buffer_pos == pos_first_digit)
		put_char('0');

	/* suffixe de formatage */

	if (pos < len_fmt)
		add_string(&fmt[pos], len_fmt - pos, NULL);

	/* return the result */

	end(str, len_str);
	return FALSE;
}

static void add_strftime(const char *format, struct tm *tm)
{
	int n;

	n = strftime(get_current(), get_size_left(), format, tm);
	buffer_pos += n;
}


static void add_number(int value, int pad)
{
	static char temp[8] = { 0 };
	int i, n;
	bool minus = FALSE;

	if (value < 0)
	{
		value = (-value);
		minus = TRUE;
	}

	n = 0;
	for (i = 7; i >= 0; i--)
	{
		n++;
		if (value < 10)
		{
			temp[i] = value + '0';
			value = 0;
			if (n >= pad)
				break;
		}
		else
		{
			temp[i] = (value % 10) + '0';
			value /= 10;
		}
	}

	if (minus)
	{
		i--;
		temp[i] = '-';
		n++;
	}

	add_string(&temp[i], n, NULL);
}

static bool add_date_token(DATE_SERIAL *date, char *token, int count)
{
	struct tm tm = {0};
	char buf[8];
	int n;
	bool date_token;

	if (*token == 0)
		return FALSE;

	date_token = *token == 'd' || *token == 'm' || *token == 'y';

	if ((date_token && DATE_SERIAL_has_no_date(date))) // || (!date_token && DATE_SERIAL_has_no_time(date)))
	{
		*token = 0;
		return TRUE;
	}

	switch (*token)
	{
		case 'd':

			if (count <= 2)
			{
				add_number(date->day, (count == 1 ? 0 : 2));
			}
			else if (count >= 3)
			{
				tm.tm_wday = date->weekday;
				add_strftime(count == 3 ? "%a" : "%A", &tm);
			}

			break;

		case 'm':

			if (count <= 2)
			{
				add_number(date->month, (count == 1 ? 0 : 2));
			}
			else if (count >= 3)
			{
				tm.tm_mon = date->month - 1;
				add_strftime(count == 3 ? "%b" : "%B", &tm);
			}

			break;

		case 'y':

			if (count <= 2 && date->year >= 1939 && date->year <= 2038)
				add_number(date->year - (date->year >= 2000 ? 2000 : 1900), 2);
			else
				add_number(date->year, (count == 1 ? 0 : count));

			break;

		case 'h':
		case 'n':
		case 's':

			add_number((*token == 'h') ? date->hour : ((*token == 'n') ? date->min : date->sec), (count == 1 ? 0 : 2));
			break;

		case 'u':

			if (date->msec || count == 2)
			{
				if (count >= 2)
					add_number(date->msec, 3);
				else
				{
					n = snprintf(buf, sizeof(buf), "%03d", date->msec);
					while (buf[n - 1] == '0')
						n--;
					buf[n] = 0;
					add_string(buf, n, NULL);
				}
			}

			break;

		case 't':

			if (count <= 2)
			{
				time_t t = time(NULL);
				localtime_r(&t, &tm);
				add_strftime(count == 2 ? "%z" : "%Z", &tm);
			}
			break;
	}

	*token = 0;
	return FALSE;
}


bool LOCAL_format_date(const DATE_SERIAL *date, int fmt_type, const char *fmt, int len_fmt, char **str, int *len_str)
{
	DATE_SERIAL vdate;
	char c;
	int pos;
	int pos_ampm = -1;
	struct tm date_tm;

	char token;
	int token_count;

	local_current = &LOCAL_local;
	vdate = *date;

	switch(fmt_type)
	{
		case LF_USER:
			break;

		case LF_STANDARD:
		case LF_GENERAL_DATE:
			if (date->year == 0)
			{
				if (date->hour == 0 && date->min == 0 && date->sec == 0)
				{
					*str = NULL;
					*len_str = 0;
					return FALSE;
				}
				fmt = local_current->long_time;
			}
			else
				fmt = local_current->general_date;
			break;

		case LF_LONG_DATE:
			fmt = local_current->long_date;
			break;

		case LF_MEDIUM_DATE:
			fmt = local_current->medium_date;
			break;

		case LF_SHORT_DATE:
			fmt = local_current->short_date;
			break;

		case LF_LONG_TIME:
			fmt = local_current->long_time;
			break;

		case LF_MEDIUM_TIME:
			fmt = local_current->medium_time;
			break;

		case LF_SHORT_TIME:
			fmt = local_current->short_time;
			break;

		default:
			return TRUE;
	}

	if (len_fmt == 0)
		len_fmt = strlen(fmt);

	if (len_fmt >= COMMON_BUF_MAX)
		return TRUE;

	/* looking for AM/PM */

	for (pos = 0; pos < len_fmt - 4; pos++)
	{
		if (fmt[pos] == '\\')
		{
			pos++;
			continue;
		}

		if (strncasecmp(&fmt[pos], "am/pm", 5) == 0)
		{
			pos_ampm = pos;
			if (vdate.hour > 12)
				vdate.hour -= 12;
			else if (vdate.hour == 0)
				vdate.hour = 12;
			break;
		}
	}

	/* Formatting */

	begin();

	token = 0;
	token_count = 0;

	for (pos = 0; pos < len_fmt; pos++)
	{
		c = fmt[pos];
		if (c == '\\')
		{
			pos++;
			if (pos >= len_fmt)
				break;
			add_date_token(&vdate, &token, token_count);
			put_char(fmt[pos]);
			continue;
		}

		if (pos == pos_ampm)
		{
			add_date_token(&vdate, &token, token_count);

			/* convert to struct tm */

			date_tm.tm_sec = date->sec;
			date_tm.tm_min = date->min;
			date_tm.tm_hour = date->hour;
			date_tm.tm_mday = 1;
			date_tm.tm_mon = 0;
			date_tm.tm_year = 0;

			add_strftime((c == 'a' ? "%P" : "%p"), &date_tm);

			pos += 4;
			continue;
		}

		if (c == 'd' || c == 'm' || c == 'y' || c == 'h' || c == 'n' || c == 's' || c == 'u' || c == 't')
		{
			if (c != token)
			{
				add_date_token(&vdate, &token, token_count);

				token = c;
				token_count = 0;
			}

			token_count++;
		}
		else
		{
			if (!add_date_token(&vdate, &token, token_count))
			{
				if (c == '/')
					add_unicode(local_current->date_sep);
				else if (c == ':')
					add_unicode(local_current->time_sep);
				else
					put_char(c);
			}
		}
	}

	add_date_token(&vdate, &token, token_count);

	/* return the result */

	end(str, len_str);
	return FALSE;
}


static void LOCAL_load_translation(ARCHIVE *arch)
{
	char *domain = NULL;
	char *lang_list;
	char *lang;
	char *src;
	char *test;
	char c;
	FILE *file;
	char *addr;
	int len;
	COMPONENT *save = COMPONENT_current;
	char *path;
	const char *dir;
	char *dst;

	/* We must force GB_LoadFile() to look in our archive, because all translation
		files of one language have the same path!
	*/

	if (arch)
	{
		domain = arch->domain;
		COMPONENT_current = COMPONENT_find(arch->name);
	}

	if (!domain)
		domain = "gb";

	#ifdef DEBUG_LANG
	fprintf(stderr, "LOCAL_load_translation: %s / domain = %s\n", arch ? arch->name : "NULL", domain);
	#endif

	lang_list = get_languages();

	lang = strtok(lang_list, ":");

	for(;;)
	{
		if (!lang)
			break;

		if (*lang)
		{
			test = STRING_new_zero(lang);
			src = test;

			for(;;)
			{
				c = *src;
				if (c == 0 || c == '_')
					break;
				*src = tolower(c);
				src++;
			}

			test = STRING_add(test, ".mo", 3);

			dir = arch ? ".lang" : ".../.lang";
			
			FILE_dir_first(dir, test, 0);
			STRING_free_real(test);
			
			if (!FILE_dir_next(&path, &len))
			{
				dst = STRING_new_zero(dir);
				dst = STRING_add_char(dst, '/');
				dst = STRING_add(dst, path, len);
				break;
			}
			
			/*if (!arch)
				dst = FILE_cat("...", ".lang", test, NULL);
			else
				dst = FILE_cat(".lang", test, NULL);
			
			dst = FILE_set_ext(dst, "mo");

			#ifdef DEBUG_LANG
			fprintf(stderr, "trying %s\n", dst);
			#endif

			if (FILE_exist(dst))
				break;*/
		}

		lang = strtok(NULL, ":");
	}

	if (!lang)
	{
		#ifdef DEBUG_LANG
		fprintf(stderr, "No translation\n");
		#endif
		goto __NOTRANS;
	}

	#ifdef DEBUG_LANG
	fprintf(stderr, "Loading %s\n", dst);
	#endif

	STRING_free_later(dst);
	if (GB_LoadFile(dst, 0, &addr, &len))
	{
		#ifdef DEBUG_LANG
		fprintf(stderr, "Cannot load %s\n", dst);
		#endif
		goto __ERROR;
	}

	// These temporary files have predictable names because they
	// are *.mo files read by the gettext system.

	dir = FILE_cat(FILE_make_temp(NULL, NULL), "tr", NULL);
	mkdir(dir, S_IRWXU);

	dir = FILE_cat(FILE_make_temp(NULL, NULL), "tr", setlocale(LC_ALL, NULL), NULL);
	mkdir(dir, S_IRWXU);

	dir = FILE_cat(dir, "LC_MESSAGES", NULL);
	mkdir(dir, S_IRWXU);

	dir = FILE_cat(dir, domain, NULL);
	strcat((char *)dir, ".mo");

	unlink(dir);

	#ifdef DEBUG_LANG
	fprintf(stderr, "Writing to %s (%d bytes)\n", dir, len);
	#endif

	// No need to test previous system calls as the failure will be detected now

	file = fopen(dir, "w");
	if (file)
	{
		fwrite(addr, len, 1, file);
		fclose(file);
	}

	GB_ReleaseFile(addr, len);

__ERROR:

	// If the *.mo was not copied, then the following functions will failed

	#ifdef DEBUG_LANG

		fprintf(stderr, "bindtextdomain: %s\n", bindtextdomain(domain, FILE_cat(FILE_make_temp(NULL, NULL), "tr", NULL)));
		fprintf(stderr, "bind_textdomain_codeset: %s\n", bind_textdomain_codeset(domain, "UTF-8"));
		if (!arch)
			fprintf(stderr, "textdomain: %s\n", textdomain(domain));

	#else

		bindtextdomain(domain, FILE_cat(FILE_make_temp(NULL, NULL), "tr", NULL));
		#ifdef OS_SOLARIS
		fprintf(stderr, "Warning: bind_textdomain_codeset() unavailable.\n");
		#else
		bind_textdomain_codeset(domain, "UTF-8");
		#endif
		if (!arch)
			textdomain(domain); /* default domain */

	#endif

__NOTRANS:

	STRING_free(&lang_list);

	if (arch)
		arch->translation_loaded = TRUE;
	else
		_translation_loaded = TRUE;

	COMPONENT_current = save;
}


const char *LOCAL_gettext(const char *msgid)
{
	const char *tr = msgid;
	ARCHIVE *arch = NULL;

	/*
			If LOCAL_gettext() is called, then we are in the context of
			the archive, so the translation loaded will be the good one.
	*/

	if (!msgid)
		return "";

	if (!ARCHIVE_get_current(&arch))
	{
		#ifdef DEBUG_LANG
		fprintf(stderr, "dgettext(\"%s\", \"%s\")\n", arch->domain, msgid);
		#endif
		if (!arch->translation_loaded)
			LOCAL_load_translation(arch);
		tr = dgettext(arch->domain, msgid);
	}

	if (tr == msgid)
	{
		#ifdef DEBUG_LANG
		fprintf(stderr, "dgettext(\"%s\", \"%s\")\n", "gb", msgid);
		#endif
		if (!_translation_loaded)
			LOCAL_load_translation(NULL);
		tr = dgettext("gb", msgid);
		//tr = gettext(msgid);
	}

	/*printf("tr: %s -> %s\n", msgid, tr);*/

	if (!tr || tr[0] == 0 || (tr[0] == '-' && (tr[1] == 0 || (tr[1] == '\n' && tr[2] == 0))))
		tr = msgid;

	#ifdef DEBUG_LANG
	fprintf(stderr, "--> \"%s\"\n", tr);
	#endif
	
	return tr;
}

int LOCAL_get_first_day_of_week()
{
	const char *lang;

	if (LOCAL_first_day_of_week >= 0)
		return LOCAL_first_day_of_week;

	lang = LOCAL_get_lang();

	if (strcmp(lang, "en") == 0 || strncmp(lang, "en_", 3) == 0 || strcmp(lang, "C") == 0)
		return 0;
	else
		return 1;
}

void LOCAL_set_first_day_of_week(char day)
{
	if (day >= -1 && day <= 6)
		LOCAL_first_day_of_week = day;
}
