# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# warning-ignore-all:unused_signal
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #

extends InkObject

class_name InkStory

const INK_VERSION_CURRENT := 21
const INK_VERSION_MINIMUM_COMPATIBLE := 18

# ############################################################################ #
# Imports
# ############################################################################ #

var InkStopWatch := preload("res://addons/inkgd/runtime/extra/stopwatch.gd") as GDScript
var InkProfiler := preload("res://addons/inkgd/runtime/profiler.gd") as GDScript

var InkSimpleJSON := preload("res://addons/inkgd/runtime/simple_json.gd") as GDScript
var InkStringSet := preload("res://addons/inkgd/runtime/extra/string_set.gd") as GDScript
var InkListDefinitionsOrigin := preload("res://addons/inkgd/runtime/lists/list_definitions_origin.gd") as GDScript

var InkPointer := preload("res://addons/inkgd/runtime/structs/pointer.gd") as GDScript

var StoryErrorMetadata := preload("res://addons/inkgd/runtime/extra/story_error_metadata.gd") as GDScript

# ############################################################################ #

var InkValue := load("res://addons/inkgd/runtime/values/value.gd") as GDScript
var InkIntValue := load("res://addons/inkgd/runtime/values/int_value.gd") as GDScript
var InkStringValue := load("res://addons/inkgd/runtime/values/string_value.gd") as GDScript
var InkVariablePointerValue := load("res://addons/inkgd/runtime/values/variable_pointer_value.gd") as GDScript
var InkListValue := load("res://addons/inkgd/runtime/values/list_value.gd") as GDScript

var InkList := load("res://addons/inkgd/runtime/lists/ink_list.gd") as GDScript

var InkStoryState := load("res://addons/inkgd/runtime/story_state.gd") as GDScript

# ############################################################################ #

var current_choices: Array: # Array<Choice>
	get:
		var choices: Array = [] # Array<Choice>

		for c in self._state.current_choices:
			if !c.is_invisible_default:
				c.index = choices.size()
				choices.append(c)

		return choices


var current_text: # String?
	get:
		if async_we_cant("call currentText since it's a work in progress"):
			return null

		return self.state.current_text

# Array?
var current_tags: # Array<String>
	get:
		if async_we_cant("call currentTags since it's a work in progress"):
			return null

		return self.state.current_tags

var current_errors: # Array<String>?
	get: return self.state.current_errors

var current_warnings: # Array<String>?
	get: return self.state.current_warnings

var current_flow_name: String:
	get: return self.state.current_flow_name

var has_error: bool:
	get: return self.state.has_error

var has_warning: bool:
	get: return self.state.has_warning

var variables_state: InkVariablesState:
	get: return self.state.variables_state

var list_definitions: # ListDefinitionsOrigin?
	get: return self._list_definitions

var state: InkStoryState:
	get: return self._state

signal on_error(message, type)

signal on_did_continue()

signal on_make_choice(choice)

signal on_evaluate_function(function_name, arguments)

signal on_complete_evaluate_function(function_name, arguments, text_output, result)

signal on_choose_path_string(path, arguments)


func start_profiling() -> InkProfiler:
	if async_we_cant ("Start Profiling"):
		return null

	_profiler = InkProfiler.new()
	return _profiler


func end_profiling() -> void:
	_profiler = null


# (InkContainer, Array<ListDefinition>) -> void
func _init_with(content_container: InkContainer, lists = null, runtime = null):
	_initialize_runtime(runtime)
	self._main_content_container = content_container

	if lists != null:
		self._list_definitions = InkListDefinitionsOrigin.new(lists)

	self._externals = {} # Dictionary<String, ExternalFunctionDef>


func _init(json_string: String, runtime = null):
	_init_with(null, null, runtime)

	var root_object = InkSimpleJSON.text_to_dictionary(json_string)

	var version_obj = root_object["inkVersion"]
	if version_obj == null:
		InkUtils.throw_exception(
				"ink version number not found. " +
				"Are you sure it's a valid .ink.json file?"
		)
		return

	var format_from_file = int(version_obj)
	if format_from_file > INK_VERSION_CURRENT:
		InkUtils.throw_exception(
				"Version of ink used to build story was newer " +
				"than the current version of the engine"
		)
		return
	elif format_from_file < INK_VERSION_MINIMUM_COMPATIBLE:
		InkUtils.throw_exception(
				"Version of ink used to build story is too old " +
				"to be loaded by this version of the engine"
		)
		return
	elif format_from_file != INK_VERSION_CURRENT:
		print(
				"[Ink] [WARNING] Version of ink used to build story doesn't match " +
				"current version of engine. Non-critical, but recommend synchronising."
		)

	var root_token = root_object["root"]
	if root_token == null:
		InkUtils.throw_exception(
				"Root node for ink not found. Are you sure it's a valid .ink.json file?"
		)
		return

	if root_object.has("listDefs"):
		self._list_definitions = self.StaticJSON.jtoken_to_list_definitions(root_object["listDefs"])

	self._main_content_container = InkUtils.as_or_null(
			self.StaticJSON.jtoken_to_runtime_object(root_token),
			"InkContainer"
	)

	self.reset_state()


# () -> String
func to_json() -> String:
	var writer: InkSimpleJSON.Writer = InkSimpleJSON.Writer.new()
	to_json_with_writer(writer)
	return writer._to_string()


func write_root_property(writer: InkSimpleJSON.Writer) -> void:
	self.StaticJSON.write_runtime_container(writer, self._main_content_container)


func to_json_with_writer(writer: InkSimpleJSON.Writer) -> void:
	writer.write_object_start()

	writer.write_property("inkVersion", INK_VERSION_CURRENT)

	writer.write_property("root", Callable(self, "write_root_property"))

	if self._list_definitions != null:
		writer.write_property_start("listDefs")
		writer.write_object_start()

		for def in self._list_definitions.lists:
			writer.write_property_start(def.name)
			writer.write_object_start()

			for item_to_val_key in def.items:
				var item = InkListItem.from_serialized_key(item_to_val_key)
				var val = def.items[item_to_val_key]
				writer.write_property(item.item_name, val)

			writer.write_object_end()
			writer.write_property_end()

		writer.write_object_end()
		writer.write_property_end()

	writer.write_object_end()


# () -> void
func reset_state() -> void:
	if async_we_cant ("ResetState"):
		return

	self._state = InkStoryState.new(self, self._ink_runtime)
	self._state.variables_state.connect("variable_changed", Callable(self, "variable_state_did_change_event"))

	self.reset_globals()


# () -> void
func reset_errors() -> void:
	self._state.reset_errors()


# () -> void
func reset_callstack() -> void:
	if async_we_cant("ResetCallstack"):
		return

	self._state.force_end()


# () -> void
func reset_globals() -> void:
	if (self._main_content_container.named_content.has("global decl")):
		var original_pointer = self.state.current_pointer

		self.choose_path(InkPath.new_with_components_string("global decl"), false)

		self.continue_internal()

		self.state.current_pointer = original_pointer

	self.state.variables_state.snapshot_default_globals()


func switch_flow(flow_name: String) -> void:
	if async_we_cant("SwitchFlow"):
		return

	if self._async_saving:
		InkUtils.throw_exception("Story is already in background saving mode, can't switch flow to " + flow_name)

	self.state.switch_flow_internal(flow_name)


func remove_flow(flow_name: String) -> void:
	self.state.remove_flow_internal(flow_name)


func switch_to_default_flow() -> void:
	self.state.switch_to_default_flow_internal()


func continue_story() -> String:
	self.continue_async(0)
	return self.current_text


var can_continue: bool: get = get_continue
func get_continue() -> bool:
	return self.state.can_continue


var async_continue_complete: bool: get = get_async_continue_complete
func get_async_continue_complete() -> bool:
	return !self._async_continue_active


