tool
extends Control

var last_mouse_mode = null
var input_next: String = 'ui_accept'
var dialog_index: int = 0
var finished: bool = false
var waiting_for_answer: bool = false
var waiting_for_input: bool = false
var waiting: bool = false
var preview: bool = false
var definitions: Dictionary = {}
var definition_visible: bool = false

var settings: ConfigFile
var current_theme: ConfigFile
var current_timeline: String = ''
var current_event: Dictionary

## The timeline to load when starting the scene
export(String, "TimelineDropdown") var timeline: String
## Should we clear saved data (definitions and timeline progress) on start?
export(bool) var reset_saves = true
## Should we show debug information when running?
export(bool) var debug_mode = true

# Event end/start
signal event_start(type, event)
signal event_end(type)
# Timeline end/start
signal timeline_start(timeline_name)
signal timeline_end(timeline_name)
# Custom user signal
signal dialogic_signal(value)

var dialog_resource
var characters

onready var ChoiceButton = load("res://addons/dialogic/Nodes/ChoiceButton.tscn")
onready var Portrait = load("res://addons/dialogic/Nodes/Portrait.tscn")
var dialog_script: Dictionary = {}
var questions #for keeping track of the questions answered


func _ready():
	# Loading the config files
	load_config_files()
	
	# Checking if the dialog should read the code from a external file
	if not timeline.empty():
		dialog_script = set_current_dialog(timeline)
	elif dialog_script.keys().size() == 0:
		dialog_script = {
			"events":[{"character":"","portrait":"",
			"text":"[Dialogic Error] No timeline specified."}]
		}
	
	# Connecting resize signal
	get_viewport().connect("size_changed", self, "resize_main")
	resize_main()

	# Setting everything up for the node to be default
	$DefinitionInfo.visible = false
	$TextBubble.connect("text_completed", self, "_on_text_completed")
	$TextBubble/RichTextLabel.connect('meta_hover_started', self, '_on_RichTextLabel_meta_hover_started')
	$TextBubble/RichTextLabel.connect('meta_hover_ended', self, '_on_RichTextLabel_meta_hover_ended')

	# Getting the character information
	characters = DialogicUtil.get_character_list()

	if Engine.is_editor_hint():
		if preview:
			_init_dialog()
	else:
		_init_dialog()


func load_config_files():
	if not Engine.is_editor_hint():
		if reset_saves:
			DialogicSingleton.init(reset_saves)
		definitions = DialogicSingleton.get_definitions()
	else:
		definitions = DialogicResources.get_default_definitions()
	settings = DialogicResources.get_settings_config()
	var theme_file = 'res://addons/dialogic/Editor/ThemeEditor/default-theme.cfg'
	if settings.has_section('theme'):
		theme_file = settings.get_value('theme', 'default')
	current_theme = load_theme(theme_file)


func resize_main():
	# This function makes sure that the dialog is displayed at the correct
	# size and position in the screen.
	var reference = rect_size
	if not Engine.is_editor_hint():
		set_global_position(Vector2(0,0))
		reference = get_viewport().get_visible_rect().size

	$Options.rect_position.x = (reference.x / 2) - ($Options.rect_size.x / 2)
	$Options.rect_position.y = (reference.y / 2) - ($Options.rect_size.y / 2)
	
	$TextBubble.rect_position.x = (reference.x / 2) - ($TextBubble.rect_size.x / 2)
	if current_theme != null:
		$TextBubble.rect_position.y = (reference.y) - ($TextBubble.rect_size.y) - current_theme.get_value('box', 'bottom_gap', 40)
	
	
	var background = get_node_or_null('Background')
	if background != null:
		background.rect_size = reference
	
	var portraits = get_node_or_null('Portraits')
	if portraits != null:
		portraits.rect_position.x = reference.x / 2
		portraits.rect_position.y = reference.y


func set_current_dialog(dialog_path: String):
	current_timeline = dialog_path
	var dialog_script = DialogicResources.get_timeline_json(dialog_path)
	# All this parse events should be happening in the same loop ideally
	# But until performance is not an issue I will probably stay lazy
	# And keep adding different functions for each parsing operation.
	if settings.has_section_key('dialog', 'auto_color_names'):
		if settings.get_value('dialog', 'auto_color_names'):
			dialog_script = parse_characters(dialog_script)
	else:
		dialog_script = parse_characters(dialog_script)
	
	dialog_script = parse_text_lines(dialog_script)
	dialog_script = parse_branches(dialog_script)
	return dialog_script


