# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# warning-ignore-all:return_value_discarded
# ############################################################################ #
# 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 InkBase

class_name InkVariablesState

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

var InkTryGetResult := preload("res://addons/inkgd/runtime/extra/try_get_result.gd") as GDScript
var InkStringSet := preload("res://addons/inkgd/runtime/extra/string_set.gd") as GDScript

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

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

# (String, InkObject)
signal variable_changed(variable_name, new_value)

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

var patch: InkStatePatch # StatePatch

var batch_observing_variable_changes: bool:
	get:
		return _batch_observing_variable_changes

	set(value):
		_batch_observing_variable_changes = value
		if value:
			_changed_variables_for_batch_obs = InkStringSet.new()
		else:
			if _changed_variables_for_batch_obs != null:
				for variable_name in _changed_variables_for_batch_obs.enumerate():
					var current_value = _global_variables[variable_name]
					emit_signal("variable_changed", variable_name, current_value)

			_changed_variables_for_batch_obs = null

var _batch_observing_variable_changes: bool = false

var callstack: InkCallStack: get = get_callstack, set = set_callstack
func get_callstack() -> InkCallStack:
		return _callstack

func set_callstack(value: InkCallStack):
		_callstack = value

# (String) -> Variant
func get_variable(variable_name: String):
	if self.patch != null:
		var global: InkTryGetResult = patch.try_get_global(variable_name)
		if global.exists:
			return global.result.value_object

	if _global_variables.has(variable_name):
		return _global_variables[variable_name].value_object
	elif _default_global_variables.has(variable_name):
		return _default_global_variables[variable_name].value_object
	else:
		return null

# (String, Variant) -> void
func set_variable(variable_name: String, value) -> void:
	if !_default_global_variables.has(variable_name):
		InkUtils.throw_story_exception(
				"Cannot assign to a variable (%s) that hasn't been declared in the story" \
				% variable_name
		)
		return

	var val: InkValue = InkValue.create(value)
	if val == null:
		if value == null:
			InkUtils.throw_exception("Cannot pass null to VariableState")
		else:
			InkUtils.throw_exception("Invalid value passed to VariableState: %s" % str(value))
		return

	set_global(variable_name, val)

func enumerate() -> Array:
	return _global_variables.keys()

func _init(callstack: InkCallStack, list_defs_origin: InkListDefinitionsOrigin, ink_runtime = null):
	find_static_objects(ink_runtime)

	_global_variables = {}
	_callstack = callstack
	_list_defs_origin = list_defs_origin

# () -> void
func apply_patch() -> void:
	for named_var_key in self.patch.globals:
		_global_variables[named_var_key] = self.patch.globals[named_var_key]

	if _changed_variables_for_batch_obs != null:
		for name in self.patch.changed_variables.enumerate():
			_changed_variables_for_batch_obs.append(name)

	patch = null

# (Dictionary<string, Variant>) -> void
func set_json_token(jtoken: Dictionary) -> void:
	_global_variables.clear()

	for var_val_key in _default_global_variables:
		if jtoken.has(var_val_key):
			var loaded_token = jtoken[var_val_key]
			_global_variables[var_val_key] = self.StaticJSON.jtoken_to_runtime_object(loaded_token)
		else:
			_global_variables[var_val_key] = _default_global_variables[var_val_key]

func write_json(writer: InkSimpleJSON.Writer) -> void:
	writer.write_object_start()
	for key in _global_variables:
		var name: String = key
		var val: InkObject = _global_variables[key]

		if self._ink_runtime.dont_save_default_values:
			if self._default_global_variables.has(name):
				if runtime_objects_equal(val, self._default_global_variables[name]):
					continue

		writer.write_property_start(name)
		self.StaticJSON.write_runtime_object(writer, val)
		writer.write_property_end()
	writer.write_object_end()

func runtime_objects_equal(obj1: InkObject, obj2: InkObject) -> bool:
	if !InkUtils.are_of_same_type(obj1, obj2):
		return false

	var bool_val: InkBoolValue = InkUtils.as_or_null(obj1, "BoolValue")
	if bool_val != null:
		return bool_val.value == InkUtils.cast(obj2, "BoolValue").value

	var int_val: InkIntValue = InkUtils.as_or_null(obj1, "IntValue")
	if int_val != null:
		return int_val.value == InkUtils.cast(obj2, "IntValue").value

	var float_val: InkFloatValue = InkUtils.as_or_null(obj1, "FloatValue")
	if float_val != null:
		return float_val.value == InkUtils.cast(obj2, "FloatValue").value

	var val1: InkValue = InkUtils.as_or_null(obj1, "Value")
	var val2: InkValue = InkUtils.as_or_null(obj2, "Value")

	if val1 != null:
		if val1.value_object is Object && val2.value_object is Object:
			return val1.value_object.equals(val2.value_object)
		else:
			return val1.value_object == val2.value_object

	InkUtils.throw_exception(
			"FastRoughDefinitelyEquals: Unsupported runtime object type: %s" \
			% obj1.get_ink_class()
	)

	return false

func get_variable_with_name(name: String, context_index = -1) -> InkObject:
	var var_value: InkObject = get_raw_variable_with_name(name, context_index)

	var var_pointer: InkVariablePointerValue = InkUtils.as_or_null(var_value, "VariablePointerValue")
	if var_pointer:
		var_value = value_at_variable_pointer(var_pointer)

	return var_value

# (String) -> { exists: bool, result: InkObject }
func try_get_default_variable_value(name: String) -> InkTryGetResult:
	if _default_global_variables.has(name):
		return InkTryGetResult.new(true, _default_global_variables[name])
	else:
		return InkTryGetResult.new(false, null)