func continue_async(millisecs_limit_async: float):
	if !self._has_validated_externals:
		self.validate_external_bindings()

	continue_internal(millisecs_limit_async)


func continue_internal(millisecs_limit_async: float = 0) -> void:
	if _profiler != null:
		_profiler.pre_continue()

	var is_async_time_limited = millisecs_limit_async > 0

	self._recursive_continue_count += 1

	if !self._async_continue_active:
		self._async_continue_active = is_async_time_limited

		if !self.can_continue:
			InkUtils.throw_exception("Can't continue - should check canContinue before calling Continue")
			return

		self._state.did_safe_exit = false
		self._state.reset_output()

		if self._recursive_continue_count == 1:
			self._state.variables_state.batch_observing_variable_changes = true

	var duration_stopwatch = InkStopWatch.new()
	duration_stopwatch.start()

	var output_stream_ends_in_newline = false
	self._saw_lookahead_unsafe_function_after_newline = false

	# In the original code, exceptions raised during 'continue_single_step()'
	# are catched and added to the error array. Since exceptions don't exist
	# in GDScript, they are recorded instead. See 'ink_runtime.gd' for more
	# information.
	self._enable_story_exception_recording(true)
	var first_time = true
	while (first_time || self.can_continue):
		first_time = false

		output_stream_ends_in_newline = self.continue_single_step()
		var recorded_exceptions = _get_and_clear_recorded_story_exceptions()
		if recorded_exceptions.size() > 0:
			for error in recorded_exceptions:
				add_story_error(error)
			break

		if output_stream_ends_in_newline:
			break

		if _async_continue_active && duration_stopwatch.elapsed_milliseconds > millisecs_limit_async:
			break

	self._enable_story_exception_recording(false)
	duration_stopwatch.stop()

	if output_stream_ends_in_newline || !self.can_continue:

		if self._state_snapshot_at_last_newline != null:
			self.restore_state_snapshot()

		if !self.can_continue:
			if self.state.callstack.can_pop_thread:
				add_error("Thread available to pop, threads should always be flat by the end of evaluation?")

			if self.state.generated_choices.size() == 0 && !self.state.did_safe_exit && self._temporary_evaluation_container == null:
				if self.state.callstack.can_pop_type(Ink.PushPopType.TUNNEL):
					add_error("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?")
				elif self.state.callstack.can_pop_type(Ink.PushPopType.FUNCTION):
					add_error("unexpectedly reached end of content. Do you need a '~ return'?")
				elif !self.state.callstack.can_pop:
					add_error("ran out of content. Do you need a '-> DONE' or '-> END'?")
				else:
					add_error("unexpectedly reached end of content for unknown reason. Please debug compiler!")

		self.state.did_safe_exit = false
		self._saw_lookahead_unsafe_function_after_newline = false

		if _recursive_continue_count == 1:
			_state.variables_state.batch_observing_variable_changes = false

		self._async_continue_active = false
		emit_signal("on_did_continue")

	self._recursive_continue_count -= 1

	if _profiler != null:
		_profiler.post_continue()

	if self.state.has_error || self.state.has_warning:
		if !self.get_signal_connection_list("on_error").is_empty():
			if self.state.has_error:
				for err in self.state.current_errors:
					emit_signal("on_error", err, Ink.ErrorType.ERROR)

			if self.state.has_warning:
				for err in self.state.current_warnings:
					emit_signal("on_error", err, Ink.ErrorType.WARNING)

			self.reset_errors()
		else:
			var exception = "Ink had "

			if self.state.has_error:
				exception += str(self.state.current_errors.size())
				exception += " error" if self.state.current_errors.size() == 1 else " errors"
				if self.state.has_warning:
					exception += " and "

			if self.state.has_warning:
				exception += str(self.state.current_warnings.size())
				exception += " warning" if self.state.current_warnings.size() == 1 else " warnings"

			exception += ". It is strongly suggested that you assign an error handler to story.onError. The first issue was: "
			exception += self.state.current_errors[0] if self.state.has_error else self.state.current_warnings[0]

			# If you get this exception, please connect an error handler to the appropriate signal: "on_error".
			self._throw_story_exception(exception)


func continue_single_step() -> bool:
	if _profiler != null:
		_profiler.pre_step()

	self.step()

	if _profiler != null:
		_profiler.post_step()


	if !self.can_continue && !self.state.callstack.element_is_evaluate_from_game:
		self.try_follow_default_invisible_choice()


	if _profiler != null:
		_profiler.pre_snapshot()

	if !self.state.in_string_evaluation:
		if self._state_snapshot_at_last_newline != null:

			var change = calculate_newline_output_state_change(
				self._state_snapshot_at_last_newline.current_text, self.state.current_text,
				self._state_snapshot_at_last_newline.current_tags.size(), self.state.current_tags.size()
			)

			if change == OutputStateChange.EXTENDED_BEYOND_NEWLINE || self._saw_lookahead_unsafe_function_after_newline:
				self.restore_state_snapshot()

				return true
			elif change == OutputStateChange.NEWLINE_REMOVED:
				self.discard_snapshot()

		if self.state.output_stream_ends_in_newline:
			if self.can_continue:
				if self._state_snapshot_at_last_newline == null:
					self.state_snapshot()
			else:
				self.discard_snapshot()

	if _profiler != null:
		_profiler.post_snapshot()

	return false


enum OutputStateChange {
	NO_CHANGE,
	EXTENDED_BEYOND_NEWLINE,
	NEWLINE_REMOVED
}


# (String, String, int, int) -> OutputStateChange
func calculate_newline_output_state_change(
	prev_text: String,
	curr_text: String,
	prev_tag_count: int,
	curr_tag_count: int
) -> int:
	var newline_still_exists = curr_text.length() >= prev_text.length() && prev_text.length() > 0 && curr_text[prev_text.length() - 1] == "\n"
	if (prev_tag_count == curr_tag_count && prev_text.length() == curr_text.length() && newline_still_exists):
		return OutputStateChange.NO_CHANGE

	if !newline_still_exists:
		return OutputStateChange.NEWLINE_REMOVED

	if curr_tag_count > prev_tag_count:
		return OutputStateChange.EXTENDED_BEYOND_NEWLINE

	var i = prev_text.length()
	while i < curr_text.length():
		var c = curr_text[i]

		if c != " " && c != "\t":
			return OutputStateChange.EXTENDED_BEYOND_NEWLINE

		i += 1

	return OutputStateChange.NO_CHANGE


func continue_story_maximally() -> String:
	if async_we_cant("ContinueMaximally"):
		return ""

	var _str = ""

	while (self.can_continue):
		_str += self.continue_story()

	return _str


func content_at_path(path: InkPath) -> InkSearchResult:
	return self.main_content_container.content_at_path(path)


func knot_container_with_name(name: String) -> InkContainer:
	if self.main_content_container.named_content.has(name):
		return InkUtils.as_or_null(self.main_content_container.named_content[name], "InkContainer")

	return null


func pointer_at_path(path: InkPath) -> InkPointer:
	if (path.length == 0):
		return InkPointer.null_pointer

	var p = InkPointer.new()

	var path_length_to_use = path.length

	var result = null # SearchResult
	if (path.last_component.is_index):
		path_length_to_use = path.length - 1
		result = self.main_content_container.content_at_path(path, 0, path_length_to_use)
		p = InkPointer.new(result.container, path.last_component.index)
	else:
		result = self.main_content_container.content_at_path(path)
		p = InkPointer.new(result.container, -1)

	if result.obj == null || result.obj == self.main_content_container && path_length_to_use > 0:
		error(
			"Failed to find content at path '%s', " % path._to_string() +
			"and no approximation of it was possible."
		)
	elif result.approximate:
		warning(
			"Failed to find content at path '%s', " %  path +
			"so it was approximated to: '%s'." % result.obj.path._to_string()
		)

	return p