func parse_characters(dialog_script):
	var names = DialogicUtil.get_character_list()
	# I should use regex here, but this is way easier :)
	if names.size() > 0:
		var index = 0
		for t in dialog_script['events']:
			if t.has('text'):
				for n in names:
					if n.has('name'):
						dialog_script['events'][index]['text'] = t['text'].replace(n['name'],
							'[color=#' + n['color'].to_html() + ']' + n['name'] + '[/color]'
						)
			index += 1
	return dialog_script


func parse_text_lines(unparsed_dialog_script: Dictionary) -> Dictionary:
	var parsed_dialog: Dictionary = unparsed_dialog_script
	var new_events: Array = []
	var split_new_lines = true
	var remove_empty_messages = true

	# Return the same thing if it doesn't have events
	if unparsed_dialog_script.has('events') == false:
		return unparsed_dialog_script

	# Getting extra settings
	if settings.has_section_key('dialog', 'remove_empty_messages'):
		remove_empty_messages = settings.get_value('dialog', 'remove_empty_messages')
	if settings.has_section_key('dialog', 'new_lines'):
		split_new_lines = settings.get_value('dialog', 'new_lines')

	# Parsing
	for event in unparsed_dialog_script['events']:
		if event.has('text') and event.has('character') and event.has('portrait'):
			if event['text'].empty() and remove_empty_messages == true:
				pass
			elif '\n' in event['text'] and preview == false and split_new_lines == true:
				var lines = event['text'].split('\n')
				for line in lines:
					if not line.empty():
						new_events.append({
							'text': line,
							'character': event['character'],
							'portrait': event['portrait']
						})
			else:
				new_events.append(event)
		else:
			new_events.append(event)

	parsed_dialog['events'] = new_events

	return parsed_dialog


func parse_alignment(text):
	var alignment = current_theme.get_value('text', 'alignment', 'Left')
	var fname = current_theme.get_value('settings', 'name', 'none')
	if alignment == 'Center':
		text = '[center]' + text + '[/center]'
	elif alignment == 'Right':
		text = '[right]' + text + '[/right]'
	return text


func parse_branches(dialog_script: Dictionary) -> Dictionary:
	questions = [] # Resetting the questions

	# Return the same thing if it doesn't have events
	if dialog_script.has('events') == false:
		return dialog_script

	var parser_queue = [] # This saves the last question opened, and it gets removed once it was consumed by a endbranch event
	var event_id: int = 0 # The current id for jumping later on
	var question_id: int = 0 # identifying the questions to assign options to it
	for event in dialog_script['events']:
		if event.has('choice'):
			var opened_branch = parser_queue.back()
			var option = {
				'question_id': opened_branch['question_id'],
				'label': event['choice'],
				'event_id': event_id,
				}
			if event.has('condition') and event.has('definition') and event.has('value'):
				option = {
					'question_id': opened_branch['question_id'],
					'label': event['choice'],
					'event_id': event_id,
					'condition': event['condition'],
					'definition': event['definition'],
					'value': event['value'],
					}
			else:
				option = {
					'question_id': opened_branch['question_id'],
					'label': event['choice'],
					'event_id': event_id,
					'condition': '',
					'definition': '',
					'value': '',
					}
			dialog_script['events'][opened_branch['event_id']]['options'].append(option)
			event['question_id'] = opened_branch['question_id']
		elif event.has('question'):
			event['event_id'] = event_id
			event['question_id'] = question_id
			event['answered'] = false
			question_id += 1
			questions.append(event)
			parser_queue.append(event)
		elif event.has('condition'):
			event['event_id'] = event_id
			event['question_id'] = question_id
			event['answered'] = false
			question_id += 1
			questions.append(event)
			parser_queue.append(event)
		elif event.has('endbranch'):
			event['event_id'] = event_id
			var opened_branch = parser_queue.pop_back()
			event['end_branch_of'] = opened_branch['question_id']
			dialog_script['events'][opened_branch['event_id']]['end_id'] = event_id
		event_id += 1

	return dialog_script


func _should_show_glossary():
	if current_theme != null:
		return current_theme.get_value('definitions', 'show_glossary', true)
	return true


func parse_definitions(text: String, variables: bool = true, glossary: bool = true):
	var final_text: String = text
	if variables:
		final_text = _insert_variable_definitions(text)
	if glossary and _should_show_glossary():
		final_text = _insert_glossary_definitions(final_text)
	return final_text


