package i18n
/*
	Internationalization helpers.

	Copyright 2021-2022 Jeroen van Rijn <nom@duclavier.com>.
	Made available under Odin's license.

	List of contributors:
		Jeroen van Rijn: Initial implementation.
*/
import    "core:strings"

// Currently active catalog.
ACTIVE: ^Translation

// Allow between 1 and 255 plural forms. Default: 10.
MAX_PLURALS :: min(max(#config(ODIN_i18N_MAX_PLURAL_FORMS, 10), 1), 255)

// The main data structure. This can be generated from various different file formats, as long as we have a parser for them.
Section :: map[string][]string

Translation :: struct {
	k_v:    map[string]Section, // k_v[section][key][plural_form] = ...
	intern: strings.Intern,

	pluralize: proc(number: int) -> int,
}

Error :: enum {
	// General return values.
	None = 0,
	Empty_Translation_Catalog,
	Duplicate_Key,

	// Couldn't find, open or read file.
	File_Error,

	// File too short.
	Premature_EOF,

	// GNU Gettext *.MO file errors.
	MO_File_Invalid_Signature,
	MO_File_Unsupported_Version,
	MO_File_Invalid,
	MO_File_Incorrect_Plural_Count,

	// Qt Linguist *.TS file errors.
	TS_File_Parse_Error,
	TS_File_Expected_Context,
	TS_File_Expected_Context_Name,
	TS_File_Expected_Source,
	TS_File_Expected_Translation,
	TS_File_Expected_NumerusForm,
	Bad_Str,
	Bad_Id,

}

Parse_Options :: struct {
	merge_sections: bool,
}

DEFAULT_PARSE_OPTIONS :: Parse_Options{
	merge_sections = false,
}

/*
	Returns the first translation string for the passed `key`.
	It is also aliased with `get()`.

	Two ways to use it:
	- get(key), which defaults to the `i18n.ACTIVE` catalogue, or
	- get(key, catalog) to grab text from a specific loaded catalogue

	Inputs:
	- key:     the string to translate
	- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)

	Returns:   the translated string, or the original `key` if no translation was found.
*/
get_single_section :: proc(key: string, catalog: ^Translation = ACTIVE) -> (value: string) {
	return get_by_slot(key, 0, catalog)
}

/*
	Returns the first translation string for the passed `key` in a specific section or context.
	It is also aliases with `get()`.

	Two ways to use it:
	- get(section, key), which defaults to the `i18n.ACTIVE` catalogue, or
	- get(section, key, catalog) to grab text from a specific loaded catalogue

	Inputs:
	- section: the catalogue section (sometimes also called 'context') in which to look up the translation
	- key:     the string to translate
	- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)

	Returns:   the translated string, or the original `key` if no translation was found.
*/
get_by_section :: proc(section, key: string, catalog: ^Translation = ACTIVE) -> (value: string) {
	return get_by_slot(section, key, 0, catalog)
}

get :: proc{get_single_section, get_by_section}

/*
	Returns the translation string for the passed `key` in a specific plural form (if present in the catalogue).
	It is also aliased with `get_n()`.

	Two ways to use it:
	- get_n(key, quantity), which returns the appropriate plural from the active catalogue, or
	- get_n(key, quantity, catalog) to grab text from a specific loaded catalogue

	Inputs:
	- key:      the string to translate
	- quantity: the quantity of item to be used to select the correct plural form
	- catalog:  the catalogue to use for the translation (defaults to i18n.ACTIVE)

	Returns:    the translated string, or the original `key` if no translation was found.
*/
get_single_section_with_quantity :: proc(key: string, quantity: int, catalog: ^Translation = ACTIVE) -> (value: string) {
	/*
		A lot of languages use singular for 1 item and plural for 0 or more than 1 items. This is our default pluralize rule.
	*/
	slot := 1 if quantity != 1 else 0

	if catalog.pluralize != nil {
		slot = catalog.pluralize(quantity)
	}
	return get_by_slot(key, slot, catalog)
}

/*
	Returns the translation string for the passed `key` in a specific plural form (if present in the catalogue)
	in a specific section or context.
	It is also aliases with `get_n()`.

	Two ways to use it:
	- get(section, key, quantity), which returns the appropriate plural from the active catalogue, or
	- get(section, key, quantity, catalog) to grab text from a specific loaded catalogue

	Inputs:
	- section: the catalogue section (sometime also called 'context') from which to lookup the translation
	- key:     the string to translate
	- qantity: the quantity of item to be used to select the correct plural form
	- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)

	Returns:   the translated string, or the original `key` if no translation was found
*/
get_by_section_with_quantity :: proc(section, key: string, quantity: int, catalog: ^Translation = ACTIVE) -> (value: string) {
	/*
		A lot of languages use singular for 1 item and plural for 0 or more than 1 items. This is our default pluralize rule.
	*/
	slot := 1 if quantity != 1 else 0

	if catalog.pluralize != nil {
		slot = catalog.pluralize(quantity)
	}
	return get_by_slot(section, key, slot, catalog)
}
get_n :: proc{get_single_section_with_quantity, get_by_section_with_quantity}

/*
	Two ways to use:
	- get_by_slot(key, slot), which returns the requested plural from the active catalogue, or
	- get_by_slot(key, slot, catalog) to grab text from a specific loaded catalogue.

	If a file format parser doesn't (yet) support plural slots, each of the slots will point at the same string.
	- section: the catalogue section (sometime also called 'context') from which to lookup the translation

	Inputs:
	- key:     the string to translate.
	- slot:    the translation slot to choose (slots refer to plural forms specific for each language and their meaning changes from catalogue to catalogue).
	- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)

	Returns:   the translated string, or the original `key` if no translation was found.
*/
get_by_slot_single_section :: proc(key: string, slot: int, catalog: ^Translation = ACTIVE) -> (value: string) {
	return get_by_slot_by_section("", key, slot, catalog)
}

/*
	Two ways to use:
	- get_by_slot(key, slot), which returns the requested plural from the active catalog, or
	- get_by_slot(key, slot, catalog) to grab text from a specific one.

	If a file format parser doesn't (yet) support plural slots, each of the slots will point at the same string.

	Inputs:
	- section: the catalogue section (sometime also called 'context') from which to lookup the translation
	- key:     the string to translate.
	- slot:    the translation slot to choose (slots refer to plural forms specific for each language and their meaning changes from catalogue to catalogue).
	- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)

	Returns:   the translated string or the original `key` if no translation was found.
*/
get_by_slot_by_section :: proc(section, key: string, slot: int, catalog: ^Translation = ACTIVE) -> (value: string) {
	if catalog == nil || section not_in catalog.k_v {
		// Return the key if the catalog catalog hasn't been initialized yet, or the section is not present.
		return key
	}

	// Return the translation from the requested slot if this key is known, else return the key.
	if translations, ok := catalog.k_v[section][key]; ok {
		plural := min(max(0, slot), len(catalog.k_v[section][key]) - 1)
		return translations[plural]
	}
	return key
}
get_by_slot :: proc{get_by_slot_single_section, get_by_slot_by_section}

/*
	- destroy(), to clean up the currently active catalog catalog i18n.ACTIVE
	- destroy(catalog), to clean up a specific catalog.
*/
destroy :: proc(catalog: ^Translation = ACTIVE, allocator := context.allocator) {
	context.allocator = allocator

	if catalog == nil {
		return
	}

	for section in catalog.k_v {
		for key in catalog.k_v[section] {
			delete(catalog.k_v[section][key])
		}
		delete(catalog.k_v[section])
	}
	delete(catalog.k_v)
	strings.intern_destroy(&catalog.intern)
	free(catalog)
}