func state_snapshot() -> void:
	self._state_snapshot_at_last_newline = self._state
	self._state = self._state.copy_and_start_patching()


func restore_state_snapshot() -> void:
	self._state_snapshot_at_last_newline.restore_after_patch()

	self._state = self._state_snapshot_at_last_newline
	self._state_snapshot_at_last_newline = null

	if !self._async_saving:
		self._state.apply_any_patch()


func discard_snapshot() -> void:
	if !self._async_saving:
		self._state.apply_any_patch()

	self._state_snapshot_at_last_newline = null


func copy_state_for_background_thread_save() -> InkStoryState:
	if async_we_cant("start saving on a background thread"):
		return null

	if self._async_saving:
		InkUtils.throw_exception(
				"Story is already in background saving mode, " +
				"can't call CopyStateForBackgroundThreadSave again!"
		)
		return null

	var state_to_save = self._state
	self._state = self._state.copy_and_start_patching()
	self._async_saving = true

	return state_to_save


func background_save_complete() -> void:
	if self._state_snapshot_at_last_newline == null:
		_state.apply_any_patch()

	self._async_saving = false


func step() -> void:
	var should_add_to_stream = true

	var pointer = self.state.current_pointer
	if pointer.is_null:
		return

	var container_to_enter = InkUtils.as_or_null(pointer.resolve(), "InkContainer")
	while (container_to_enter):
		self.visit_container(container_to_enter, true)

		if container_to_enter.content.size() == 0:
			break

		pointer = InkPointer.start_of(container_to_enter)
		container_to_enter = InkUtils.as_or_null(pointer.resolve(), "InkContainer")

	self.state.current_pointer = pointer

	if _profiler != null:
		_profiler.step(state.callstack)

	var current_content_obj = pointer.resolve()
	var is_logic_or_flow_control = perform_logic_and_flow_control(current_content_obj)

	if self.state.current_pointer.is_null:
		return

	if is_logic_or_flow_control:
		should_add_to_stream = false

	var choice_point = InkUtils.as_or_null(current_content_obj, "ChoicePoint")
	if choice_point:
		var choice = process_choice(choice_point)
		if choice:
			self.state.generated_choices.append(choice)

		current_content_obj = null
		should_add_to_stream = false

	if InkUtils.is_ink_class(current_content_obj, "InkContainer"):
		should_add_to_stream = false

	if should_add_to_stream:
		var var_pointer = InkUtils.as_or_null(current_content_obj, "VariablePointerValue")
		if var_pointer && var_pointer.context_index == -1:
			var context_idx = self.state.callstack.context_for_variable_named(var_pointer.variable_name)
			current_content_obj = InkVariablePointerValue.new_with_context(var_pointer.variable_name, context_idx)

		if self.state.in_expression_evaluation:
			self.state.push_evaluation_stack(current_content_obj)
		else:
			self.state.push_to_output_stream(current_content_obj)

	self.next_content()

	var control_cmd = InkUtils.as_or_null(current_content_obj, "ControlCommand")
	if control_cmd && control_cmd.command_type == InkControlCommand.CommandType.START_THREAD:
		self.state.callstack.push_thread()


func visit_container(container: InkContainer, at_start: bool) -> void:
	if !container.counting_at_start_only || at_start:
		if container.visits_should_be_counted:
			self.state.increment_visit_count_for_container(container)

		if container.turn_index_should_be_counted:
			self.state.record_turn_index_visit_to_container(container)


var _prev_containers = [] # Array<Container>
func visit_changed_containers_due_to_divert() -> void:
	var previous_pointer = self.state.previous_pointer
	var pointer = self.state.current_pointer

	if pointer.is_null || pointer.index == -1:
		return

	self._prev_containers.clear()
	if !previous_pointer.is_null:
		var prev_ancestor = InkUtils.as_or_null(previous_pointer.resolve(), "InkContainer")
		prev_ancestor = prev_ancestor if prev_ancestor else InkUtils.as_or_null(previous_pointer.container, "InkContainer")
		while prev_ancestor:
			self._prev_containers.append(prev_ancestor)
			prev_ancestor = InkUtils.as_or_null(prev_ancestor.parent, "InkContainer")

	var current_child_of_container = pointer.resolve()

	if current_child_of_container == null: return

	var current_container_ancestor = InkUtils.as_or_null(current_child_of_container.parent, "InkContainer")

	var all_children_entered_at_start = true
	while current_container_ancestor && (self._prev_containers.find(current_container_ancestor) < 0 || current_container_ancestor.counting_at_start_only):

		var entering_at_start = (current_container_ancestor.content.size() > 0 &&
								current_child_of_container == current_container_ancestor.content[0] &&
								all_children_entered_at_start)

		if !entering_at_start:
			all_children_entered_at_start = false

		self.visit_container(current_container_ancestor, entering_at_start)

		current_child_of_container = current_container_ancestor
		current_container_ancestor = InkUtils.as_or_null(current_container_ancestor.parent, "InkContainer")


# The original implementation would return the choice string and update the
# array of tags passed in parameter. Since in/out (ref) parameters are not supported
# in GDScript, the method returns a tuple (array) instead.
# (Array<String>) -> [String, Array<String>]
func pop_choice_string_and_tags(tags) -> Array:
	var choice_only_str_val = InkUtils.cast(self.state.pop_evaluation_stack(), "StringValue")

	while self.state.evaluation_stack.size() > 0 and InkUtils.is_ink_class(state.peek_evaluation_stack(), "Tag"):
		if tags == null:
			tags = []
		var tag = InkUtils.cast(self.state.pop_evaluation_stack(), "Tag")
		tags.insert(0, tag.text)

	return [choice_only_str_val.value, tags]


func process_choice(choice_point: InkChoicePoint) -> InkChoice:
	var show_choice := true

	if choice_point.has_condition:
		var condition_value = self.state.pop_evaluation_stack()
		if !self.is_truthy(condition_value):
			show_choice = false

	var start_text := ""
	var choice_only_text := ""
	var tags = null

	if choice_point.has_choice_only_content:
		var choice_strings_and_tags = self.pop_choice_string_and_tags(tags)
		choice_only_text = choice_strings_and_tags[0]
		tags = choice_strings_and_tags[1]

	if choice_point.has_start_content:
		var choice_strings_and_tags = self.pop_choice_string_and_tags(tags)
		start_text = choice_strings_and_tags[0]
		tags = choice_strings_and_tags[1]

	if choice_point.once_only:
		var visit_count = self.state.visit_count_for_container(choice_point.choice_target)
		if visit_count > 0:
			show_choice = false

	if !show_choice:
		return null

	var choice = InkChoice.new()
	choice.target_path = choice_point.path_on_choice
	choice.source_path = choice_point.path._to_string()
	choice.is_invisible_default = choice_point.is_invisible_default
	choice.tags = tags
	choice.thread_at_generation = self.state.callstack.fork_thread()

	choice.text = InkUtils.trim(start_text + choice_only_text, [" ", "\t"])

	return choice


func is_truthy(obj: InkObject) -> bool:
	var truthy = false
	if InkUtils.is_ink_class(obj, "Value"):
		var val = obj

		if InkUtils.is_ink_class(obj, "DivertTargetValue"):
			var div_target = val
			error(str("Shouldn't use a divert target (to ", div_target.target_path._to_string(),
					  ") as a conditional value. Did you intend a function call 'likeThis()'",
					  " or a read count check 'likeThis'? (no arrows)"))
			return false

		return val.is_truthy

	return truthy