func _insert_variable_definitions(text: String):
	var final_text := text;
	for d in definitions['variables']:
		var name : String = d['name'];
		final_text = final_text.replace('[' + name + ']', d['value'])
	return final_text;
	
	
func _insert_glossary_definitions(text: String):
	var color = current_theme.get_value('definitions', 'color', '#ffbebebe')
	var final_text := text;
	# I should use regex here, but this is way easier :)
	for d in definitions['glossary']:
		final_text = final_text.replace(d['name'],
			'[url=' + d['id'] + ']' +
			'[color=' + color + ']' + d['name'] + '[/color]' +
			'[/url]'
		)
	return final_text;


func _process(delta):
	$TextBubble/NextIndicatorContainer/NextIndicator.visible = finished
	if waiting_for_answer and Input.is_action_just_released(input_next):
		if $Options.get_child_count() > 0:
			$Options.get_child(0).grab_focus()


func _input(event: InputEvent) -> void:
	if not Engine.is_editor_hint() and event.is_action_pressed(input_next) and not waiting:
		if not $TextBubble.is_finished():
			# Skip to end if key is pressed during the text animation
			$TextBubble.skip()
		else:
			if waiting_for_answer == false and waiting_for_input == false:
				_load_next_event()
		if settings.has_section_key('dialog', 'propagate_input'):
			var propagate_input: bool = settings.get_value('dialog', 'propagate_input')
			if not propagate_input:
				get_tree().set_input_as_handled()


func show_dialog():
	visible = true


func update_name(character) -> void:
	if character.has('name'):
		var parsed_name = character['name']
		var color = Color.white
		if character.has('display_name'):
			if character['display_name'] != '':
				parsed_name = character['display_name']
		if character.has('color'):
			color = character['color']
		parsed_name = parse_definitions(parsed_name, true, false)
		$TextBubble.update_name(parsed_name, color, current_theme.get_value('name', 'auto_color', true))
	else:
		$TextBubble.update_name('')


func update_text(text: String) -> String:
	var final_text = parse_definitions(parse_alignment(text))
	final_text = final_text.replace('[br]', '\n')
	$TextBubble.update_text(final_text)
	return final_text


func _on_text_completed():
	finished = true
	if current_event.has('options'):
		for o in current_event['options']:
			add_choice_button(o)


func on_timeline_start():
	if not Engine.is_editor_hint():
		DialogicSingleton.save_definitions()
		DialogicSingleton.set_current_timeline(current_timeline)
	# TODO remove event_start in 2.0
	emit_signal("event_start", "timeline", current_timeline)
	emit_signal("timeline_start", current_timeline)


func on_timeline_end():
	if not Engine.is_editor_hint():
		DialogicSingleton.save_definitions()
		DialogicSingleton.set_current_timeline('')
	# TODO remove event_end in 2.0
	emit_signal("event_end", "timeline")
	emit_signal("timeline_end", current_timeline)
	dprint('[D] Timeline End')


func _init_dialog():
	dialog_index = 0
	_load_event()


func _load_event_at_index(index: int):
	dialog_index = index
	_load_event()


func _load_next_event():
	dialog_index += 1
	_load_event()


func _is_dialog_starting():
	return dialog_index == 0


func _is_dialog_finished():
	return dialog_index >= dialog_script['events'].size()


func _load_event():
	_emit_timeline_signals()
	_hide_definition_popup()
	
	if dialog_script.has('events'):
		if not _is_dialog_finished():
			var func_state = event_handler(dialog_script['events'][dialog_index])
			if (func_state is GDScriptFunctionState):
				yield(func_state, "completed")
		elif not Engine.is_editor_hint():
			# Do not free the dialog if we are in the preview
			queue_free()


func _emit_timeline_signals():
	if dialog_script.has('events'):
		if _is_dialog_starting():
			on_timeline_start()
		elif _is_dialog_finished():
			on_timeline_end()


func _hide_definition_popup():
	definition_visible = false
	$DefinitionInfo.visible = definition_visible


func get_character(character_id):
	for c in characters:
		if c['file'] == character_id:
			return c
	return {}


