/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 */

#include "glk/adrift/scare.h"
#include "glk/adrift/scprotos.h"

namespace Glk {
namespace Adrift {

/*
 * Module notes:
 *
 * o Standard libc ctype.h functions vary their results according to the
 *   currently set locale.  We want consistent Windows codepage 1252 or
 *   codepage 1251 (WinLatin1 or WinCyrillic) results.  To get this, then,
 *   we have to define the needed functions internally to SCARE.
 */

/*
 * All the ctype boolean and character tables contain 256 elements, one for
 * each possible sc_char value.  This is used to size arrays and to verify
 * that range setting functions do not overrun array boundaries.
 */
enum { TABLE_SIZE = 256 };


/*
 * loc_setrange_bool()
 * loc_setranges_bool()
 *
 * Helpers for building ctype tables.  Sets all elements from start to end
 * inclusive to TRUE, and iterate this on a ranges array.
 */
static void loc_setrange_bool(sc_int start, sc_int end, sc_bool table[]) {
	sc_int index_;

	for (index_ = start; index_ <= end; index_++) {
		assert(index_ > -1 && index_ < TABLE_SIZE);
		table[index_] = TRUE;
	}
}

static void loc_setranges_bool(const sc_int ranges[], sc_bool table[]) {
	sc_int index_;

	for (index_ = 0; ranges[index_] > -1; index_ += 2) {
		assert(ranges[index_] <= ranges[index_ + 1]);
		loc_setrange_bool(ranges[index_], ranges[index_ + 1], table);
	}
}


/*
 * loc_setrange_char()
 * loc_setranges_char()
 *
 * Helpers for building ctype conversion tables.  Sets all elements from start
 * to end inclusive to their index value plus the given offset, and iterate
 * this on a ranges array.
 */
static void loc_setrange_char(sc_int start, sc_int end, sc_int offset, sc_char table[]) {
	sc_int index_;

	for (index_ = start; index_ <= end; index_++) {
		assert(index_ > -1 && index_ < TABLE_SIZE);
		assert(index_ + offset > -1 && index_ + offset < TABLE_SIZE);
		table[index_] = index_ + offset;
	}
}

static void loc_setranges_char(const sc_int ranges[], sc_char table[]) {
	sc_int index_;

	for (index_ = 0; ranges[index_] > -1; index_ += 3) {
		assert(ranges[index_] <= ranges[index_ + 1]);
		loc_setrange_char(ranges[index_],
		                  ranges[index_ + 1], ranges[index_ + 2], table);
	}
}


/*
 * A locale consists of a name, ranges for each table, and signatures for
 * autodetection based on the game's compilation date.  This is the static
 * data portion of a locale.
 */
enum { RANGES_LENGTH = 32 };
enum { SIGNATURE_COUNT = 24, SIGNATURE_LENGTH = 3 };
struct sc_locale_t {
	const sc_char *const name;
	const sc_int isspace_ranges[RANGES_LENGTH];
	const sc_int isdigit_ranges[RANGES_LENGTH];
	const sc_int isalpha_ranges[RANGES_LENGTH];
	const sc_int toupper_ranges[RANGES_LENGTH];
	const sc_int tolower_ranges[RANGES_LENGTH];
	const sc_byte signature[SIGNATURE_COUNT][SIGNATURE_LENGTH];
};


/*
 * The locale table set is built from a locale using its ranges.  There is one
 * table for each function, and a pointer to the locale used to construct the
 * table, for synchronization with changed locales.  This is the dynamic data
 * portion of a locale.
 */
struct sc_locale_table_t {
	const sc_locale_t *locale;
	sc_bool isspace[TABLE_SIZE];
	sc_bool isdigit[TABLE_SIZE];
	sc_bool isalpha[TABLE_SIZE];
	sc_char toupper[TABLE_SIZE];
	sc_char tolower[TABLE_SIZE];
};

/*
 * Define a single static locale table set.  This set re-initializes if it
 * detects a locale change.
 */
static sc_locale_table_t loc_locale_tables = {nullptr, {0}, {0}, {0}, {0}, {0}};


/*
 * loc_synchronize_tables()
 * loc_check_tables_synchronized()
 *
 * Initialize tables for a locale.  And compare the locale tables to a locale
 * and if not for the same locale, (re-)initialize.
 */
static void loc_synchronize_tables(const sc_locale_t *locale) {
	/* Clear all tables and the locale pointer. */
	memset(&loc_locale_tables, 0, sizeof(loc_locale_tables));

	/* Set ranges and attach the new locale. */
	loc_setranges_bool(locale->isspace_ranges, loc_locale_tables.isspace);
	loc_setranges_bool(locale->isdigit_ranges, loc_locale_tables.isdigit);
	loc_setranges_bool(locale->isalpha_ranges, loc_locale_tables.isalpha);
	loc_setranges_char(locale->toupper_ranges, loc_locale_tables.toupper);
	loc_setranges_char(locale->tolower_ranges, loc_locale_tables.tolower);

	loc_locale_tables.locale = locale;
}

static void loc_check_tables_synchronized(const sc_locale_t *locale) {
	if (locale != loc_locale_tables.locale)
		loc_synchronize_tables(locale);
}


/*
 * Locale for Latin1.  The signatures in this locale are null since it is the
 * default locale; no matching required.  Also, none may be practical, as this
 * locale works for a large number of Western European languages (though in
 * practice, it seems that only English and French Adrift Latin1 games exist).
 */
static const sc_locale_t LATIN1_LOCALE = {
	"Latin1",
	{9, 13,  32, 32,  160, 160,  -1},
	{48, 57,  -1},
	{
		65, 90,  97, 122,  192, 214,  216, 246,  248, 255,  138, 138,  140, 140,
		142, 142,  154, 154,  156, 156,  158, 158,  159, 159,  -1
	},
	{
		0, TABLE_SIZE - 1, 0,  97, 122, -32,  224, 246, -32,  248, 254, -32,  154, 154, -16,
		156, 156, -16,  158, 158, -16,  255, 255, -96,  -1
	},
	{
		0, TABLE_SIZE - 1, 0,  65, 90, 32,  192, 214, 32,  216, 222, 32,  138, 138, 16,
		140, 140, 16,  142, 142, 16,  159, 159, 96,  -1
	},
	{{0}}
};


/*
 * Locale for Cyrillic.  The signatures in this locale are month names in
 * both mixed case and lowercase Russian Cyrillic.
 */
static const sc_locale_t CYRILLIC_LOCALE = {
	"Cyrillic",
	{9, 13,  32, 32,  160, 160,  -1},
	{48, 57,  -1},
	{
		65, 90,  97, 122,  168, 168,  184, 184,  175, 175,  191, 191,  178, 179,
		192, 255,  -1
	},
	{
		0, TABLE_SIZE - 1, 0,  97, 122, -32,  184, 184, -16,  191, 191, -16,  179, 179, -1,
		224, 255, -32,  -1
	},
	{
		0, TABLE_SIZE - 1, 0,  65, 90, 32,  168, 168, 16,  175, 175, 16,  178, 178, 1,
		192, 223, 32,  -1
	},
	{	{223, 237, 226}, {212, 229, 226}, {204, 224, 240}, {192, 239, 240},
		{204, 224, 233}, {200, 254, 237}, {200, 254, 235}, {192, 226, 227},
		{209, 229, 237}, {206, 234, 242}, {205, 238, 255}, {196, 229, 234},
		{255, 237, 226}, {244, 229, 226}, {236, 224, 240}, {224, 239, 240},
		{236, 224, 233}, {232, 254, 237}, {232, 254, 235}, {224, 226, 227},
		{241, 229, 237}, {238, 234, 242}, {237, 238, 255}, {228, 229, 234}
	}
};


/* List of pointers to supported and available locales, NULL terminated. */
static const sc_locale_t *const AVAILABLE_LOCALES[] = {
	&LATIN1_LOCALE,
	&CYRILLIC_LOCALE,
	nullptr
};

/*
 * The locale for the game, set below explicitly or on game startup, and
 * a flag to note if it's been set explicitly, to prevent autodetection from
 * overwriting a manual setting.
 */
static const sc_locale_t *loc_locale = &LATIN1_LOCALE;
static sc_bool loc_is_autodetect_enabled = TRUE;


/*
 * loc_locate_signature_in_date()
 *
 * Checks the format of the input date to ensure it matches the format
 * "dd [Mm]mm yyyy".  Returns the address of the month part of the string, or
 * NULL if it doesn't match the expected format.
 */
static const sc_char *loc_locate_signature_in_date(const sc_char *date) {
	sc_int day, year, converted;
	sc_char signature[SIGNATURE_LENGTH + 1];

	/* Clear signature, and convert using a scanf format. */
	memset(signature, 0, sizeof(signature));
	converted = sscanf(date, "%2ld %3[^ 0-9] %4ld", &day, signature, &year);

	/* Valid if we converted three values, and month has three characters. */
	if (converted == 3 && strlen(signature) == SIGNATURE_LENGTH)
		return strstr(date, signature);
	else
		return nullptr;
}


/*
 * loc_compare_locale_signatures()
 *
 * Search a locale's signatures for a match with the signature passed in.
 * Returns TRUE if a match found, FALSE otherwise.  Uses memcmp rather than
 * any strcasecmp() variant because the signatures are in the locale's
 * codepage, but the locale is not yet (by definition) set.
 */
static sc_bool loc_compare_locale_signatures(const char *signature, const sc_locale_t *locale) {
	sc_int index_;
	sc_bool is_matched;

	/* Compare signatures, stopping on the first match found. */
	is_matched = FALSE;
	for (index_ = 0; index_ < SIGNATURE_COUNT; index_++) {
		if (memcmp(locale->signature[index_],
		           signature, sizeof(locale->signature[0])) == 0) {
			is_matched = TRUE;
			break;
		}
	}

	return is_matched;
}


/*
 * loc_find_matching_locale()
 *
 * Look at the incoming date, expected to be in the format "dd [Mm]mm yyyy",
 * where "[Mm]mm" is a standard month abbreviation of the locale in which the
 * Generator was run.  Match this with locale signatures, and return the
 * first locale that matches, or NULL if none match.
 */
static const sc_locale_t *loc_find_matching_locale(const sc_char *date,
		const sc_locale_t *const *locales) {
	const sc_char *signature;
	const sc_locale_t *matched = nullptr;

	/* Get the month part of date, and if valid, search locale signatures. */
	signature = loc_locate_signature_in_date(date);
	if (signature) {
		const sc_locale_t *const *iterator;

		/* Search for this signature in the locale's signatures. */
		for (iterator = locales; *iterator; iterator++) {
			if (loc_compare_locale_signatures(signature, *iterator)) {
				matched = *iterator;
				break;
			}
		}
	}

	/* Return the matching locale, NULL if none matched. */
	return matched;
}


/*
 * loc_detect_game_locale()
 *
 * Set an autodetected value for the locale based on looking at a game's
 * compilation date.
 */
void loc_detect_game_locale(sc_prop_setref_t bundle) {
	assert(bundle);

	/* If an explicit locale has already been set, ignore the call. */
	if (loc_is_autodetect_enabled) {
		sc_vartype_t vt_key[1];
		const sc_char *compile_date;
		const sc_locale_t *matched;

		/* Read the game's compilation date from the properties. */
		vt_key[0].string = "CompileDate";
		compile_date = prop_get_string(bundle, "S<-s", vt_key);

		/* Search for a matching locale based on the game compilation date. */
		matched = loc_find_matching_locale(compile_date, AVAILABLE_LOCALES);

		/* If a locale matched, set the global locale to it. */
		if (matched)
			loc_locale = matched;
	}
}


/*
 * loc_ascii_tolower()
 * loc_ascii_strncasecmp()
 *
 * The standard sc_strncasecmp() calls sc_tolower(), which is locale specific.
 * This isn't a particular problem because it will set the Latin1 locale
 * automatically before continuing.  However, since locale names should always
 * be in ascii anyway, it's slightly safer to just use an ascii-only version
 * of this function.
 */
static sc_char loc_ascii_tolower(sc_char ch) {
	return (ch >= 'A' && ch <= 'Z') ? ch - 'A' + 'a' : ch;
}

static sc_int loc_ascii_strncasecmp(const sc_char *s1, const sc_char *s2, sc_int n) {
	sc_int index_;

	for (index_ = 0; index_ < n; index_++) {
		sc_int diff;

		diff = loc_ascii_tolower(s1[index_]) - loc_ascii_tolower(s2[index_]);
		if (diff < 0 || diff > 0)
			return diff < 0 ? -1 : 1;
	}

	return 0;
}


/*
 * loc_set_locale()
 * loc_get_locale()
 *
 * Set a locale explicitly from the name passed in, returning TRUE if a locale
 * matched the name.  Get the current locale, which may be the default locale
 * if none yet set.
 */
sc_bool loc_set_locale(const sc_char *name) {
	const sc_locale_t *matched = nullptr;
	const sc_locale_t *const *iterator;
	assert(name);

	/*
	 * Search locales for a matching name, abbreviated if necessary.  Stop on
	 * the first match found.
	 */
	for (iterator = AVAILABLE_LOCALES; *iterator; iterator++) {
		const sc_locale_t *const locale = *iterator;

		if (loc_ascii_strncasecmp(name, locale->name, strlen(name)) == 0) {
			matched = locale;
			break;
		}
	}

	/* If matched, set the global locale, and lock out future autodetection. */
	if (matched) {
		loc_locale = matched;
		loc_is_autodetect_enabled = FALSE;
	}

	return matched ? TRUE : FALSE;
}

const sc_char *loc_get_locale(void) {
	return loc_locale->name;
}


/*
 * loc_debug_dump_new_line()
 * loc_debug_dump_bool_table()
 * loc_debug_dump_char_table()
 * loc_debug_dump()
 *
 * Print out locale tables.
 */
static int loc_debug_dump_new_line(sc_int index_, sc_int count) {
	return index_ < TABLE_SIZE - 1 && index_ % count == count - 1;
}

static void loc_debug_dump_bool_table(const sc_char *label, sc_int count, const sc_bool table[]) {
	sc_int index_;

	sc_trace("loc_locale_tables.%s = {\n  ", label);
	for (index_ = 0; index_ < TABLE_SIZE; index_++) {
		sc_trace("%s%s", table[index_] ? "T" : "F",
		         loc_debug_dump_new_line(index_, count) ? "\n  " : "");
	}
	sc_trace("\n}\n");
}

static void loc_debug_dump_char_table(const sc_char *label, sc_int count, const sc_char table[]) {
	sc_int index_;

	sc_trace("loc_locale_tables.%s = {\n  ", label);
	for (index_ = 0; index_ < TABLE_SIZE; index_++) {
		sc_trace("%02lx%s", (sc_int)(sc_byte) table[index_],
		         loc_debug_dump_new_line(index_, count) ? "\n  " : " ");
	}
	sc_trace("\n}\n");
}

void loc_debug_dump(void) {
	sc_trace("Locale: debug dump follows...\n");

	loc_check_tables_synchronized(loc_locale);
	sc_trace("loc_locale_tables"
	         ".locale->name = %s\n", loc_locale_tables.locale->name);

	loc_debug_dump_bool_table("isspace", 64, loc_locale_tables.isspace);
	loc_debug_dump_bool_table("isdigit", 64, loc_locale_tables.isdigit);
	loc_debug_dump_bool_table("isalpha", 64, loc_locale_tables.isalpha);
	loc_debug_dump_char_table("toupper", 16, loc_locale_tables.toupper);
	loc_debug_dump_char_table("tolower", 16, loc_locale_tables.tolower);
}


/*
 * loc_bool_template()
 * loc_char_template()
 *
 * "Template" functions for locale variant ctype functions.  Synchronize
 * tables to the currently set locale, and return the value from the table.
 */
static sc_bool loc_bool_template(sc_char character, const sc_bool table[]) {
	loc_check_tables_synchronized(loc_locale);
	return table[(sc_byte) character];
}

static sc_char loc_char_template(sc_char character, const sc_char table[]) {
	loc_check_tables_synchronized(loc_locale);
	return table[(sc_byte) character];
}


/*
 * sc_isspace()
 * sc_isalpha()
 * sc_isdigit()
 * sc_tolower()
 * sc_toupper()
 *
 * Public entry points into locale variant ctype functions.
 */
sc_bool sc_isspace(sc_char character) {
	return loc_bool_template(character, loc_locale_tables.isspace);
}

sc_bool sc_isalpha(sc_char character) {
	return loc_bool_template(character, loc_locale_tables.isalpha);
}

sc_bool sc_isdigit(sc_char character) {
	return loc_bool_template(character, loc_locale_tables.isdigit);
}

sc_char sc_toupper(sc_char character) {
	return loc_char_template(character, loc_locale_tables.toupper);
}

sc_char sc_tolower(sc_char character) {
	return loc_char_template(character, loc_locale_tables.tolower);
}

} // End of namespace Adrift
} // End of namespace Glk
