class_name StateMachine extends Node
## Can be in one of a set of states. Dispatches signals on tick or transition.

## Stores signals for subscribers interested in specific states.
class State:
	## Emitted when entering this state.
	signal started
	## Emitted when the StateMachine is ticked while in this state.
	signal ticked(delta: float)
	## Emitted when exiting this state.
	signal stopped

## Stores subscriber connection information for all signals of one state.
class Handler:
	## Subscribes to State.started.
	var on_state_started: Callable = func () -> void: pass
	## Subscribes to State.ticked.
	var on_state_ticked: Callable = func (_delta: float) -> void: pass
	## Subscribes to State.stopped.
	var on_state_stopped: Callable = func () -> void: pass
	## How to connect to State.started.
	var state_started_flags: int
	## How to connect to State.ticked.
	var state_ticked_flags: int
	## How to connect to State.stopped.
	var state_stopped_flags: int
	func linkup(
		param: Variant,
		param2: Variant = null,
		param3: Variant = null
	) -> void:
		if param is Callable:
			param3 = param2
			param2 = param
			param = &'ticked'
		if param is StringName:
			if param == &'started':
				on_state_started = param2
				if param3 is int:
					state_started_flags = param3
			elif param == &'ticked':
				on_state_ticked = param2
				if param3 is int:
					state_ticked_flags = param3
			elif param == &'stopped':
				on_state_stopped = param2
				if param3 is int:
					state_stopped_flags = param3
		elif param is Array:
			callv(&'linkup', param)
		elif param is Dictionary:
			for sig in param:
				var rest = param[sig]
				if rest is Callable:
					linkup(sig, rest)
				elif rest is Array:
					rest = rest.duplicate()
					rest.push_front(sig)
					callv(&'linkup', rest)
				else:
					push_error("StateMachine.Handler invalid param %s" % param)
		elif param is Handler:
			on_state_started = param.on_state_started
			on_state_ticked = param.on_state_ticked
			on_state_stopped = param.on_state_stopped
			state_started_flags = param.state_started_flags
			state_ticked_flags = param.state_ticked_flags
			state_stopped_flags = param.state_stopped_flags
		else:
			push_error("StateMachine.Handler invalid param %s" % param)
	func _init(
		param: Variant,
		param2: Variant = null,
		param3: Variant = null
	) -> void:
		linkup(param, param2, param3)

static func _make_handler_dict(
	descriptors: Dictionary[StringName, Variant]
) -> Dictionary[StringName, Handler]:
	var result: Dictionary[StringName, Handler] = {}
	for state_name in descriptors:
		result[state_name] = Handler.new(descriptors[state_name])
	return result

## Emitted when starting any state (in addition to State.started).
## [br][br]
## The state argument is the name of the state that is starting.
signal state_changed(state: StringName)

var _states: Dictionary[StringName, State] = {}
var _state: StringName = &''

## Current state. Setting will emit all relevant signals.
var state: StringName:
	get():
		return _state
	set(state):
		if _state != state:
			if _state:
				_ensure_state_exists(_state)
				_states[_state].stopped.emit()
			_state = state
			state_changed.emit(_state)
			if _state:
				_ensure_state_exists(_state)
				_states[_state].started.emit()

func _ensure_state_exists(state_name: StringName) -> void:
	if !(state_name in _states):
		_states[state_name] = State.new()

## Creates described handlers and subscribes them to corresponding states.
## [br][br]
## A handler descriptor is a key-value pair in the passed Dictionary.
## The key part is a StringName referring to any state,
## and the value part is either a Callable, an Array, or a further Dictionary.
## The value specifies what Callable(s) to connect to the state's signals,
## what signal(s) to connect them to, and what connection flags to use.
## [br][br]
## If the value part of the key-value pair is a Callable, it is connected
## to the signal State.ticked, and no connection flags are used.
## [br][br]
## If the value part of the key-value pair is an Array, it must contain
## optionally a StringName which defaults to &'ticked' if not provided,
## mandatorily a Callable, and optionally an int which defaults to 0
## if not provided, in that order; these, respectively, are the signal name
## to connect to, the Callable to connect to it, and the connection flags
## to use.
## [br][br]
## If the value part of the key-value pair is an additional Dictionary,
## then for each key-value pair of that subordinate Dictionary,
## the key part is a StringName referring to a signal member of State
## (either &'started', &'ticked', or &'stopped') and specifies which signal
## to connect the value part to, and the value part is either a Callable
## or an array containing a Callable and an int indicating connection flags
## to use.
## [br][br]
## Any Callable which is ultimately to be connected to Signal.started
## or Signal.stopped should take no arguments and return nothing.
## Any Callable which is ultimately to be connected to Signal.ticked
## should take one float and return nothing. The float will be the time delta
## of the physics step.
## [br][br]
## If you are interested in performing some uniform action
## whenever any state starts, no matter which it is,
## prefer to subscribe to StateMachine.state_changed.
func connect_handlers(
	handler_descriptors: Dictionary[StringName, Variant]
) -> void:
	var handlers := _make_handler_dict(handler_descriptors)
	for state_name in handlers:
		_ensure_state_exists(state_name)
		var state_obj := _states[state_name]
		var handler := handlers[state_name]
		state_obj.started.connect(
			handler.on_state_started,
			handler.state_started_flags
		)
		state_obj.ticked.connect(
			handler.on_state_ticked,
			handler.state_ticked_flags
		)
		state_obj.stopped.connect(
			handler.on_state_stopped,
			handler.state_stopped_flags
		)

## Unsubscribes described handlers from corresponding states.
## [br][br]
## For information on the expected format of the handler_descriptors parameter,
## see documentation for connect_handlers.
## [br][br]
## It is recommended you build the handler_descriptors parameter only once,
## and use the same Dictionary instance to disconnect as you did to connect,
## in order to ensure the contained Callable instances being disconnected
## are the same instances that were initially connected --
## thus ensuring they are actually disconnected, and not left dangling
## due to failure to find them among the current connections.
func disconnect_handlers(
	handler_descriptors: Dictionary[StringName, Variant]
) -> void:
	var handlers := _make_handler_dict(handler_descriptors)
	for state_name in handlers:
		_ensure_state_exists(state_name)
		var state_obj := _states[state_name]
		var handler := handlers[state_name]
		state_obj.started.disconnect(handler.on_state_started)
		state_obj.ticked.disconnect(handler.on_state_ticked)
		state_obj.stopped.disconnect(handler.on_state_stopped)

func _physics_process(delta: float) -> void:
	if _state:
		_ensure_state_exists(_state)
		_states[_state].ticked.emit(delta)