func perform_logic_and_flow_control(content_obj: InkObject) -> bool:
	if (content_obj == null):
		return false

	if InkUtils.is_ink_class(content_obj, "Divert"):
		var current_divert = content_obj

		if current_divert.is_conditional:
			var condition_value = self.state.pop_evaluation_stack()

			if !self.is_truthy(condition_value):
				return true

		if current_divert.has_variable_target:
			var var_name = current_divert.variable_divert_name
			var var_contents = self.state.variables_state.get_variable_with_name(var_name)

			if var_contents == null:
				error(str("Tried to divert using a target from a variable that could not be found (",
						  var_name, ")"))
				return false
			elif !InkUtils.is_ink_class(var_contents, "DivertTargetValue"):
				var int_content = InkUtils.as_or_null(var_contents, "IntValue")

				var error_message = str("Tried to divert to a target from a variable,",
										"but the variable (", var_name,
										") didn't contain a divert target, it ")
				if int_content && int_content.value == 0:
					error_message += "was empty/null (the value 0)."
				else:
					error_message += "contained '" + var_contents + "'."

				error(error_message)
				return false

			var target = var_contents
			self.state.diverted_pointer = self.pointer_at_path(target.target_path)

		elif current_divert.is_external:
			call_external_function(current_divert.target_path_string, current_divert.external_args)
			return true
		else:
			self.state.diverted_pointer = current_divert.target_pointer

		if current_divert.pushes_to_stack:
			self.state.callstack.push(
				current_divert.stack_push_type,
				0,
				self.state.output_stream.size()
			)

		if self.state.diverted_pointer.is_null && !current_divert.is_external:
			if current_divert && current_divert.debug_metadata != null && current_divert.debug_metadata.source_name != null:
				error("Divert target doesn't exist: " + current_divert.debug_metadata.source_name)
				return false
			else:
				error("Divert resolution failed: " + current_divert._to_string())
				return false

		return true
	elif InkUtils.is_ink_class(content_obj, "ControlCommand"):
		var eval_command = content_obj

		match eval_command.command_type:

			InkControlCommand.CommandType.EVAL_START:
				self.__assert__(
						self.state.in_expression_evaluation == false,
						"Already in expression evaluation?"
				)
				self.state.in_expression_evaluation = true

			InkControlCommand.CommandType.EVAL_END:
				self.__assert__(
						self.state.in_expression_evaluation == true,
						"Not in expression evaluation mode"
				)
				self.state.in_expression_evaluation = false

			InkControlCommand.CommandType.EVAL_OUTPUT:
				if self.state.evaluation_stack.size() > 0:
					var output = self.state.pop_evaluation_stack()

					if !InkUtils.as_or_null(output, "Void"):
						var text = InkStringValue.new_with(output._to_string())
						self.state.push_to_output_stream(text)

			InkControlCommand.CommandType.NO_OP:
				pass

			InkControlCommand.CommandType.DUPLICATE:
				self.state.push_evaluation_stack(self.state.peek_evaluation_stack())

			InkControlCommand.CommandType.POP_EVALUATED_VALUE:
				self.state.pop_evaluation_stack()

			InkControlCommand.CommandType.POP_FUNCTION, InkControlCommand.CommandType.POP_TUNNEL:
				var is_pop_function = (
						eval_command.command_type == InkControlCommand.CommandType.POP_FUNCTION
				)
				var pop_type = Ink.PushPopType.FUNCTION if is_pop_function else Ink.PushPopType.TUNNEL

				var override_tunnel_return_target = null # DivertTargetValue
				if pop_type == Ink.PushPopType.TUNNEL:
					var popped = self.state.pop_evaluation_stack()
					override_tunnel_return_target = InkUtils.as_or_null(popped, "DivertTargetValue")
					if override_tunnel_return_target == null:
						self.__assert__(
								InkUtils.is_ink_class(popped, "Void"),
								"Expected void if ->-> doesn't override target"
						)

				if self.state.try_exit_function_evaluation_from_game():
					pass
				elif self.state.callstack.current_element.type != pop_type || !self.state.callstack.can_pop:
					var names = {} # Dictionary<Ink.PushPopType, String>
					names[Ink.PushPopType.FUNCTION] = "function return statement (~ return)"
					names[Ink.PushPopType.TUNNEL] = "tunnel onwards statement (->->)"

					var expected = names[self.state.callstack.current_element.type]
					if !self.state.callstack.can_pop:
						expected = "end of flow (-> END or choice)"

					var error_msg = "Found %s, when expected %s" % [names[pop_type], expected]

					error(error_msg)
				else:
					self.state.pop_callstack()

					if override_tunnel_return_target:
						self.state.diverted_pointer = self.pointer_at_path(override_tunnel_return_target.target_path)

			InkControlCommand.CommandType.BEGIN_STRING:
				self.state.push_to_output_stream(eval_command)

				self.__assert__(
						self.state.in_expression_evaluation == true,
						"Expected to be in an expression when evaluating a string"
				)
				self.state.in_expression_evaluation = false

			InkControlCommand.CommandType.BEGIN_TAG:
				self.state.push_to_output_stream(eval_command)

			InkControlCommand.CommandType.END_TAG:
				if self.state.in_string_evaluation:
					var content_stack_for_tag := [] # Stack<InkObject>
					var output_count_consumed := 0

					var i = self.state.output_stream.size() - 1
					while i >= 0:
						var obj = self.state.output_stream[i]

						output_count_consumed += 1

						var command = InkUtils.as_or_null(obj, "ControlCommand")
						if command != null:
							if command.command_type == InkControlCommand.CommandType.BEGIN_TAG:
								break
							else:
								self.error("Unexpected ControlCommand while extracting tag from choice")
								break

						if InkUtils.is_ink_class(obj, "StringValue"):
							content_stack_for_tag.push_front(obj)

						i -= 1

					self.state.pop_from_output_stream(output_count_consumed)

					var sb = ""
					for str_val in content_stack_for_tag:
						sb += str_val.value

					var choice_tag = InkTag.new(self.state.clean_output_whitespace(sb))

					self.state.push_evaluation_stack(choice_tag)
				else:
					self.state.push_to_output_stream(eval_command)


			InkControlCommand.CommandType.END_STRING:
				var content_stack_for_string := [] # Stack<InkObject>
				var content_to_retain := [] # Stack<InkObject>

				var output_count_consumed := 0
				var i = self.state.output_stream.size() - 1
				while (i >= 0):
					var obj = self.state.output_stream[i]

					output_count_consumed += 1

					var command = InkUtils.as_or_null(obj, "ControlCommand")
					if (command != null &&
						command.command_type == InkControlCommand.CommandType.BEGIN_STRING):
						break

					if InkUtils.is_ink_class(obj, "Tag"):
						content_to_retain.push_front(obj)

					if InkUtils.is_ink_class(obj, "StringValue"):
						content_stack_for_string.push_front(obj)

					i -= 1

				self.state.pop_from_output_stream(output_count_consumed)

				for rescued_tag in content_to_retain:
					state.push_to_output_stream(rescued_tag)

				var _str := ""
				for c in content_stack_for_string:
					_str += c._to_string()

				self.state.in_expression_evaluation = true
				self.state.push_evaluation_stack(InkStringValue.new_with(_str))

			InkControlCommand.CommandType.CHOICE_COUNT:
				var choice_count = self.state.generated_choices.size()
				self.state.push_evaluation_stack(InkIntValue.new_with(choice_count))

			InkControlCommand.CommandType.TURNS:
				self.state.push_evaluation_stack(InkIntValue.new_with(self.state.current_turn_index + 1))

			InkControlCommand.CommandType.TURNS_SINCE, InkControlCommand.CommandType.READ_COUNT:
				var target = self.state.pop_evaluation_stack()
				if !InkUtils.is_ink_class(target, "DivertTargetValue"):
					var extra_note = ""
					if InkUtils.is_ink_class(target, "IntValue"):
						extra_note = ". Did you accidentally pass a read count ('knot_name') instead of a target ('-> knot_name')?"
					error(str("TURNS_SINCE expected a divert target (knot, stitch, label name), but saw ",
							  target, extra_note))
					return false

				var divert_target = InkUtils.as_or_null(target, "DivertTargetValue")
				var container = InkUtils.as_or_null(self.content_at_path(divert_target.target_path).correct_obj, "InkContainer")

				var either_count = 0
				if container != null:
					if eval_command.command_type == InkControlCommand.CommandType.TURNS_SINCE:
						either_count = self.state.turns_since_for_container(container)
					else:
						either_count = self.state.visit_count_for_container(container)
				else:
					if eval_command.command_type == InkControlCommand.CommandType.TURNS_SINCE:
						either_count = -1
					else:
						either_count = 0

					warning(str("Failed to find container for ", eval_command._to_string(),
								" lookup at ", divert_target.target_path._to_string()))

				self.state.push_evaluation_stack(InkIntValue.new_with(either_count))

			InkControlCommand.CommandType.RANDOM:
				var max_int = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "IntValue")
				var min_int = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "IntValue")

				if min_int == null:
					error("Invalid value for minimum parameter of RANDOM(min, max)")
					return false

				if max_int == null:
					error("Invalid value for maximum parameter of RANDOM(min, max)")
					return false

				var random_range
				if max_int.value == (1 << 63) - 1 && min_int.value == 0:
					random_range = max_int.value
					error(str("RANDOM was called with a range that exceeds the size that ink numbers can use."))
					return false
				else:
					random_range = max_int.value - min_int.value + 1
				if random_range <= 0:
					error(str("RANDOM was called with minimum as ", min_int.value,
							  " and maximum as ", max_int.value, ". The maximum must be larger"))
					return false

				var result_seed = self.state.story_seed + self.state.previous_random
				seed(result_seed)

				var next_random = randi()
				var chosen_value = (next_random % random_range) + min_int.value
				self.state.push_evaluation_stack(InkIntValue.new_with(chosen_value))

				self.state.previous_random = next_random

			InkControlCommand.CommandType.SEED_RANDOM:
				var _seed = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "IntValue")
				if _seed == null:
					error("Invalid value passed to SEED_RANDOM")
					return false

				self.state.story_seed = _seed.value
				self.state.previous_random = 0

				self.state.push_evaluation_stack(InkVoid.new())

			InkControlCommand.CommandType.VISIT_INDEX:
				var count = self.state.visit_count_for_container(self.state.current_pointer.container) - 1
				self.state.push_evaluation_stack(InkIntValue.new_with(count))

			InkControlCommand.CommandType.SEQUENCE_SHUFFLE_INDEX:
				var shuffle_index = self.next_sequence_shuffle_index()
				self.state.push_evaluation_stack(InkIntValue.new_with(shuffle_index))

			InkControlCommand.CommandType.START_THREAD:
				pass

			InkControlCommand.CommandType.DONE:
				if self.state.callstack.can_pop_thread:
					self.state.callstack.pop_thread()
				else:
					self.state.did_safe_exit = true
					self.state.current_pointer = InkPointer.null_pointer

			InkControlCommand.CommandType.END:
				self.state.force_end()

			InkControlCommand.CommandType.LIST_FROM_INT:
				var int_val = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "IntValue")
				var list_name_val = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "StringValue")

				if int_val == null:
					self._throw_story_exception(
							"Passed non-integer when creating a list element from a numerical value."
					)
					return false

				var generated_list_value = null # ListValue

				var found_list_def: InkTryGetResult = self.list_definitions.try_list_get_definition(list_name_val.value)
				if found_list_def.exists:
					var found_item: InkTryGetResult = found_list_def.result.try_get_item_with_value(int_val.value)
					if found_item.exists:
						generated_list_value = InkListValue.new_with_single_item(
								found_item.result,
								int_val.value
						)
				else:
					self._throw_story_exception("Failed to find LIST called %s" % list_name_val.value)
					return false

				if generated_list_value == null:
					generated_list_value = InkListValue.new()

				self.state.push_evaluation_stack(generated_list_value)

			InkControlCommand.CommandType.LIST_RANGE:
				var max_value = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "Value")
				var min_value = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "Value")

				var target_list = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "ListValue")

				if target_list == null || min_value == null || max_value == null:
					self._throw_story_exception("Expected list, minimum and maximum for LIST_RANGE")
					return false

				var result = target_list.value.list_with_sub_range(min_value.value_object, max_value.value_object)

				self.state.push_evaluation_stack(InkListValue.new_with(result))

			InkControlCommand.CommandType.LIST_RANDOM:

				var list_val = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "ListValue")
				if list_val == null:
					self._throw_story_exception("Expected list for LIST_RANDOM")
					return false

				var list = list_val.value

				var new_list = null # InkList

				if list.size() == 0:
					new_list = InkList.new()
				else:
					var result_seed = self.state.story_seed + self.state.previous_random
					seed(result_seed)

					var next_random = randi()
					var list_item_index = next_random % list.size()

					# Iterator-based code in replaced with this code:
					if list_item_index < 0: list_item_index = 0
					if list_item_index >= list.size(): list_item_index = list.size() - 1

					var raw_random_item = list.raw_keys()[list_item_index]
					var random_item = InkListItem.from_serialized_key(raw_random_item)
					var random_item_value = list.get_raw(raw_random_item)

					new_list = InkList.new_with_origin(random_item.origin_name, self)
					new_list.set_raw(raw_random_item, random_item_value)

					self.state.previous_random = next_random

				self.state.push_evaluation_stack(InkListValue.new_with(new_list))

			_:
				error("unhandled ControlCommand: " + eval_command._to_string())
				return false

		return true

	elif InkUtils.as_or_null(content_obj, "VariableAssignment"):
		var var_ass = content_obj
		var assigned_val = self.state.pop_evaluation_stack()

		# This does not exist in the original codebase.
		# It ensures the type is correct, so we execution
		# isn't stopped when the argument has an incorrect
		# type. (In the original codebase, this is handled
		# through exceptions.)
		if !InkUtils.is_ink_class(assigned_val, "InkObject"):
			return false

		self.state.variables_state.assign(var_ass, assigned_val)

		return true

	elif InkUtils.as_or_null(content_obj, "VariableReference"):
		var var_ref = content_obj
		var found_value = null # InkValue

		if var_ref.path_for_count != null:
			var container = var_ref.container_for_count
			var count = self.state.visit_count_for_container(container)
			found_value = InkIntValue.new_with(count)
		else:
			found_value = self.state.variables_state.get_variable_with_name(var_ref.name)

			if found_value == null:
				warning(str("Variable not found: '", var_ref.name,
							"', using default value of 0 (false). this can ",
							"happen with temporary variables if the declaration ",
							"hasn't yet been hit. Globals are always given a default ",
							"value on load if a value doesn't exist in the save state."))
				found_value = InkIntValue.new_with(0)

		self.state.push_evaluation_stack(found_value)
		return true

	elif InkUtils.as_or_null(content_obj, "NativeFunctionCall"):
		var function = content_obj
		var func_params = self.state.pop_evaluation_stack(function.number_of_parameters)
		var result = function.call_with_parameters(func_params, _make_story_error_metadata())
		self.state.push_evaluation_stack(result)
		return true

	return false

