@tool
extends Node

const _SYMBOLS_TO_STRING: Dictionary[String, String] = {
	"#": "sharp",
	"@": "at",
	"*": "star",
	"$": "usd",
	"%": "pcent",
	"§": "para"
}
const BASE_PATH: String = "user://language_resources/"
const TRACING_DATA_FOLDER: String = "tracing_data/"
const LOOK_AND_LEARN_IMAGES: String = "/look_and_learn/images/"
const LOOK_AND_LEARN_SOUNDS: String = "/look_and_learn/sounds/"
const LOOK_AND_LEARN_VIDEOS: String = "/look_and_learn/video/"
const LANGUAGE_SOUNDS: String = "/language_sounds/"
const KALULU_FOLDER: String = "/kalulu/"
const ADDITIONAL_WORD_LIST_PATH: String = "word_list.csv"
const VIDEO_EXTENSION: String = ".ogv"
const IMAGE_EXTENSION: String = ".png"
const SOUND_EXTENSION: String = ".mp3"

var language: String:
	set(value):
		Log.trace("Database: set language from %s to %s" % [language, value])
		language = value
		db_path = get_language_folder() + "/language.db"
		words_path = get_language_folder() + "/words/"
var db_path: String = get_language_folder() + "/language.db":
	set(value):
		db_path = value
		if db:
			db.path = db_path
			db.foreign_keys = true
			connect_to_db()
var words_path: String = get_language_folder() + "/words/"
var additional_word_list: Dictionary = {}
var is_open: bool = false

@onready var db: SQLite = SQLite.new()


func _exit_tree() -> void:
	db.close_db()


func connect_to_db() -> void:
	if is_open:
		Log.trace("Database: Opening database while a connection is already opened. Closing the previous one.")
		db.close_db()
		is_open = false
	if FileAccess.file_exists(db.path):
		is_open = db.open_db()
		if not is_open:
			Log.error("Database: Database is not opened")
			return
		Log.info("Database: Database opened at %s" % ProjectSettings.globalize_path(db.path))
		if db.get_error_message() != "" and db.get_error_message() != "not an error":
			Log.warn("Database: Database just opened but already contains an error message: %s" % db.get_error_message())
	else:
		Log.warn("Database: Database file not found at %s" % db.path)


func get_additional_word_list_path() -> String:
	return BASE_PATH.path_join(ADDITIONAL_WORD_LIST_PATH)


func load_additional_word_list() -> String:
	additional_word_list.clear()
	var word_list_path: String = get_additional_word_list_path()
	if FileAccess.file_exists(word_list_path):
		var file: FileAccess = FileAccess.open(word_list_path, FileAccess.READ)
		var error: Error = FileAccess.get_open_error()
		if error != OK:
			Log.error("Database: Load additional word list: Cannot open file %s. Error: %s" % [word_list_path, error_string(error)])
			return ""
		if file == null:
			Log.error("Database: Load additional word list: Cannot open file %s. File is null" % word_list_path)
			return ""
		var title_line: PackedStringArray = file.get_csv_line()
		if (not "ORTHO" in title_line) or (not "PHON" in title_line) or (not "GPMATCH" in title_line):
			var msg: String = "word list should have columns ORTHO, PHON and GPMATCH"
			Log.error("Database: " + msg)
			return msg
		var ortho_index: int = title_line.find("ORTHO")
		while not file.eof_reached():
			var line: PackedStringArray = file.get_csv_line()
			if line.size() != title_line.size():
				return "Formatting error on: %s" % line
			var data: Dictionary = {}
			for index: int in range(line.size()):
				data[title_line[index]] = line[index]
			additional_word_list[line[ortho_index]] = data
		file.close()
		Log.info("Database: Loaded additional word list from %s (%d entries)" % [word_list_path, additional_word_list.size()])
	else:
		Log.warn("Database: Additional word list file not found: %s" % word_list_path)
	return ""