func event_handler(event: Dictionary):
	# Handling an event and updating the available nodes accordingly.
	$TextBubble.reset()
	reset_options()
	
	dprint('[D] Current Event: ', event)
	current_event = event
	match event:
		{'text', 'character', 'portrait'}:
			emit_signal("event_start", "text", event)
			show_dialog()
			finished = false
			var character_data = get_character(event['character'])
			update_name(character_data)
			grab_portrait_focus(character_data, event)
			update_text(event['text'])
		{'question', 'question_id', 'options', ..}:
			emit_signal("event_start", "question", event)
			show_dialog()
			finished = false
			waiting_for_answer = true
			if event.has('name'):
				update_name(event['name'])
			elif event.has('character'):
				var character_data = get_character(event['character'])
				update_name(character_data)
				grab_portrait_focus(character_data, event)
			update_text(event['question'])
		{'choice', 'question_id'}:
			emit_signal("event_start", "choice", event)
			for q in questions:
				if q['question_id'] == event['question_id']:
					if q['answered']:
						# If the option is for an answered question, skip to the end of it.
						_load_event_at_index(q['end_id'])
		{'action', ..}:
			emit_signal("event_start", "action", event)
			if event['action'] == 'leaveall':
				if event['character'] == '[All]':
					characters_leave_all()
				else:
					for p in $Portraits.get_children():
						if p.character_data['file'] == event['character']:
							p.fade_out()
				_load_next_event()
			elif event['action'] == 'join':
				if event['character'] == '':
					_load_next_event()
				else:
					var character_data = get_character(event['character'])
					var exists = grab_portrait_focus(character_data)
					if exists == false:
						var p = Portrait.instance()
						var char_portrait = event['portrait']
						if char_portrait == '':
							char_portrait = 'Default'
						p.character_data = character_data
						p.init(char_portrait, get_character_position(event['position']), event.get('mirror', false))
						$Portraits.add_child(p)
						p.fade_in()
				_load_next_event()
		{'scene'}:
			get_tree().change_scene(event['scene'])
		{'background'}:
			emit_signal("event_start", "background", event)
			var background = get_node_or_null('Background')
			if event['background'] == '' and background != null:
				background.queue_free()
			else:
				if background == null:
					background = TextureRect.new()
					background.expand = true
					background.name = 'Background'
					background.anchor_right = 1
					background.anchor_bottom = 1
					background.stretch_mode = TextureRect.STRETCH_SCALE
					background.show_behind_parent = true
					background.mouse_filter = Control.MOUSE_FILTER_IGNORE
					call_deferred('resize_main') # Executing the resize main to update the background size
					
					add_child(background)
				background.texture = null
				if (background.get_child_count() > 0):
					for c in background.get_children():
						c.get_parent().remove_child(c)
						c.queue_free()
				if (event['background'].ends_with('.tscn')):
					var bg_scene = load(event['background'])
					if (bg_scene):
						bg_scene = bg_scene.instance()
						background.add_child(bg_scene)
				elif (event['background'] != ''):
					background.texture = load(event['background'])
			_load_next_event()
		{'audio'}, {'audio', 'file', ..}:
			emit_signal("event_start", "audio", event)
			if event['audio'] == 'play' and 'file' in event.keys() and not event['file'].empty():
				var audio = get_node_or_null('AudioEvent')
				if audio == null:
					audio = AudioStreamPlayer.new()
					audio.name = 'AudioEvent'
					add_child(audio)
				if event.has('audio_bus'):
					if AudioServer.get_bus_index(event['audio_bus']) >= 0:
						audio.bus = event['audio_bus']
				if event.has('volume'):
					audio.volume_db = event['volume']
				audio.stream = load(event['file'])
				audio.play()
			else:
				var audio = get_node_or_null('AudioEvent')
				if audio != null:
					audio.stop()
					audio.queue_free()
			_load_next_event()
		{'background-music'}, {'background-music', 'file',..}:
			emit_signal("event_start", "background-music", event)
			if event['background-music'] == 'play' and 'file' in event.keys() and not event['file'].empty():
				$FX/BackgroundMusic.crossfade_to(event['file'], event.get('audio_bus', 'Master'), event.get('volume', 0), event.get('fade_length', 1))
			else:
				$FX/BackgroundMusic.fade_out(event.get('fade_length', 1))
			_load_next_event()
		{'endbranch', ..}:
			emit_signal("event_start", "endbranch", event)
			_load_next_event()
		{'change_scene'}:
			get_tree().change_scene(event['change_scene'])
		{'emit_signal', ..}:
			dprint('[!] Emitting signal: dialogic_signal(', event['emit_signal'], ')')
			emit_signal("dialogic_signal", event['emit_signal'])
			_load_next_event()
		{'close_dialog', ..}:
			emit_signal("event_start", "close_dialog", event)
			var transition_duration = 1.0
			if event.has('transition_duration'):
				transition_duration = event['transition_duration']
			close_dialog_event(transition_duration)
		{'set_theme'}:
			emit_signal("event_start", "set_theme", event)
			if event['set_theme'] != '':
				current_theme = load_theme(event['set_theme'])
			_load_next_event()
		{'wait_seconds'}:
			emit_signal("event_start", "wait", event)
			wait_seconds(event['wait_seconds'])
			waiting = true
		{'change_timeline'}:
			dialog_script = set_current_dialog(event['change_timeline'])
			_init_dialog()
		{'condition', 'definition', 'value', 'question_id', ..}:
			# Treating this conditional as an option on a regular question event
			var def_value = null
			var current_question = questions[event['question_id']]
			
			for d in definitions['variables']:
				if d['id'] == event['definition']:
					def_value = d['value']
			
			var condition_met = def_value != null and _compare_definitions(def_value, event['value'], event['condition']);
			
			current_question['answered'] = !condition_met
			if !condition_met:
				# condition not met, skipping branch
				_load_event_at_index(current_question['end_id'])
			else:
				# condition met, entering branch
				_load_next_event()
		{'set_value', 'definition', ..}:
			emit_signal("event_start", "set_value", event)
			var operation = '='
			if 'operation' in event and not event['operation'].empty():
				operation = event["operation"]
			DialogicSingleton.set_variable_from_id(event['definition'], event['set_value'], operation)
			_load_next_event()
		{'call_node', ..}:
			dprint('[!] Call Node signal: dialogic_signal(call_node) ', var2str(event['call_node']))
			emit_signal("event_start", "call_node", event)
			$TextBubble.visible = false
			waiting = true
			var target = get_node_or_null(event['call_node']['target_node_path'])
			var method_name = event['call_node']['method_name']
			var args = event['call_node']['arguments']
			if (not args is Array):
				args = []

			if (target != null):
				if (target.has_method(method_name)):
					if (args.empty()):
						var func_result = target.call(method_name)
						if (func_result is GDScriptFunctionState):
							yield(func_result, "completed")
					else:
						var func_result = target.call(method_name, args)
						if (func_result is GDScriptFunctionState):
							yield(func_result, "completed")

			waiting = false
			$TextBubble.visible = true
			_load_next_event()
		_:
			visible = false
			dprint('[D] Other event. ', event)
	
	$Options.visible = waiting_for_answer


