extends Node
class_name StateMachine
## Base interface for a generic state machine
## It handles initializing, setting the machine active or not
## delegating _physics_process, _input calls to the State nodes,
## and changing the current/active state.

## Signal emitted when the state has changed. Parameter is the new current state.[br]
## [br]
## #### Parameters[br]
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |current_state|`Variant`|Instance of the state that became active.|yes|[br]
## [br]
signal state_changed(current_state)

## Starting state. This node has to be set *before* the initialize(START_STATE)
## command is called.
var START_STATE: Node

## List of states
var states_map = {}

## Stack of states
var states_stack = []  # can also be used as a pushdown automaton

## Value of the current state
var current_state = null

## Name of the current state
var current_state_name = ""

## Whether the state machine is currently enabled or not.
var _active = false:
	set = set_active


## Initialize the state machine with the start_state parameter.[br]
## [br]
## #### Parameters[br]
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |start_state|`State`|State value to use as starting state for the state machine.|yes|[br]
## [br]
## #### Returns[br]
## [br]
## Returns nothing.
func initialize(start_state: State):
	if START_STATE == null:
		escoria.logger.error(
			self,
			"Starting state is required to be initialized with a defined state,
			but it is null. Escoria cannot determine which of the defined states
			(in states_map dictionary) is supposed to be the starting one.
			Please assign a state to START_STATE in your implementation of the StateMachine class.")
	for child in get_children():
		child.connect("finished", Callable(self, "_change_state"))

	set_active(true)
	states_stack.push_front(start_state)
	current_state = states_stack[0]
	current_state.enter()


## Enable or disable the state machine.[br]
## [br]
## #### Parameters[br]
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |value|`bool`|if true, enables the state machine. If false, disables it.|yes|[br]
## [br]
## #### Returns[br]
## [br]
## Returns nothing.
func set_active(value: bool):
	_active = value
	set_physics_process(value)
	set_process_input(value)
	if not _active:
		states_stack = []
		current_state = null


# Manage an input event by the state machine's current state.
#
# #### Parameters
#
# - event: InputEvent to manage.
func _input(event: InputEvent):
	current_state.handle_input(event)


# Lets the state machine's current state perform an update during
# _physics_process() phase.
#
# #### Parameters
#
# - delta: float value corresponding to the elapsed time since last frame update.
func _physics_process(delta: float):
	current_state.update(delta)


# Lets the state machine's current state perform an action on animation_finished
# signal.
#
# #### Parameters
#
# - anim_name: name of the animation that finished.
func _on_animation_finished(anim_name: String):
	if not _active:
		return
	current_state._on_animation_finished(anim_name)


# Change the current state of the state machine using its name. The value of
# the state to be set is obtained in states_map dictionary.
#
# #### Parameters
#
# - state_name: name of the state to set.
func _change_state(state_name: String):
	if not _active:
		return

	escoria.logger.trace(
		self,
		"Dialog State Machine: Changing state from '%s' to '%s'." % [current_state_name, state_name]
	)

	current_state.exit()

	if state_name == "previous":
		states_stack.pop_front()
	else:
		states_stack[0] = states_map[state_name]

	current_state = states_stack[0]

	state_changed.emit(current_state)

	#if state_name != "previous":
	current_state.enter()

	current_state_name = state_name
