/**
 * Maps the Espruino datetime format to min and max character lengths.
 * Used when determining if a format can produce outputs that are too short or long.
 */
const datetime_length_map = {
	// %A, %a, %B, %b vary depending on the locale, so they are calculated later
	"%Y": [4, 4, "2024", "2024"],
	"%y": [2, 2, "24", "24"],
	"%m": [2, 2, "10", "10"],
	"%-m": [1, 2, "1", "10"],
	"%d": [2, 2, "10", "10"],
	"%-d": [1, 2, "1", "10"],
	"%HH": [2, 2, "10", "10"],
	"%MM": [2, 2, "10", "10"],
	"%SS": [2, 2, "10", "10"],
};

/**
 * Takes an Espruino datetime format string and returns the minumum and maximum possible length of characters that the format could use.
 *
 * @param {string} datetimeEspruino - The datetime Espruino format
 * @returns first the minimum possible length, second the maximum possible length.
 */
function getLengthOfDatetimeFormat(name, datetimeEspruino, locale, errors) {

	// Generate the length_map based on the actual names in the locale
	const length_map = {...datetime_length_map};
	for(const [symbol, values] of [
		["%A", locale.day],
		["%a", locale.abday],
		["%B", locale.month],
		["%b", locale.abmonth],
	]) {
		const length = [Infinity, 0];
		for(const value of values.split(",")){
			if(length[0] > value.length) { length[0] = value.length; length[2] = value; }
			if(length[1] < value.length) { length[1] = value.length; length[3] = value; }
		}
		length_map[symbol] = length;
	}

	// Find the length of the output
	let formatLength = [0, 0, "", ""];
	let i = 0;
	while (i < datetimeEspruino.length) {
        let ch = datetimeEspruino[i];
		if (ch === "%") {
			let match;
			for(const symbolLength of [2, 3]){
				const length = length_map[datetimeEspruino.substring(i, i+symbolLength)];
				if(length){
					match = {
						length,
						symbolLength,
					}
				}
			}
			if(match){
				formatLength[0] += match.length[0];
				formatLength[1] += match.length[1];
                formatLength[2] += match.length[2];
                formatLength[3] += match.length[3];
				i += match.symbolLength;
			}else{
				errors.push({name, value: datetimeEspruino, lang: locale.lang, error: `uses an unsupported format symbol: ${datetimeEspruino.substring(i, i+3)}`});
				formatLength[0]++;
				formatLength[1]++;
				formatLength[2]+=" ";
				formatLength[3]+=" ";
				i++;
			}
		} else {
			formatLength[0]++;
			formatLength[1]++;
			formatLength[2]+=ch;
			formatLength[3]+=ch;
			i++;
		}
	}
	return formatLength;
}

/**
 * Checks that a locale conforms to some basic standards.
 *
 * @param {object} locale - The locale to test.
 * @param {object} meta - Meta information that is needed to check if locales are supported.
 * @param {object} meta.speedUnits - The table of speed units.
 * @param {object} meta.distanceUnits - The table of distance units.
 * @param {object} meta.codePages - Custom codepoint mappings.
 * @param {object} meta.CODEPAGE_CONVERSIONS - The table of custom codepoint conversions.
 * @returns an object with an array of errors and warnings.
 */