func get_exercise_for_lesson(lesson_nb: int) -> Array[int]:
	var query: String = "Select Exercise1, Exercise2, Exercise3 FROM LessonsExercises
	INNER JOIN Lessons ON Lessons.ID = LessonsExercises.LessonID
	WHERE LessonNB == " + str(lesson_nb)
	db.query(query)
	
	var result: Array[int] = []
	for element: Dictionary in db.query_result:
		result.append(element.Exercise1)
		result.append(element.Exercise2)
		result.append(element.Exercise3)
	
	return result


func get_gps_for_lesson(lesson_nb: int, distinct: bool, only_new: bool = false, only_vowels: bool = false, with_other_phonemes: bool = false, include_exceptions: bool = false) -> Array[Dictionary]:
	
	var parameters: Array = []
	var symbol: String = "<=" if not only_new else "=="
	var query: String = "SELECT g.ID, g.Grapheme, g.Phoneme, "
	
	if with_other_phonemes:
		query += "(SELECT group_concat(Phoneme) FROM GPs g2 
		INNER JOIN GPsInLessons ON GPsInLessons.GPID = g2.ID 
		INNER JOIN Lessons ON Lessons.ID = GPsInLessons.LessonID AND Lessons.LessonNb " + symbol + " ?
		WHERE g.Grapheme = g2.Grapheme AND g.Phoneme != g2.Phoneme)
	AS OtherPhonemes, "
		parameters.append(lesson_nb)
	
	query += "Type, LessonNb FROM GPs g
	INNER JOIN GPsInLessons ON GPsInLessons.GPID = g.ID 
	INNER JOIN Lessons ON Lessons.ID = GPsInLessons.LessonID AND Lessons.LessonNb " + symbol + " ?"
	parameters.append(lesson_nb)
	
	var conditions: Array = []
	if not include_exceptions:
		conditions.append("Exception = 0")
	if only_vowels:
		conditions.append("Type = 1")
	
	if conditions.size() > 0:
		query += " WHERE " + " AND ".join(conditions)
	
	if distinct:
		query += " GROUP BY Grapheme"
	
	query += " ORDER BY LessonNb ASC"
	
	db.query_with_bindings(query, parameters)
	
	var result: Array[Dictionary] = db.query_result
	
	if with_other_phonemes:
		for gp: Dictionary in result:
			if gp.OtherPhonemes:
				var phonemes: String = gp.OtherPhonemes
				gp.OtherPhonemes = phonemes.split(",")
	
	return result


func get_gps_from_syllable(syllable_id: int) -> Array[Dictionary]:
	db.query_with_bindings("SELECT GPs.ID, GPs.Grapheme, GPs.Phoneme, GPs.Type FROM Syllables INNER JOIN GPsInSyllables ON Syllables.ID = GPsInSyllables.SyllableID AND Syllables.ID=? INNER JOIN GPs WHERE GPs.ID = GPsInSyllables.GPID ORDER BY Position", [syllable_id])
	return db.query_result


func get_gp_from_word(id: int) -> Array:
	db.query_with_bindings("SELECT GPs.* FROM Words INNER JOIN GPsInWords ON Words.ID = GPsInWords.WordID AND Words.ID=? INNER JOIN GPs WHERE GPs.ID = GPsInWords.GPID ORDER BY Position", [id])
	return db.query_result