func reset_options():
	# Clearing out the options after one was selected.
	for option in $Options.get_children():
		option.queue_free()


func _should_add_choice_button(option: Dictionary):
	if not option['condition'].empty() and not option['definition'].empty() and not option['value'].empty():
		var def_value = null
		for d in definitions['variables']:
			if d['id'] == option['definition']:
				def_value = d['value']
		return def_value != null and _compare_definitions(def_value, option['value'], option['condition']);
	else:
		return true


func use_custom_choice_button():
	return current_theme.get_value('buttons', 'use_custom', false) and not current_theme.get_value('buttons', 'custom_path', "").empty()

func use_native_choice_button():
	return current_theme.get_value('buttons', 'use_native', false)



func get_custom_choice_button(label: String):
	var theme = current_theme
	var custom_path = current_theme.get_value('buttons', 'custom_path', "")
	var CustomChoiceButton = load(custom_path)
	var button = CustomChoiceButton.instance()
	button.text = label
	return button


func get_classic_choice_button(label: String):
	var theme = current_theme
	var button : Button = ChoiceButton.instance()
	button.text = label
	# Text
	button.set('custom_fonts/font', DialogicUtil.path_fixer_load(theme.get_value('text', 'font', "res://addons/dialogic/Example Assets/Fonts/DefaultFont.tres")))

	if not use_native_choice_button():
		var text_color = Color(theme.get_value('text', 'color', "#ffffffff"))
		button.set('custom_colors/font_color', text_color)
		button.set('custom_colors/font_color_hover', text_color)
		button.set('custom_colors/font_color_pressed', text_color)
		
		if theme.get_value('buttons', 'fixed', false):
			var size = theme.get_value('buttons', 'fixed_size', Vector2(130,40))
			button.rect_min_size = size
			button.rect_size = size

		if theme.get_value('buttons', 'text_color_enabled', true):
			var button_text_color = Color(theme.get_value('buttons', 'text_color', "#ffffffff"))
			button.set('custom_colors/font_color', button_text_color)
			button.set('custom_colors/font_color_hover', button_text_color)
			button.set('custom_colors/font_color_pressed', button_text_color)

		# Background
		button.get_node('ColorRect').color = Color(theme.get_value('buttons', 'background_color', '#ff000000'))
		button.get_node('ColorRect').visible = theme.get_value('buttons', 'use_background_color', false)

		button.get_node('TextureRect').visible = theme.get_value('buttons', 'use_image', true)
		if theme.get_value('buttons', 'use_image', true):
			button.get_node('TextureRect').texture = DialogicUtil.path_fixer_load(theme.get_value('buttons', 'image', "res://addons/dialogic/Example Assets/backgrounds/background-2.png"))
			if theme.get_value('buttons', 'modulation', false):
				button.get_node('TextureRect').modulate = Color(theme.get_value('buttons', 'modulation_color', "#ffffffff"))

		var padding = theme.get_value('buttons', 'padding', Vector2(5,5))
		button.get_node('ColorRect').set('margin_left', -1 * padding.x)
		button.get_node('ColorRect').set('margin_right',  padding.x)
		button.get_node('ColorRect').set('margin_top', -1 * padding.y)
		button.get_node('ColorRect').set('margin_bottom', padding.y)

		button.get_node('TextureRect').set('margin_left', -1 * padding.x)
		button.get_node('TextureRect').set('margin_right',  padding.x)
		button.get_node('TextureRect').set('margin_top', -1 * padding.y)
		button.get_node('TextureRect').set('margin_bottom', padding.y)
		
		$Options.set('custom_constants/separation', theme.get_value('buttons', 'gap', 20) + (padding.y*2))
	else:
		button.get_node('ColorRect').visible = false
		button.get_node('TextureRect').visible = false
		button.set_flat(false)
	return button


