@tool
@icon("res://addons/guide/guide_action.svg")
class_name GUIDEAction
extends Resource

enum GUIDEActionValueType {
	BOOL = 0,
	AXIS_1D = 1,
	AXIS_2D = 2,
	AXIS_3D = 3
}

enum GUIDEActionState {
	TRIGGERED,
	ONGOING,
	COMPLETED
}

## The name of this action. Required when this action should be used as
## Godot action. Also displayed in the debugger.
@export var name:StringName:
	set(value):
		if name == value:
			return
		name = value
		emit_changed()
	

## The action value type.
@export var action_value_type: GUIDEActionValueType = GUIDEActionValueType.BOOL:
	set(value):
		if action_value_type == value:
			return
		action_value_type = value
		emit_changed()
		
## If this action triggers, lower-priority actions cannot trigger 
## if they share input with this action unless these actions are
## chorded with this action.		
@export var block_lower_priority_actions:bool = true:
	set(value):
		if block_lower_priority_actions == value:
			return
		block_lower_priority_actions = value
		emit_changed()	


@export_category("Godot Actions")
## If true, then this action will be emitted into Godot's 
## built-in action system. This can be helpful to interact with
## code using this system, like Godot's UI system. Actions
## will be emitted on trigger and completion (e.g. button down
## and button up).
@export var emit_as_godot_actions:bool = false:
	set(value):
		if emit_as_godot_actions == value:
			return
		emit_as_godot_actions = value
		emit_changed()
		
		
@export_category("Action Remapping")

## If true, players can remap this action. To be remappable, make sure
## that a name and the action type are properly set.
@export var is_remappable:bool:
	set(value):
		if is_remappable == value:
			return
		is_remappable = value
		emit_changed()
		
## The display name of the action shown to the player.
@export var display_name:String:
	set(value):
		if display_name == value:
			return
		display_name = value
		emit_changed()

## The display category of the action shown to the player.
@export var display_category:String:
	set(value):
		if display_category == value:
			return
		display_category = value
		emit_changed()
		
## Emitted every frame while the action is triggered.
signal triggered()

## Emitted when the action started evaluating.
signal started()

## Emitted every frame while the action is still evaluating.
signal ongoing()

## Emitted when the action finished evaluating.
signal completed()

## Emitted when the action was cancelled.
signal cancelled()

var _last_state:GUIDEActionState = GUIDEActionState.COMPLETED

var _value_bool:bool
## Returns the value of this action as bool.
var value_bool:bool:
	get: return _value_bool

## Returns the value of this action as float.
var value_axis_1d:float:
	get: return _value.x
		
var _value_axis_2d:Vector2 = Vector2.ZERO
## Returns the value of this action as Vector2.
var value_axis_2d:Vector2:
	get: return _value_axis_2d

var _value:Vector3 = Vector3.ZERO
## Returns the value of this action as Vector3.
var value_axis_3d:Vector3:
	get: return _value
	

var _elapsed_seconds:float
## The amount of seconds elapsed since the action started evaluating.
var elapsed_seconds:float:
	get: return _elapsed_seconds

var _elapsed_ratio:float
## The ratio of the elapsed time to the hold time. This is a percentage
## of the hold time that has passed. If the action has no hold time, this will
## be 0 when the action is not triggered and 1 when the action is triggered.
## Otherwise, this will be a value between 0 and 1.
var elapsed_ratio:float:
	get: return _elapsed_ratio

var _triggered_seconds:float
## The amount of seconds elapsed since the action triggered.
var triggered_seconds:float:
	get: return _triggered_seconds


## This is a hint for how long the input must remain actuated (in seconds) before the action triggers.
## It depends on the mapping in which this action is used. If the mapping has no hold trigger it will be -1.
## In general, you should not access this variable directly, but rather the `elapsed_ratio` property of the action
## which is a percentage of the hold time that has passed.
var _trigger_hold_threshold:float = -1.0

func _triggered(value:Vector3, delta:float) -> void:
	_triggered_seconds += delta
	_elapsed_ratio = 1.0
	_update_value(value)
	_last_state = GUIDEActionState.TRIGGERED
	triggered.emit()
	_emit_godot_action_maybe(true)
		
func _started(value:Vector3) -> void:
	_elapsed_ratio = 0.0
	_update_value(value)
	_last_state = GUIDEActionState.ONGOING
	started.emit()
	ongoing.emit()

func _ongoing(value:Vector3, delta:float) -> void:
	_elapsed_seconds += delta
	if _trigger_hold_threshold > 0:
		_elapsed_ratio = _elapsed_seconds / _trigger_hold_threshold
	_update_value(value)
	var was_triggered:bool = _last_state == GUIDEActionState.TRIGGERED
	_last_state = GUIDEActionState.ONGOING
	ongoing.emit()
	# if the action reverts from triggered to ongoing, this counts as 
	# releasing the action for the godot action system.
	if was_triggered:
		_emit_godot_action_maybe(false)
	

func _cancelled(value:Vector3) -> void:
	_elapsed_seconds = 0
	_elapsed_ratio = 0
	_update_value(value)
	_last_state = GUIDEActionState.COMPLETED
	cancelled.emit()
	completed.emit()

func _completed(value:Vector3) -> void:
	_elapsed_seconds = 0
	_elapsed_ratio = 0
	_triggered_seconds = 0
	_update_value(value)
	_last_state = GUIDEActionState.COMPLETED
	completed.emit()
	_emit_godot_action_maybe(false)
		
func _emit_godot_action_maybe(pressed:bool) -> void:
	if not emit_as_godot_actions:
		return
		
	if name.is_empty():
		push_error("Cannot emit action into Godot's system because name is empty.")
		return
		
	var godot_action = InputEventAction.new()
	godot_action.action = name
	godot_action.strength = _value.x
	godot_action.pressed = pressed
	Input.parse_input_event(godot_action)

func _update_value(value:Vector3):
	match action_value_type:
		GUIDEActionValueType.BOOL, GUIDEActionValueType.AXIS_1D:
			_value_bool = abs(value.x) > 0
			_value_axis_2d = Vector2(abs(value.x), 0)
			_value = Vector3(value.x, 0, 0)
		GUIDEActionValueType.AXIS_2D:
			_value_bool = abs(value.x) > 0
			_value_axis_2d = Vector2(value.x, value.y)
			_value = Vector3(value.x, value.y, 0)
		GUIDEActionValueType.AXIS_3D:
			_value_bool = abs(value.x) > 0
			_value_axis_2d = Vector2(value.x, value.y)
			_value = value

## Returns whether the action is currently triggered. Can be used for a 
## polling style input.
func is_triggered() -> bool:
	return _last_state == GUIDEActionState.TRIGGERED

	
## Returns whether the action is currently completed. Can be used for a 
## polling style input.
func is_completed() -> bool:
	return _last_state == GUIDEActionState.COMPLETED


## Returns whether the action is currently completed. Can be used for a 
## polling style input.
func is_ongoing() -> bool:
	return _last_state == GUIDEActionState.ONGOING


func _editor_name() -> String:
	# Try to give the most user friendly name
	if display_name != "":
		return display_name
		
	if name != "":
		return name
		
	return resource_path.get_file().replace(".tres", "")