function checkLocale(locale, {speedUnits, distanceUnits, codePages, CODEPAGE_CONVERSIONS}){
	const errors = [];
	const warnings = [];

	const speeds = Object.keys(speedUnits);
	const distances = Object.keys(distanceUnits);

	checkLength("lang", locale.lang, 4, undefined);
	checkLength("decimal point", locale.decimal_point, 1, 1);
	checkLength("thousands separator", locale.thousands_sep, 1, 1);
	checkLength("speed", locale.speed, 2, 4);
	checkIsIn("speed", locale.speed, "speedUnits", speeds);
	checkLength("distance", locale.distance["0"], 1, 3);
	checkLength("distance", locale.distance["1"], 1, 3);
	checkIsIn("distance", locale.distance["0"], "distanceUnits", distances);
	checkIsIn("distance", locale.distance["1"], "distanceUnits", distances);
	checkLength("temperature", locale.temperature, 1, 2);
	checkLength("meridian", locale.ampm["0"], 1, 3);
	checkLength("meridian", locale.ampm["1"], 1, 3);
	warnIfNot("long time format", locale.timePattern["0"], "%HH:%MM:%SS");
	warnIfNot("short time format", locale.timePattern["1"], "%HH:%MM");
	checkFormatLength("long time", locale.timePattern["0"], 8, 8);
	checkFormatLength("short time", locale.timePattern["1"], 5, 5);
	checkFormatLength("long date", locale.datePattern["0"], 6, 14);
	checkFormatLength("short date", locale.datePattern["1"], 6, 11);
	checkArrayLength("short months", locale.abmonth.split(","), 12, 12);
	checkArrayLength("long months", locale.month.split(","), 12, 12);
	checkArrayLength("short days", locale.abday.split(","), 7, 7);
	checkArrayLength("long days", locale.day.split(","), 7, 7);
	for (const abmonth of locale.abmonth.split(",")) {
		checkLength("short month", abmonth, 2, 4);
	}
	for (const month of locale.month.split(",")) {
		checkLength("month", month, 3, 11);
	}
	for (const abday of locale.abday.split(",")) {
		checkLength("short day", abday, 2, 4);
	}
	for (const day of locale.day.split(",")) {
		checkLength("day", day, 3, 13);
	}
	checkEncoding(locale);

	function checkLength(name, value, min, max) {
		if(typeof value !== "string"){
			errors.push({name, value, lang: locale.lang, error: `must be defined and must be a string`});
			return;
		}
		if (min && value.length < min) {
			errors.push({name, value, lang: locale.lang, error: `must be longer than ${min-1} characters`});
		}
		if (max && value.length > max) {
			errors.push({name, value, lang: locale.lang, error: `must be shorter than ${max+1} characters`});
		}
	}
	function checkArrayLength(name, value, min, max){
		if(!Array.isArray(value)){
			errors.push({name, value, lang: locale.lang, error: `must be defined and must be an array`});
			return;
		}
		if (min && value.length < min) {
			errors.push({name, value, lang: locale.lang, error: `array must be longer than ${min-1} entries`});
		}
		if (max && value.length > max) {
			errors.push({name, value, lang: locale.lang, error: `array must be shorter than ${max+1} entries`});
		}
	}
	function checkFormatLength(name, value, min, max) {
		const length = getLengthOfDatetimeFormat(name, value, locale, errors);
		if (min && length[0] < min) {
			errors.push({name, value, lang: locale.lang, error: `output must be longer than ${min-1} characters (${length[2]} -> ${length[0]})`});
		}
		if (max && length[1] > max) {
			errors.push({name, value, lang: locale.lang, error: `output must be shorter than ${max+1} characters (${length[3]} -> ${length[1]})`});
		}
	}
	function checkIsIn(name, value, listName, list) {
		if (!list.includes(value)) {
			errors.push({name, value, lang: locale.lang, error: `must be included in the ${listName} map`});
		}
	}
	function warnIfNot(name, value, expected) {
		if (value !== expected) {
			warnings.push({name, value, lang: locale.lang, error: `might not work in some apps if it is not "${expected}"`});
		}
	}
	function checkEncoding(object) {
		if(!object){
			return;
		}else if(typeof object === "string"){
			for(const char of object){
				const charCode = char.charCodeAt();
				if (charCode >= 32 && charCode < 128) {
					// ASCII - fully supported
					continue;
				} else if (codePages["ISO8859-1"].map.indexOf(char) >= 0) {
					// At upload time, the char can be converted to a custom codepage
					continue;
				} else if (CODEPAGE_CONVERSIONS[char]) {
					// At upload time, the char can be converted to a similar supported char
					continue;
				}
				errors.push({name: `character ${char}`, value: char, lang: locale.lang, error: `is not supported by BangleJS`});
			}
		}else{
			for(const [key, value] of Object.entries(object)){
				if(key === "icon") continue;
				checkEncoding(value);
			}
		}
	}

	return {errors, warnings};
}

/**
 * Checks that an array of locales conform to some basic standards.
 *
 * @param {object[]} locales - The locales to test.
 * @param {object} meta.speedUnits - The table of speed units.
 * @param {object} meta.distanceUnits - The table of distance units.
 * @param {object} meta.codePages - Custom codepoint mappings.
 * @param {object} meta.CODEPAGE_CONVERSIONS - The table of custom codepoint conversions.
 * @returns an object with an array of errors and warnings.
 */
function checkLocales(locales, meta){
	let errors = [];
	let warnings = [];

	for(const locale of Object.values(locales)){
		const result = checkLocale(locale, meta);
		errors = [...errors, ...result.errors];
		warnings = [...warnings, ...result.warnings];
	}

	return {errors, warnings};
}

if(typeof module !== "undefined"){
	module.exports = {
		checkLocale,
		checkLocales,
	};
}else{
	globalThis.checkLocale = checkLocale;
	globalThis.checkLocales = checkLocales;
}