func add_choice_button(option: Dictionary):
	if not _should_add_choice_button(option):
		return
	
	var button
	print(use_custom_choice_button())
	if use_custom_choice_button():
		button = get_custom_choice_button(option['label'])
	else:
		button = get_classic_choice_button(option['label'])
	button.connect("pressed", self, "answer_question", [button, option['event_id'], option['question_id']])

	if use_native_choice_button() or use_custom_choice_button():
		$Options.set('custom_constants/separation', current_theme.get_value('buttons', 'gap', 20))
	$Options.add_child(button)

	if Input.get_mouse_mode() != Input.MOUSE_MODE_VISIBLE:
		last_mouse_mode = Input.get_mouse_mode()
		Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) # Make sure the cursor is visible for the options selection


func answer_question(i, event_id, question_id):
	dprint('[!] Going to ', event_id + 1, i, 'question_id:', question_id)
	waiting_for_answer = false
	questions[question_id]['answered'] = true
	reset_options()
	_load_event_at_index(event_id + 1)
	if last_mouse_mode != null:
		Input.set_mouse_mode(last_mouse_mode) # Revert to last mouse mode when selection is done
		last_mouse_mode = null


func _on_option_selected(option, variable, value):
	dialog_resource.custom_variables[variable] = value
	waiting_for_answer = false
	reset_options()
	_load_next_event()
	dprint('[!] Option selected: ', option.text, ' value= ' , value)


func grab_portrait_focus(character_data, event: Dictionary = {}) -> bool:
	var exists = false
	var visually_focus = true
	if settings.has_section_key('dialog', 'dim_characters'):
		visually_focus = settings.get_value('dialog', 'dim_characters')

	for portrait in $Portraits.get_children():
		if portrait.character_data == character_data:
			exists = true
			
			if visually_focus:
				portrait.focus()
			if event.has('portrait'):
				if event['portrait'] != '':
					portrait.set_portrait(event['portrait'])
		else:
			if visually_focus:
				portrait.focusout()
	return exists


func get_character_position(positions) -> String:
	if positions['0']:
		return 'left'
	if positions['1']:
		return 'center_left'
	if positions['2']:
		return 'center'
	if positions['3']:
		return 'center_right'
	if positions['4']:
		return 'right'
	return 'left'


func deferred_resize(current_size, result):
	#var result = theme.get_value('box', 'size', Vector2(910, 167))
	$TextBubble.rect_size = result
	if current_size != $TextBubble.rect_size:
		resize_main()