func get_gps_from_sentence(sentence_id: int) -> Array[Dictionary]:
	db.query_with_bindings("SELECT GPs.ID, GPs.Grapheme, GPs.Phoneme, GPs.Type, GPsInWords.WordID, GPsInWords.Position AS GPPosition, WordsInSentences.Position AS WordPosition FROM GPs
INNER JOIN GPsInWords ON GPsInWords.GPID = GPs.ID
INNER JOIN WordsInSentences ON WordsInSentences.WordID = GPsInWords.WordID
INNER JOIN Sentences ON Sentences.ID = WordsInSentences.SentenceID
WHERE Sentences.ID = ?
ORDER BY WordPosition ASC, GPPosition ASC", [sentence_id])
	return db.query_result


func get_words_containing_grapheme(grapheme: String) -> Array[Dictionary]:
	db.query_with_bindings("SELECT Word FROM Words INNER JOIN GPsInWords INNER JOIN GPs on Words.ID = GPsInWords.WordID AND GPs.Grapheme=? AND GPs.ID = GPsInWords.GPID", [grapheme])
	return db.query_result


func get_word_id_from_text(text: String) -> int:
	var clean_text: String = text.replace(".", "").replace(",", "")
	db.query_with_bindings(
		"SELECT ID FROM Words WHERE Word = ? COLLATE NOCASE;",
		[clean_text]
	)
	if db.query_result.size() > 0:
		if db.query_result[0].has("ID"):
			return db.query_result[0].ID
	# If not found, trying again with even cleaner text
	clean_text = clean_text.replace("'", "")
	db.query_with_bindings(
		"SELECT ID FROM Words WHERE Word = ? COLLATE NOCASE;",
		[clean_text]
	)
	if db.query_result.size() > 0:
		if db.query_result[0].has("ID"):
			return db.query_result[0].ID
	Log.trace("Database: Word " + clean_text + " ID not found")
	return -1


func get_syllables_for_lesson(lesson_nb: int, only_new: bool = false) -> Array[Dictionary]:
	var query: String = "SELECT Syllables.ID, Syllables.Syllable as Grapheme, VerifiedCount.LessonNb FROM Syllables
	 INNER JOIN 
	(SELECT SyllableID, count() as Count FROM GPsInSyllables 
		INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInSyllables.GPID 
		GROUP BY SyllableID 
		) TotalCount ON TotalCount.SyllableID = Syllables.ID 
	INNER JOIN 
	(SELECT SyllableID, count() as Count, max(LessonNb) AS LessonNb FROM GPsInSyllables 
		INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInSyllables.GPID 
		INNER JOIN Lessons ON Lessons.ID = GPsInLessons.LessonID AND Lessons.LessonNb <= ?
		GROUP BY SyllableID 
		) VerifiedCount ON VerifiedCount.SyllableID = Syllables.ID AND VerifiedCount.Count = TotalCount.Count"
	if only_new:
		query += " INNER JOIN GPsInSyllables ON GPsInSyllables.SyllableID = Syllables.ID 
			INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInSyllables.GPID 
			INNER JOIN Lessons ON Lessons.ID = GPsInLessons.LessonID AND Lessons.LessonNb = ?"
		db.query_with_bindings(query, [lesson_nb, lesson_nb])
	else:
		db.query_with_bindings(query, [lesson_nb])
	
	var res: Array[Dictionary] = db.query_result
	for syllable: Dictionary in res:
		syllable.GPs = get_gps_from_syllable(syllable.ID as int)
		var phonemes: Array[String] = []
		for gp: Dictionary in syllable.GPs:
			phonemes.append(gp.Phoneme)
		syllable.Phoneme = "-".join(phonemes)
	
	return res


func get_words_for_lesson(lesson_nb: int, only_new: bool = false, min_length: int = 2, max_length: int = 99, include_exception: bool = false) -> Array[Dictionary]:
	var parameters: Array = []
	var query: String = "SELECT DISTINCT Words.ID, Words.Word, Words.Exception, VerifiedCount.Count as GPsCount, MaxLessonNb as LessonNb, VerifiedCount.gpsid as GPs_IDs
FROM Words
	 INNER JOIN 
	(SELECT WordID, count() as Count FROM GPsInWords 
		INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInWords.GPID 
		GROUP BY WordID 
		) TotalCount ON TotalCount.WordID = Words.ID 
	INNER JOIN 
	(SELECT WordID, count() as Count, max(LessonNb) as MaxLessonNb, group_concat(GPsInWords.GPID) as gpsid FROM GPsInWords 
		INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInWords.GPID 
		INNER JOIN Lessons ON Lessons.ID = GPsInLessons.LessonID AND Lessons.LessonNb <= ?
		GROUP BY WordID 
		) VerifiedCount ON VerifiedCount.WordID = Words.ID AND VerifiedCount.Count = TotalCount.Count"
	parameters.append(lesson_nb)
	
	if only_new:
		query += " INNER JOIN GPsInWords ON GPsInWords.WordID = Words.ID 
			INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInWords.GPID 
			INNER JOIN Lessons ON Lessons.ID = GPsInLessons.LessonID AND Lessons.LessonNb = ?"
		parameters.append(lesson_nb)
	
	query += " WHERE TotalCount.Count <= ? and TotalCount.Count >= ?"

	if not include_exception:
		query += " and Words.Exception = false"

	query += " ORDER BY LessonNb, GPsCount ASC"
	parameters.append(max_length)
	parameters.append(min_length)
	
	db.query_with_bindings(query, parameters)
	var res: Array[Dictionary] = db.query_result
	
	# Parse the GPs IDs
	for word: Dictionary in res:
		var word_gps: Array = []
		var word_gps_id: String = word.GPs_IDs
		for gp_id: String in word_gps_id.split(","):
			word_gps.append({ID = int(gp_id)})
		word.GPs = word_gps
		word.erase("GPs_IDs")
	
	return res


func get_words_with_silent_gp_for_lesson(lesson_nb: int, only_new: bool = false, min_length: int = 2, max_length: int = 6) -> Array[Dictionary]:
	var parameters: Array = []
	var query: String = "SELECT Words.ID, Words.Word, MaxLessonNb as LessonNb
FROM Words
	 INNER JOIN 
	(SELECT WordID, count() as Count FROM GPsInWords 
		INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInWords.GPID 
		GROUP BY WordID 
		) TotalCount ON TotalCount.WordID = Words.ID 
	INNER JOIN 
	(SELECT WordID, count() as Count, max(LessonNb) as MaxLessonNb, group_concat(GPs.Phoneme) as gpphoneme FROM GPsInWords 
		INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInWords.GPID 
		INNER JOIN Lessons ON Lessons.ID = GPsInLessons.LessonID AND Lessons.LessonNb <= ?
		INNER JOIN GPs ON GPs.ID = GPsInWords.GPID
		GROUP BY WordID 
		) VerifiedCount ON VerifiedCount.WordID = Words.ID AND VerifiedCount.Count = TotalCount.Count"
	parameters.append(lesson_nb)
	
	if only_new:
		query += " INNER JOIN GPsInWords ON GPsInWords.WordID = Words.ID 
			INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInWords.GPID 
			INNER JOIN Lessons ON Lessons.ID = GPsInLessons.LessonID AND Lessons.LessonNb = ?"
		parameters.append(lesson_nb)
	
	query += " WHERE TotalCount.Count <= ? and TotalCount.Count >= ?
	AND VerifiedCount.gpphoneme LIKE '%#%' 
	ORDER BY LessonNb, VerifiedCount.Count ASC"
	parameters.append(max_length)
	parameters.append(min_length)
	
	db.query_with_bindings(query, parameters)
	return db.query_result


func get_sentences_by_lessons() -> Dictionary:
	var sentences_by_lesson: Dictionary = {}
	
	db.query("SELECT * FROM Sentences")
	var sentences: Array[Dictionary] = db.query_result
	
	for sentence: Dictionary in sentences:
		var lesson_nb: int = get_min_lesson_for_sentence_id(sentence.ID as int)
		var arr: Array = sentences_by_lesson.get(lesson_nb, [])
		arr.append(sentence)
		sentences_by_lesson[lesson_nb] = arr
	return sentences_by_lesson


func get_sentences(p_lesson_nb: int, only_new: bool = false, sentences_by_lesson: Dictionary = {}) -> Array:
	if sentences_by_lesson.is_empty():
		sentences_by_lesson = get_sentences_by_lessons()
	
	if only_new:
		return sentences_by_lesson.get(p_lesson_nb, [])
	
	var ret: Array = []
	for index: int in range(p_lesson_nb):
		ret.append_array(sentences_by_lesson.get(index, []) as Array)
	return ret


func get_sentences_for_lesson(lesson_nb: int, min_length: int = 2, max_length:int = 5) -> Array[Dictionary]:
	var query: String = "SELECT Sentences.*, VerifiedCount.MaxLessonNb AS LessonNb FROM Sentences
INNER JOIN 
	(SELECT SentenceID, count() as WordsCount FROM WordsInSentences 
	GROUP BY SentenceID 
	) WordCount ON WordCount.SentenceID = Sentences.ID
INNER JOIN 
	(SELECT SentenceID, count() as Count FROM GPsInWords 
		INNER JOIN WordsInSentences ON GPsInWords.WordID = WordsInSentences.WordID
		INNER JOIN Sentences ON WordsInSentences.SentenceID = Sentences.ID
		INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInWords.GPID 
	GROUP BY SentenceID 
	) TotalCount ON TotalCount.SentenceID = Sentences.ID 
INNER JOIN 
	(SELECT SentenceID, count() as Count, max(LessonNb) as MaxLessonNb FROM GPsInWords 
		INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInWords.GPID 
		INNER JOIN WordsInSentences ON GPsInWords.WordID = WordsInSentences.WordID
		INNER JOIN Sentences ON WordsInSentences.SentenceID = Sentences.ID
		INNER JOIN Lessons ON Lessons.ID = GPsInLessons.LessonID AND Lessons.LessonNb <= ?
		INNER JOIN GPs ON GPs.ID = GPsInWords.GPID
	GROUP BY SentenceID 
	) VerifiedCount ON VerifiedCount.SentenceID = Sentences.ID AND VerifiedCount.Count = TotalCount.Count
WHERE Sentences.Exception = 0
	AND WordsCount >=? 
	AND WordsCount <=?
	ORDER BY LessonNb"

	db.query_with_bindings(query, [lesson_nb, min_length, max_length])
	return db.query_result


func get_sentences_for_lesson_with_silent_gps(lesson_nb: int, min_length: int = 2, max_length:int = 5) -> Array[Dictionary]:
	var query: String = "SELECT Sentences.*, VerifiedCount.MaxLessonNb AS LessonNb FROM Sentences
INNER JOIN 
	(SELECT SentenceID, count() as WordsCount FROM WordsInSentences 
	GROUP BY SentenceID 
	) WordCount ON WordCount.SentenceID = Sentences.ID
INNER JOIN 
	(SELECT SentenceID, count() as Count FROM GPsInWords 
		INNER JOIN WordsInSentences ON GPsInWords.WordID = WordsInSentences.WordID
		INNER JOIN Sentences ON WordsInSentences.SentenceID = Sentences.ID
		INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInWords.GPID 
	GROUP BY SentenceID 
	) TotalCount ON TotalCount.SentenceID = Sentences.ID 
INNER JOIN 
	(SELECT SentenceID, count() as Count, max(LessonNb) as MaxLessonNb, group_concat(GPs.Phoneme) as SentencePhoneme FROM GPsInWords 
		INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInWords.GPID 
		INNER JOIN WordsInSentences ON GPsInWords.WordID = WordsInSentences.WordID
		INNER JOIN Sentences ON WordsInSentences.SentenceID = Sentences.ID
		INNER JOIN Lessons ON Lessons.ID = GPsInLessons.LessonID AND Lessons.LessonNb <= ?
		INNER JOIN GPs ON GPs.ID = GPsInWords.GPID
	GROUP BY SentenceID 
	) VerifiedCount ON VerifiedCount.SentenceID = Sentences.ID AND VerifiedCount.Count = TotalCount.Count
WHERE Sentences.Exception = 0
	AND WordsCount >=? 
	AND WordsCount <=? 
	AND VerifiedCount.SentencePhoneme LIKE '%#%'"

	db.query_with_bindings(query, [lesson_nb, min_length, max_length])
	return db.query_result


func get_pseudowords_for_lesson(p_lesson_nb: int) -> Array[Dictionary]:
	var query: String = "SELECT * FROM Pseudowords
	 INNER JOIN Words ON Words.ID = Pseudowords.WordID 
	 INNER JOIN 
	(SELECT WordID, count() as Count FROM GPsInWords 
		INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInWords.GPID 
		GROUP BY WordID 
		) TotalCount ON TotalCount.WordID = Words.ID 
	INNER JOIN 
	(SELECT WordID, count() as Count FROM GPsInWords 
		INNER JOIN GPsInLessons ON GPsInLessons.GPID = GPsInWords.GPID 
		INNER JOIN Lessons ON Lessons.ID = GPsInLessons.LessonID AND Lessons.LessonNb <= ? 
		GROUP BY WordID 
		) VerifiedCount ON VerifiedCount.WordID = Words.ID AND VerifiedCount.Count = TotalCount.Count"
	db.query_with_bindings(query, [p_lesson_nb])
	return db.query_result


func get_distractors_for_grapheme(id: int, lesson_nb: int) -> Array[Dictionary]:
	db.query_with_bindings("SELECT DISTINCT distractor.Grapheme, distractor.ID, distractor.Phoneme From GPs stimuli 
	INNER JOIN GPs distractor 
	INNER JOIN Lessons 
	INNER JOIN GPsInLessons
	ON stimuli.ID=? AND distractor.type = stimuli.type 
	AND distractor.exception = false
	AND distractor.Grapheme != stimuli.Grapheme 
	AND distractor.Phoneme != stimuli.Phoneme
	AND CASE WHEN length(distractor.Grapheme) < length(stimuli.Grapheme) 
		THEN stimuli.Grapheme NOT LIKE distractor.Grapheme || '%'
		ELSE true
	END
	AND GPsInLessons.GPID = distractor.ID
	AND Lessons.ID = GPsInLessons.LessonID AND Lessons.LessonNb <= ?", [id, lesson_nb])
	return db.query_result


func get_min_lesson_for_gp_id(gp_id: int) -> int:
	db.query_with_bindings("SELECT Lessons.LessonNb as i FROM Lessons
	INNER JOIN GPsInLessons ON Lessons.ID = GPsInLessons.LessonID AND GPsInLessons.GPID = ?", [gp_id])
	if db.query_result.is_empty():
		return -1
	return db.query_result[0].i


func get_min_lesson_for_word_id(word_id: int) -> int:
	db.query_with_bindings("SELECT * FROM Words 
	INNER JOIN GPsInWords ON GPsInWords.WordID=Words.ID
	WHERE Words.ID=? 
	ORDER BY Position", [word_id])
	var minimum: int = -1
	for result: Dictionary in db.query_result:
		var index: int = Database.get_min_lesson_for_gp_id(result.GPID as int)
		if index < 0:
			minimum = -1
			break
		minimum = maxi(minimum, index)
	return minimum


func get_min_lesson_for_sentence_id(sentence_id: int) -> int:
	db.query_with_bindings("SELECT MAX(Lessons.LessonNb) as i FROM Lessons
	INNER JOIN GPsInLessons ON Lessons.ID = GPsInLessons.LessonID
	INNER JOIN WordsInSentences ON WordsInSentences.SentenceID = ?
	INNER JOIN GPsInWords ON GPsInWords.GPID = GPsInLessons.GPID AND GPsInWords.WordID = WordsInSentences.WordID", [sentence_id])
	if not db.query_result[0].i:
		return -1
	return db.query_result[0].i


func get_words_in_sentence(sentence_id: int) -> Array[Dictionary]:
	db.query_with_bindings("SELECT * FROM WordsInSentences
	INNER JOIN Words ON Words.ID = WordsInSentences.WordID
	WHERE SentenceID = ?
	ORDER BY WordsInSentences.Position", [sentence_id])
	return db.query_result


func get_lessons_count() -> int:
	db.query("SELECT MAX(Lessons.LessonNb) as i FROM Lessons")
	if db.query_result.is_empty() or not db.query_result[0].i:
		return 0
	return db.query_result[0].i


func get_audio_stream_for_path(path: String) -> AudioStream:
	var full_path: String = BASE_PATH.path_join(language).path_join(path)
	if not FileAccess.file_exists(full_path):
		return null
	return load(full_path)


func get_audio_stream_for_word(id: int) -> AudioStream:
	var gps: Array = get_gp_from_word(id)
	var file_name: String = _phoneme_to_string(gps[0].Phoneme as String)
	for index: int in range(1, gps.size()):
		var gp: Dictionary = gps[index]
		file_name += "-" + _phoneme_to_string(gp.Phoneme as String)
	file_name += ".mp3"
	return load(words_path + file_name)


func get_audio_stream_for_phoneme(phoneme: String) -> AudioStream:
	var phoneme_array: PackedStringArray = phoneme.split("-")
	var path: String = words_path + _phoneme_to_string(phoneme_array[0])
	for index: int in range(1, phoneme_array.size()):
		path += "-" + _phoneme_to_string(phoneme_array[index])
	path += ".mp3"
	
	if FileAccess.file_exists(path) and ResourceLoader.exists(path):
		return load(path)
	Log.trace("Database: Audio stream not found for phoneme %s" % phoneme)
	return null


func get_gp_look_and_learn_image(gp: Dictionary) -> Texture:
	var path: String = get_gp_look_and_learn_image_path(gp)
	if FileAccess.file_exists(path):
		if ResourceLoader.exists(path):
			return load(path)
		else:
			var image: Image = Image.load_from_file(path)
			var texture: ImageTexture = ImageTexture.create_from_image(image)
			return texture
	
	Log.trace("Database: Look & Learn image not found for GP %s" % str(gp))
	return null


func get_gp_look_and_learn_sound(gp: Dictionary) -> AudioStream:
	var path: String = get_gp_look_and_learn_sound_path(gp)
	if FileAccess.file_exists(path):
		if ResourceLoader.exists(path):
			return load(path)
		else:
			var file: FileAccess = FileAccess.open(path, FileAccess.READ)
			var error: Error = FileAccess.get_open_error()
			if error != OK:
				Log.error("Database: Get GP look and learn sound: Cannot open file %s. Error: %s" % [path, error_string(error)])
				return null
			if file == null:
				Log.error("Database: Get GP look and learn sound: Cannot open file %s. File is null" % path)
				return null
			var sound: AudioStreamMP3 = AudioStreamMP3.new()
			sound.data = file.get_buffer(file.get_length())
			return sound
	
	Log.trace("Database: Look & Learn sound not found for GP %s" % str(gp))
	return null


func get_gp_look_and_learn_video(gp: Dictionary) -> VideoStream:
	var path: String = get_gp_look_and_learn_video_path(gp)
	if FileAccess.file_exists(path):
		var video: VideoStream = load(path)
		return video
	
	Log.trace("Database: Look & Learn video not found for GP %s" % gp)
	return null


func get_gp_name(gp: Dictionary) -> String:
	var result: String = ""
	if gp.has("Grapheme"):
		result += gp["Grapheme"]
	if gp.has("Phoneme"):
		if result != "":
			result += "-"
		result += gp["Phoneme"]
	return result


func get_language_folder() -> String:
	return BASE_PATH + language


func get_language_sound_folder() -> String:
	return get_language_folder() + LANGUAGE_SOUNDS


func get_gp_look_and_learn_image_path(gp: Dictionary) -> String:
	return get_language_folder() + LOOK_AND_LEARN_IMAGES + get_gp_name(gp) + IMAGE_EXTENSION


func get_gp_look_and_learn_sound_path(gp: Dictionary) -> String:
	return get_language_folder() + LOOK_AND_LEARN_SOUNDS + get_gp_name(gp) + SOUND_EXTENSION


func get_gp_look_and_learn_video_path(gp: Dictionary) -> String:
	return get_language_folder() + LOOK_AND_LEARN_VIDEOS + get_gp_name(gp) + VIDEO_EXTENSION


func get_gp_sound_path(gp: Dictionary) -> String:
	return get_language_sound_folder() + Database.get_gp_name(gp) + SOUND_EXTENSION


func get_syllable_sound_path(syllable: Dictionary) -> String:
	return get_language_sound_folder() + syllable.Grapheme + SOUND_EXTENSION


func get_word_sound_path(word: Dictionary) -> String:
	return get_language_sound_folder() + word.Word + SOUND_EXTENSION


func get_kalulu_speech_path(speech_category: String, speech_name: String) -> String:
	return get_language_sound_folder() + KALULU_FOLDER + speech_category + "_" + speech_name + SOUND_EXTENSION


func load_external_sound(path: String) -> AudioStreamMP3:
	Log.trace("Database: Load External Sound: %s" % path)
	
	path = Utils.get_safe_file_path(path)
	
	if not FileAccess.file_exists(path):
		path = UnicodeNormalizer.to_nfd_basic(path)
		Log.trace("Database: Load External Sound: File not found, retrying with Unicode NFD: %s" % path)
		if not FileAccess.file_exists(path):
			path = UnicodeNormalizer.to_nfd_extended(path)
			Log.trace("Database: Load External Sound: File not found, retrying with Unicode NFD extended: %s" % path)
			if not FileAccess.file_exists(path):
				Log.error("Database: Load External Sound: File not found after normalization attempts: %s" % path)
				return null
	
	var file: FileAccess = FileAccess.open(path, FileAccess.READ)
	var error: Error = FileAccess.get_open_error()
	if error != OK or file == null:
		Log.error("Database: Load External Sound: Cannot open file %s. Error: %s" % [path, error_string(error)])
		return null
	
	var file_length: int = file.get_length()
	if file_length <= 0:
		Log.error("Database: Load External Sound: Invalid file length for %s" % path)
		file.close()
		return null
	
	var data: PackedByteArray = file.get_buffer(file_length)
	file.close()
	
	if data.is_empty():
		Log.error("Database: Load External Sound: Empty buffer read for %s" % path)
		return null
	
	var audio_stream: AudioStreamMP3 = AudioStreamMP3.new()
	audio_stream.data = data
	
	if not audio_stream or audio_stream == null:
		Log.error("Database: Load External Sound: Failed to create AudioStreamMP3 for %s" % path)
		return null
	
	Log.trace("Database: Load External Sound: Loaded %s successfully (%d bytes)" % [path, file_length])
	return audio_stream


func _phoneme_to_string(phoneme: String) -> String:
	if _SYMBOLS_TO_STRING.has(phoneme):
		return _SYMBOLS_TO_STRING[phoneme]
	elif phoneme == phoneme.to_lower():
		return phoneme
	else:
		return "cap." + phoneme.to_lower()


# Returns an array containing an int followed by an array of int
func _import_word_from_csv(ortho: String, gpmatch: String, is_word: bool = true) -> Array[Variant]:
	var gp_list: PackedStringArray = gpmatch.trim_prefix("(").trim_suffix(")").split(".")
	var gp_ids: Array[int] = []
	for gp: String in gp_list:
		var split: PackedStringArray = gp.split("-")
		var grapheme: String = split[0]
		var phoneme: String = split[1]
		db.query_with_bindings("SELECT * FROM GPs WHERE Grapheme = ? AND Phoneme = ?", [grapheme, phoneme])
		var gp_id: int = -1
		if db.query_result.is_empty():
			db.insert_row("GPs", {
				Grapheme = grapheme,
				Phoneme = phoneme,
			})
			gp_id = db.last_insert_rowid
		else:
			gp_id = db.query_result[0].ID
		gp_ids.append(gp_id)
	if is_word:
		db.query_with_bindings("SELECT * FROM Words WHERE Word = ?", [ortho])
	else:
		db.query_with_bindings("SELECT * FROM Syllables WHERE Syllable = ?", [ortho])
	var word_id: int = -1
	if db.query_result.is_empty():
		if is_word:
			db.insert_row("Words", {
				Word = ortho,
			})
			word_id = db.last_insert_rowid
			for index: int in range(gp_ids.size()):
				var gp_id: int = gp_ids[index]
				db.insert_row("GPsInWords", {
					WordID = word_id,
					GPID = gp_id,
					Position = index
				})
		else:
			db.insert_row("Syllables", {
				Syllable = ortho,
			})
			word_id = db.last_insert_rowid
			for index: int in range(gp_ids.size()):
				var gp_id: int = gp_ids[index]
				db.insert_row("GPsInSyllables", {
					SyllableID = word_id,
					GPID = gp_id,
					Position = index
				})
	else:
		word_id = db.query_result[0].ID
	return [word_id, gp_ids]