# (String, bool, Array) -> void
func choose_path_string(path, reset_callstack = true, arguments = null):
	if async_we_cant("call ChoosePathString right now"):
		return

	emit_signal("on_choose_path_string", path, arguments)

	if reset_callstack:
		self.reset_callstack()
	else:
		if self.state.callstack.current_element.type == Ink.PushPopType.FUNCTION:
			var func_detail = ""
			var container = self.state.callstack.current_element.current_pointer.container
			if container != null:
				func_detail = "(" + container.path._to_string() + ") "

			InkUtils.throw_exception(
					("Story was running a function %s" % func_detail) +
					("when you called ChoosePathString(%s) " % path) +
					"- this is almost certainly not not what you want! Full stack trace: \n" +
					self.state.callstack.callstack_trace
			)

			return

	self.state.pass_arguments_to_evaluation_stack(arguments)
	self.choose_path(InkPath.new_with_components_string(path))


func async_we_cant(activity_str):
	if self._async_continue_active:
		InkUtils.throw_exception(
				"Can't %s. Story is in the middle of a ContinueAsync(). " % activity_str +
				"Make more ContinueAsync() calls or a single Continue() call beforehand."
		)

	return _async_continue_active


# (InkPath, bool)
func choose_path(p, incrementing_turn_index = true):
	self.state.set_chosen_path(p, incrementing_turn_index)

	self.visit_changed_containers_due_to_divert()