func load_theme(filename):
	var theme = DialogicResources.get_theme_config(filename)

	# Box size
	call_deferred('deferred_resize', $TextBubble.rect_size, theme.get_value('box', 'size', Vector2(910, 167)))

	input_next = theme.get_value('settings', 'action_key', 'ui_accept')

	# Definitions
	var definitions_font = DialogicUtil.path_fixer_load(theme.get_value('definitions', 'font', "res://addons/dialogic/Example Assets/Fonts/GlossaryFont.tres"))
	$DefinitionInfo/VBoxContainer/Title.set('custom_fonts/normal_font', definitions_font)
	$DefinitionInfo/VBoxContainer/Content.set('custom_fonts/normal_font', definitions_font)
	$DefinitionInfo/VBoxContainer/Extra.set('custom_fonts/normal_font', definitions_font)
	
	$TextBubble.load_theme(theme)
	
	return theme


func _on_RichTextLabel_meta_hover_started(meta):
	var correct_type = false
	for d in definitions['glossary']:
		if d['id'] == meta:
			$DefinitionInfo.load_preview({
				'title': d['title'],
				'body': d['text'],
				'extra': d['extra'],
				'color': current_theme.get_value('definitions', 'color', '#ffbebebe'),
			})
			correct_type = true
			dprint('[D] Hovered over glossary entry: ', d)

	if correct_type:
		definition_visible = true
		$DefinitionInfo.visible = definition_visible
		# Adding a timer to avoid a graphical glitch
		$DefinitionInfo/Timer.stop()


func _on_RichTextLabel_meta_hover_ended(meta):
	# Adding a timer to avoid a graphical glitch
	$DefinitionInfo/Timer.start(0.1)


func _on_Definition_Timer_timeout():
	# Adding a timer to avoid a graphical glitch
	definition_visible = false
	$DefinitionInfo.visible = definition_visible


func wait_seconds(seconds):
	var timer = Timer.new()
	add_child(timer)
	timer.connect("timeout", self, '_on_WaitSeconds_timeout', [timer])
	timer.start(seconds)
	$TextBubble.visible = false


func _on_WaitSeconds_timeout(timer):
	emit_signal("event_end", "wait")
	waiting = false
	timer.stop()
	timer.queue_free()
	$TextBubble.visible = true
	_load_next_event()


func dprint(string, arg1='', arg2='', arg3='', arg4='' ):
	# HAHAHA if you are here wondering what this is...
	# I ask myself the same question :')
	if debug_mode:
		print(str(string) + str(arg1) + str(arg2) + str(arg3) + str(arg4))


func _compare_definitions(def_value: String, event_value: String, condition: String):
	var condition_met = false;
	if def_value != null and event_value != null:
		# check if event_value equals a definition name and use that instead
		for d in definitions['variables']:
			if (d['name'] != '' and d['name'] == event_value):
				event_value = d['value']
				break;
		var converted_def_value = def_value
		var converted_event_value = event_value
		if def_value.is_valid_float() and event_value.is_valid_float():
			converted_def_value = float(def_value)
			converted_event_value = float(event_value)
		match condition:
			"==":
				condition_met = converted_def_value == converted_event_value
			"!=":
				condition_met = converted_def_value != converted_event_value
			">":
				condition_met = converted_def_value > converted_event_value
			">=":
				condition_met = converted_def_value >= converted_event_value
			"<":
				condition_met = converted_def_value < converted_event_value
			"<=":
				condition_met = converted_def_value <= converted_event_value
	return condition_met


func characters_leave_all():
	var portraits = get_node_or_null('Portraits')
	if portraits != null:
		for p in portraits.get_children():
			p.fade_out()


func close_dialog_event(transition_duration):
	characters_leave_all()
	if transition_duration == 0:
		_on_close_dialog_timeout()
	else:
		var tween = Tween.new()
		add_child(tween)
		tween.interpolate_property($TextBubble, "modulate",
			$TextBubble.modulate, Color('#00ffffff'), transition_duration,
			Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
		tween.start()
		var close_dialog_timer = Timer.new()
		close_dialog_timer.connect("timeout", self, '_on_close_dialog_timeout')
		add_child(close_dialog_timer)
		close_dialog_timer.start(transition_duration)


func _on_close_dialog_timeout():
	on_timeline_end()
	queue_free()