func global_variable_exists_with_name(name: String) -> bool:
	return (
			_global_variables.has(name) ||
			_default_global_variables != null && _default_global_variables.has(name)
	)

func get_raw_variable_with_name(name: String, context_index: int) -> InkObject:
	var var_value: InkObject = null

	if context_index == 0 || context_index == -1:
		if self.patch != null:
			var try_result: InkTryGetResult = self.patch.try_get_global(name)
			if try_result.exists: return try_result.result

		if _global_variables.has(name):
			return _global_variables[name]

		if self._default_global_variables != null:
			if self._default_global_variables.has(name):
				return self._default_global_variables[name]

		var list_item_value: InkListValue = _list_defs_origin.find_single_item_list_with_name(name)

		if list_item_value:
			return list_item_value

	var_value = _callstack.get_temporary_variable_with_name(name, context_index)

	return var_value

# (InkVariablePointerValue) -> InkObject
func value_at_variable_pointer(pointer: InkVariablePointerValue) -> InkObject:
	return get_variable_with_name(pointer.variable_name, pointer.context_index)

# (InkVariableAssignment, InkObject) -> void
func assign(var_ass: InkVariableAssignment, value: InkObject) -> void:
	var name: String = var_ass.variable_name
	var context_index: int = -1

	var set_global: bool = false
	if var_ass.is_new_declaration:
		set_global = var_ass.is_global
	else:
		set_global = global_variable_exists_with_name(name)

	if var_ass.is_new_declaration:
		var var_pointer: InkVariablePointerValue = InkUtils.as_or_null(value, "VariablePointerValue")
		if var_pointer:
			var fully_resolved_variable_pointer: InkObject = resolve_variable_pointer(var_pointer)
			value = fully_resolved_variable_pointer
	else:
		var existing_pointer: InkVariablePointerValue = null # VariablePointerValue
		var first_time: bool = true
		while existing_pointer || first_time:
			first_time = false
			existing_pointer = InkUtils.as_or_null(
				get_raw_variable_with_name(name, context_index),
				"VariablePointerValue"
			)
			if existing_pointer:
				name = existing_pointer.variable_name
				context_index = existing_pointer.context_index
				set_global = (context_index == 0)

	if set_global:
		set_global(name, value)
	else:
		_callstack.set_temporary_variable(name, value, var_ass.is_new_declaration, context_index)

# () -> void
func snapshot_default_globals():
	_default_global_variables = _global_variables.duplicate()

# (InkObject, InkObject) -> void
func retain_list_origins_for_assignment(old_value, new_value) -> void:
	var old_list: InkListValue = InkUtils.as_or_null(old_value, "ListValue")
	var new_list: InkListValue = InkUtils.as_or_null(new_value, "ListValue")

	if old_list && new_list && new_list.value.size() == 0:
		new_list.value.set_initial_origin_names(old_list.value.origin_names)

# (String, InkObject) -> void
func set_global(variable_name: String, value: InkObject) -> void:
	var old_value = null # InkObject

	# Slightly different structure from upstream, since we can't use
	# try_get_global in the conditional.
	if patch != null:
		var patch_value: InkTryGetResult = patch.try_get_global(variable_name)
		if patch_value.exists:
			old_value = patch_value.result

	if old_value == null:
		if self._global_variables.has(variable_name):
			old_value = self._global_variables[variable_name]

	InkListValue.retain_list_origins_for_assignment(old_value, value)

	if patch != null:
		self.patch.set_global(variable_name, value)
	else:
		self._global_variables[variable_name] = value

	if !value.equals(old_value):
		if _batch_observing_variable_changes:
			if patch != null:
				patch.add_changed_variable(variable_name)
			elif self._changed_variables_for_batch_obs != null:
				_changed_variables_for_batch_obs.append(variable_name)
		else:
			emit_signal("variable_changed", variable_name, value)

# (VariablePointerValue) -> VariablePointerValue
func resolve_variable_pointer(var_pointer: InkVariablePointerValue) -> InkVariablePointerValue:
	var context_index: int = var_pointer.context_index

	if context_index == -1:
		context_index = get_context_index_of_variable_named(var_pointer.variable_name)

	var value_of_variable_pointed_to = get_raw_variable_with_name(
		var_pointer.variable_name, context_index
	)

	var double_redirection_pointer: InkVariablePointerValue = InkUtils.as_or_null(
			value_of_variable_pointed_to, "VariablePointerValue"
	)

	if double_redirection_pointer:
		return double_redirection_pointer
	else:
		return InkVariablePointerValue.new_with_context(var_pointer.variable_name, context_index)

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

# (String) -> int
func get_context_index_of_variable_named(var_name):
	if global_variable_exists_with_name(var_name):
		return 0

	return _callstack.current_element_index

# Dictionary<String, InkObject>
var _global_variables: Dictionary
var _default_global_variables = null # Dictionary<String, InkObject>

var _callstack: InkCallStack
var _changed_variables_for_batch_obs: InkStringSet = null
var _list_defs_origin: InkListDefinitionsOrigin

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

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

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

# ############################################################################ #
# Static Properties
# ############################################################################ #

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

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

func find_static_objects(ink_runtime = null):
	if ink_runtime != null:
		_weak_ink_runtime = weakref(ink_runtime)
		return

	var runtime = Engine.get_main_loop().root.get_node("__InkRuntime")

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

	_weak_ink_runtime = weakref(runtime)