# (int) -> void
func choose_choice_index(choice_idx):
	var choices = self.current_choices
	self.__assert__(
			choice_idx >= 0 && choice_idx < choices.size(),
			"choice out of range"
	)

	var choice_to_choose = choices[choice_idx]
	emit_signal("on_make_choice", choice_to_choose)

	self.state.callstack.current_thread = choice_to_choose.thread_at_generation

	choose_path(choice_to_choose.target_path)


# (String) -> bool
func has_function(function_name: String) -> bool:
	return knot_container_with_name(function_name) != null


# (String, Array<Variant>, bool) -> Variant
func evaluate_function(
	function_name: String,
	arguments = null,
	return_text_output: bool = false
):
	# Like inkjs, evaluate_function behaves differently than the C# version.
	# In C#, you can pass a (second) parameter `out textOutput` to get the
	# text outputted by the function. Instead, we maintain the regular signature,
	# plus an optional third parameter return_text_output. If set to true, we will
	# return both the text_output and the returned value, as a Dictionary.

	emit_signal("on_evaluate_function", function_name, arguments)
	if async_we_cant("evaluate a function"):
		return

	if function_name == null:
		InkUtils.throw_exception("Function is null")
		return null
	elif function_name == "" || InkUtils.trim(function_name) == "":
		InkUtils.throw_exception("Function is empty or white space.")
		return null

	var func_container = knot_container_with_name(function_name)
	if func_container == null:
		InkUtils.throw_exception("Function doesn't exist: '%s'" % function_name)
		return null

	var output_stream_before = self.state.output_stream.duplicate() # Array<InkObject>
	_state.reset_output()

	self.state.start_function_evaluation_from_game(func_container, arguments)

	# TODO: Add comment - This code did not exist in upstream.
	if self._ink_runtime.clear_raised_exceptions():
		return null

	var string_output = ""
	while self.can_continue:
		string_output += self.continue_story()

	var text_output = string_output

	_state.reset_output(output_stream_before)

	var result = self.state.complete_function_evaluation_from_game()

	emit_signal("on_complete_evaluate_function", function_name, arguments, text_output, result)
	if return_text_output:
		return { "result": result, "output": text_output }
	else:
		return result


# (InkContainer) -> InkObject
func evaluate_expression(expr_container: InkContainer) -> InkObject:
	var start_callstack_height = self.state.callstack.elements.size()

	self.state.callstack.push(Ink.PushPopType.TUNNEL)

	_temporary_evaluation_container = expr_container

	self.state.go_to_start()

	var eval_stack_height = self.state.evaluation_stack.size()

	self.continue_story()

	_temporary_evaluation_container = null

	if self.state.callstack.elements.size() > start_callstack_height:
		self.state.pop_callstack()

	var end_stack_height = self.state.evaluation_stack.size()
	if end_stack_height > eval_stack_height:
		return self.state.pop_evaluation_stack()
	else:
		return null

var allow_external_function_fallbacks = false # bool


# (String, int) -> void
func call_external_function(func_name: String, number_of_arguments: int) -> void:
	var _func_def = null # ExternalFunctionDef
	var fallback_function_container = null # InkContainer

	if self._externals.has(func_name):
		_func_def = self._externals.get(func_name)
		if _func_def != null && !_func_def.lookahead_safe && self._state_snapshot_at_last_newline != null:
			self._saw_lookahead_unsafe_function_after_newline = true
			return

	if _func_def == null:
		if allow_external_function_fallbacks:
			fallback_function_container = self.knot_container_with_name(func_name)
			self.__assert__(
				fallback_function_container != null,
				"Trying to call EXTERNAL function '%s' " % func_name +
				"which has not been bound, and fallback ink function" +
				"could not be found."
			)

			self.state.callstack.push(
				Ink.PushPopType.FUNCTION,
				0,
				self.state.output_stream.size()
			)

			self.state.diverted_pointer = InkPointer.start_of(fallback_function_container)
			return
		else:
			self.__assert__(
				false,
				"Trying to call EXTERNAL function '%s' " % func_name +
				"which has not been bound (and ink fallbacks disabled)."
			)
			return

	var arguments = [] # Array<Variant>
	var i = 0
	while i < number_of_arguments:
		var popped_obj = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "Value")
		var value_obj = popped_obj.value_object
		arguments.append(value_obj)

		i += 1

	arguments.reverse()

	var func_result = _func_def.execute(arguments)

	var return_obj = null
	if func_result != null:
		return_obj = InkValue.create(func_result)
		self.__assert__(
			return_obj != null,
			"Could not create ink value from returned object of type %s" % \
			InkUtils.typename_of(typeof(func_result))
		)
	else:
		return_obj = InkVoid.new()

	self.state.push_evaluation_stack(return_obj)


# (String, Variant, ExternalFunctionDef, bool) -> void
func bind_external_function_general(
	func_name: String,
	object,
	method: String,
	lookahead_safe: bool = true
) -> void:
	if async_we_cant("bind an external function"):
		return

	self.__assert__(
			!_externals.has(func_name),
			"Function '%s' has already been bound." % func_name
	)

	_externals[func_name] = ExternalFunctionDef.new(object, method, lookahead_safe)


# try_coerce not needed.


# (String, Variant, String, bool) -> void
func bind_external_function(
	func_name: String,
	object,
	method_name: String,
	lookahead_safe: bool = false
) -> void:
	self.__assert__(
			object != null || method_name != null,
			"Can't bind a null function"
	)

	bind_external_function_general(func_name, object, method_name, lookahead_safe)


func unbind_external_function(func_name: String) -> void:
	if async_we_cant("unbind an external a function"):
		return

	self.__assert__(
			_externals.has(func_name),
			"Function '%s' has not been bound." % func_name
	)
	_externals.erase(func_name)


func validate_external_bindings() -> void:
	var missing_externals: InkStringSet = InkStringSet.new()

	validate_external_bindings_with(_main_content_container, missing_externals)
	_has_validated_externals = true

	if missing_externals.size() == 0:
		_has_validated_externals = true
	else:
		var message: String = "ERROR: Missing function binding for external %s: '%s' %s" % [
			"s" if missing_externals.size() > 1 else "",
			"', '", InkUtils.join("', '", missing_externals.to_array()),
			", and no fallback ink function found." if allow_external_function_fallbacks else " (ink fallbacks disabled)"
		]

		error(message)


func validate_external_bindings_with(o: InkObject, missing_externals: InkStringSet) -> void:
	var container = InkUtils.as_or_null(o, "InkContainer")
	if container:
		for inner_content in o.content:
			var inner_container = InkUtils.as_or_null(inner_content, "InkContainer")
			if inner_container == null || !inner_container.has_valid_name:
				validate_external_bindings_with(inner_content, missing_externals)

		for inner_key in o.named_content:
			validate_external_bindings_with(
				InkUtils.as_or_null(o.named_content[inner_key], "InkObject"),
				missing_externals
			)
		return

	var divert = InkUtils.as_or_null(o, "Divert")
	if divert && divert.is_external:
		var name = divert.target_path_string

		if !_externals.has(name):
			if allow_external_function_fallbacks:
				var fallback_found = self.main_content_container.named_content.has(name)
				if !fallback_found:
					missing_externals.append(name)
			else:
				missing_externals.append(name)


# (String, Object, String) -> void
func observe_variable(variable_name: String, object, method_name: String) -> void:
	if async_we_cant("observe a new variable"):
		return

	if _variable_observers == null:
		_variable_observers = {}

	if !self.state.variables_state.global_variable_exists_with_name(variable_name):
		InkUtils.throw_exception(
				"Cannot observe variable '%s'" % variable_name +
				"because it wasn't declared in the ink story."
		)
		return

	if _variable_observers.has(variable_name):
		_variable_observers[variable_name].connect("variable_changed", Callable(object, method_name))
	else:
		var new_observer = VariableObserver.new(variable_name)
		new_observer.connect("variable_changed", Callable(object, method_name))

		_variable_observers[variable_name] = new_observer


# (Array<String>, Object, String) -> void
func observe_variables(variable_names: Array, object, method_name: String) -> void:
	for var_name in variable_names:
		observe_variable(var_name, object, method_name)


# (Object, String, String) -> void
# TODO: Rewrite this poor documentation and improve method beyond what
#       upstream offers.
#
# Potential cases:
#     - specific_variable_name is null, but object and method_name are both present
#       -> all signals, pointing to object & method_name and regardless of the
#          variable they listen to, are disconnected.
#
#     - specific_variable_name is present, but both object and method_name are null
#       -> all signals listening to changes of specific_variable_name are disconnected.
#
#     - object and method_name have mismatched presence
#       -> this is an unsuported case at the moment.
func remove_variable_observer(object = null, method_name = null, specific_variable_name = null):
	if async_we_cant("remove a variable observer"):
		return

	if _variable_observers == null:
		return

	if specific_variable_name != null:
		if _variable_observers.has(specific_variable_name):
			var observer = _variable_observers[specific_variable_name]
			if object != null && method_name != null:
				observer.disconnect("variable_changed", Callable(object, method_name))

				if observer.get_signal_connection_list("variable_changed").is_empty():
					_variable_observers.erase(specific_variable_name)
			else:
				var connections = observer.get_signal_connection_list("variable_changed");
				for connection in connections:
					observer.disconnect(connection.signal.get_name(), connection.callable)

				_variable_observers.erase(specific_variable_name)

	elif object != null && method_name != null:
		var keys_to_remove = []
		for observer_key in _variable_observers:
			var observer = _variable_observers[observer_key]
			if observer.is_connected("variable_changed", Callable(object, method_name)):
				observer.disconnect("variable_changed", Callable(object, method_name))

			if observer.get_signal_connection_list("variable_changed").is_empty():
				keys_to_remove.append(observer_key)

		for key in keys_to_remove:
			_variable_observers.erase(key)


func variable_state_did_change_event(variable_name: String, new_value_obj: InkObject) -> void:
	if _variable_observers == null:
		return

	if _variable_observers.has(variable_name):
		var observer = _variable_observers[variable_name]

		if !InkUtils.is_ink_class(new_value_obj, "Value"):
			InkUtils.throw_exception("Tried to get the value of a variable that isn't a standard type")
			return

		var val = new_value_obj

		observer.emit_signal("variable_changed", variable_name, val.value_object)


var global_tags: # Array<String>
	get:
		return self.tags_at_start_of_flow_container_with_path_string("")

# (String) -> Array<String>?
func tags_for_content_at_path(path: String):
	return self.tags_at_start_of_flow_container_with_path_string(path)

# (String) -> Array<String>?
func tags_at_start_of_flow_container_with_path_string(path_string: String):
	var path = InkPath.new_with_components_string(path_string)

	var flow_container = content_at_path(path).container
	while (true):
		var first_content = flow_container.content[0]
		if InkUtils.is_ink_class(first_content, "InkContainer"):
			flow_container = first_content
		else: break

	var in_tag := false
	var tags = null # Array<String>
	for c in flow_container.content:
		var command = InkUtils.as_or_null(c, "ControlCommand")
		if command != null:
			if command.command_type == InkControlCommand.CommandType.BEGIN_TAG:
				in_tag = true
			elif command.command_type == InkControlCommand.CommandType.END_TAG:
				in_tag = false
		elif in_tag:
			var _str = InkUtils.as_or_null(c, "StringValue")
			if _str != null:
				if tags == null:
					tags = [] # Array<String>
				tags.append(_str.value)
				print(str("\"", _str.value, "\""))
			else:
				self.error(str(
						"Tag contained non-text content. Only plain text is allowed when using ",
						"globalTags or TagsAtContentPath. If you want to evaluate dynamic ",
						"content, you need to use story.Continue()."
				))
		else:
			break

	return tags


func build_string_of_container() -> String:
	# TODO: Implement
	return ""


func build_string_of_container_with(container: InkContainer) -> String:
	# TODO: Implement
	return ""


func next_content() -> void:

	self.state.previous_pointer = self.state.current_pointer

	if !self.state.diverted_pointer.is_null:

		self.state.current_pointer = self.state.diverted_pointer
		self.state.diverted_pointer = InkPointer.null_pointer

		self.visit_changed_containers_due_to_divert()

		if !self.state.current_pointer.is_null:
			return

	var successful_pointer_increment = self.increment_content_pointer()

	if !successful_pointer_increment:
		var did_pop = false

		if self.state.callstack.can_pop_type(Ink.PushPopType.FUNCTION):

			self.state.pop_callstack(Ink.PushPopType.FUNCTION)

			if self.state.in_expression_evaluation:
				self.state.push_evaluation_stack(InkVoid.new())

			did_pop = true
		elif self.state.callstack.can_pop_thread:
			self.state.callstack.pop_thread()

			did_pop = true
		else:
			self.state.try_exit_function_evaluation_from_game()

		if did_pop && !self.state.current_pointer.is_null:
			self.next_content()


func increment_content_pointer() -> bool:
	var successful_increment = true

	var pointer = self.state.callstack.current_element.current_pointer
	pointer = InkPointer.new(pointer.container, pointer.index + 1)

	while pointer.index >= pointer.container.content.size():

		successful_increment = false

		var next_ancestor = InkUtils.as_or_null(pointer.container.parent, "InkContainer")
		if !next_ancestor:
			break

		var index_in_ancestor = next_ancestor.content.find(pointer.container)
		if index_in_ancestor == -1:
			break

		pointer = InkPointer.new(next_ancestor, index_in_ancestor + 1)

		successful_increment = true

	if !successful_increment: pointer = InkPointer.null_pointer

	var current_element = self.state.callstack.current_element
	current_element.current_pointer = pointer

	return successful_increment


func try_follow_default_invisible_choice() -> bool:
	var all_choices = _state.current_choices

	var invisible_choices = []
	for c in all_choices:
		if c.is_invisible_default:
			invisible_choices.append(c)

	if invisible_choices.size() == 0 || all_choices.size() > invisible_choices.size():
		return false

	var choice = invisible_choices[0]

	var callstack = self.state.callstack
	callstack.current_thread = choice.thread_at_generation

	if self._state_snapshot_at_last_newline != null:
		self.state.callstack.current_thread = self.state.callstack.fork_thread()

	choose_path(choice.target_path, false)

	return true


func next_sequence_shuffle_index() -> int:
	var num_elements_int_val = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "IntValue")
	if num_elements_int_val == null:
		error("expected number of elements in sequence for shuffle index")
		return 0

	var seq_container = self.state.current_pointer.container

	var num_elements = num_elements_int_val.value

	var seq_count_val = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "IntValue")
	var seq_count = seq_count_val.value
	var loop_index = seq_count / num_elements
	var iteration_index = seq_count % num_elements

	var seq_path_str = seq_container.path._to_string()
	var sequence_hash = 0
	for c in seq_path_str:
		sequence_hash += int(c)

	var random_seed = sequence_hash + loop_index + self.state.story_seed
	seed(random_seed)

	var unpicked_indices = [] # Array<int>
	var i = 0
	while (i < num_elements):
		unpicked_indices.append(i)
		i += 1

	i = 0
	while (i <= iteration_index):
		var chosen = randi() % unpicked_indices.size()
		var chosen_index = unpicked_indices[chosen]
		unpicked_indices.remove_at(chosen)

		if i == iteration_index:
			return chosen_index

		i += 1

	InkUtils.throw_exception("Should never reach here")
	return -1


# (String, bool) -> void
func error(message: String, use_end_line_number: bool = false) -> void:
	InkUtils.throw_story_exception(message, use_end_line_number, _make_story_error_metadata())


# (String) -> void
func warning(message: String) -> void:
	add_error(message, true)


# (String, bool, bool) -> void
func add_error(message: String, is_warning: bool = false, use_end_line_number: bool = false) -> void:
	# This method differs from upstream, because GDScript doesn't support exceptions.
	# Error formatting is handled by add_error_with_metadata, because there's a new
	# method `add_story_error` used by `continue_internal` to report errors
	# that occured during the step.
	_add_error_with_metadata(
		message,
		is_warning,
		use_end_line_number,
		self.current_debug_metadata,
		self.state.current_pointer
	)


# (StoryError) -> void
#
# This method doesn't exist in upstream. It's used by `continue_internal` to
# report error that occured during the step.
func add_story_error(story_error: StoryError) -> void:
	_add_error_with_metadata(
			story_error.message,
			false,
			story_error.use_end_line_number,
			story_error.metadata.debug_metadata,
			story_error.metadata.pointer
	)


# (bool, String?, Array<Variant>?) -> void
func __assert__(condition: bool, message = null, format_params = null) -> void:
	if condition == false:
		if message == null:
			message = "Story assert"

		if format_params != null && format_params.size() > 0:
			message = message % format_params

		if self.current_debug_metadata != null:
			InkUtils.throw_exception("%s %s" % [message, str(self.current_debug_metadata)])
		else:
			InkUtils.throw_exception(message)


var current_debug_metadata: InkDebugMetadata: get = get_current_debug_metadata
func get_current_debug_metadata() -> InkDebugMetadata:
	var dm # DebugMetadata

	var pointer = self.state.current_pointer
	if !pointer.is_null:
		dm = pointer.resolve().debug_metadata
		if dm != null:
			return dm

	var i = self.state.callstack.elements.size() - 1
	while (i >= 0):
		pointer = self.state.callstack.elements[i].current_pointer
		if !pointer.is_null && pointer.resolve() != null:
			dm = pointer.resolve().debug_metadata
			if dm != null:
				return dm

		i -= 1

	i = self.state.output_stream.size() - 1
	while(i >= 0):
		var output_obj = self.state.output_stream[i]
		dm = output_obj.debug_metadata
		if dm != null:
			return dm

		i -= 1

	return null


var current_line_number: int: get = get_current_line_number
func get_current_line_number() -> int:
	var dm = self.current_debug_metadata
	if dm != null:
		return dm.start_line_number

	return 0


# InkContainer?
var main_content_container : get = get_main_content_container
func get_main_content_container():
	if _temporary_evaluation_container:
		return _temporary_evaluation_container
	else:
		return _main_content_container


# InkContainer?
var _main_content_container = null
# ListDefinitionsOrigin?
var _list_definitions = null

# Dictionary<String, ExternalFunctionDef>?
var _externals = null
# Dictionary<String, VariableObserver>?
var _variable_observers = null

var _has_validated_externals: bool = false

# InkContainer?
var _temporary_evaluation_container = null

# StoryState?
var _state = null

var _async_continue_active: bool = false
# StoryState?
var _state_snapshot_at_last_newline = null
var _saw_lookahead_unsafe_function_after_newline: bool = false # bool

var _recursive_continue_count: int = 0

var _async_saving: bool = false

# Profiler?
var _profiler = null

# ############################################################################ #
# GDScript extra methods
# ############################################################################ #

func is_ink_class(type: String) -> bool:
	return type == "Story" || super.is_ink_class(type)


func get_ink_class() -> String:
	return "Story"


func connect_exception(target: Object, method: String, binds = [], flags = 0) -> int:
	var runtime = self._ink_runtime
	if runtime == null:
		return ERR_UNAVAILABLE

	if runtime.is_connected("exception_raised", Callable(target, method)):
		return OK

	return runtime.connect("exception_raised", Callable(target, method).bind(binds), flags)


func _enable_story_exception_recording(enable: bool) -> void:
	var runtime = self._ink_runtime
	if runtime != null:
		runtime.record_story_exceptions = enable


func _get_and_clear_recorded_story_exceptions() -> Array:
	var runtime = self._ink_runtime
	if runtime == null:
		return []

	var exceptions = runtime.current_story_exceptions
	runtime.current_story_exceptions = []

	return exceptions


func _add_error_with_metadata(
	message: String,
	is_warning: bool = false,
	use_end_line_number: bool = false,
	dm = null,
	current_pointer = InkPointer.null_pointer
) -> void:
	var error_type_str = "WARNING" if is_warning else "ERROR"

	if dm != null:
		var line_num = dm.end_line_number if use_end_line_number else dm.start_line_number
		message = "RUNTIME %s: '%s' line %s: %s" % [error_type_str, dm.file_name, line_num, message]
	elif !current_pointer.is_null:
		message = "RUNTIME %s: (%s): %s" % [error_type_str, current_pointer.path._to_string(), message]
	else:
		message = "RUNTIME " + error_type_str + ": " + message

	self.state.add_error(message, is_warning)

	if !is_warning:
		self.state.force_end()


func _throw_story_exception(message: String):
	InkUtils.throw_story_exception(message, false, _make_story_error_metadata())


# This method is used to ensure that the debug metadata and pointer used
# to report errors are the ones at the moment the error occured (and not
# the current one). Since GDScript doesn't have exceptions, errors may be
# stored until they can be processed at the end of `continue_internal`.
func _make_story_error_metadata():
	return StoryErrorMetadata.new(self.current_debug_metadata, self.state.current_pointer)


# ############################################################################ #

var StaticJSON: InkStaticJSON:
	get: return self._ink_runtime.json

var _ink_runtime:
	get: return _weak_ink_runtime.get_ref()
var _weak_ink_runtime: WeakRef

var _error_raised_during_step = []

func _initialize_runtime(ink_runtime = null):
	if ink_runtime != null:
		_weak_ink_runtime = weakref(ink_runtime)
	else:
		var runtime = Engine.get_main_loop().root.get_node("__InkRuntime")

		InkUtils.__assert__(
			runtime != null,
			str("[InkStory] Could not retrieve 'InkRuntime' singleton from the scene tree.")
		)

		_weak_ink_runtime = weakref(runtime)

# ############################################################################ #

class VariableObserver extends RefCounted:
	var variable_name: String

	signal variable_changed(variable_name, new_value)

	func _init(variable_name: String):
		self.variable_name = variable_name


class ExternalFunctionDef extends InkBase:
	var object: Object
	var method: String
	var lookahead_safe: bool

	func _init(object: Object, method: String, lookahead_safe: bool):
		# If object is not a reference, you're responsible to ensure it's
		# still allocated.
		self.object = weakref(object)
		self.method = method
		self.lookahead_safe = lookahead_safe

	# (Array<Variant>) -> Variant
	func execute(params: Array):
		var object_ref = object.get_ref()
		if object_ref != null:
			return object_ref.callv(method, params)
		else:
			InkUtils.throw_exception(
					"Object binded to %s has been deallocated, cannot execute." % method
			)
			return null
