@tool
class_name TypewriterEffect
extends RichTextEffect
## BBCode effect which procedurally shows the text character by character, from start to end.
## Tag params:
## - speed - Speed at which text is displayed, in characters per second.
## - delay - Delay before displaying first character, in seconds.
## - factor - Transition animation timescale factor.

signal typing

const EDITOR_DEFAULT_SPEED = 20.0

@export var scale_curve: Curve
@export var translation_curve: CurveXYZTexture

@export var glyph_size_factor: float = 0.0

# To use this effect:
# - Enable BBCode on a RichTextLabel.
# - Instead of instantiating this effect directly, use a `TypewriterLabel` node.
# - Animation curves are exported by `TypewriterLabel`.
# - Use [type speed=10.0 delay=0.0 factor=1.0]hello[/type] in text.
var bbcode: String = "type"

var _force_visible := false


func _init(force_visible_signal: Variant = null) -> void:
	if force_visible_signal is Signal:
		@warning_ignore("unsafe_cast")
		(force_visible_signal as Signal).connect(_set_force_visible)


func _get_default_speed() -> float:
	if Engine.is_editor_hint():
		return EDITOR_DEFAULT_SPEED
	return Game.settings.default_text_speed


func _process_custom_fx(char_fx: CharFXTransform) -> bool:
	if not _force_visible:
		var speed: float = char_fx.env.get("speed", _get_default_speed())
		var delay: float = char_fx.env.get("delay", 0.0)
		var factor: float = char_fx.env.get("factor", 1.0)

		var rel_time := (speed * (char_fx.elapsed_time - delay) - char_fx.relative_index) / factor

		char_fx.visible = rel_time > 0

		if scale_curve or translation_curve:
			var server := TextServerManager.get_primary_interface()
			var glyph_size := server.font_get_glyph_size(
				char_fx.font, 0.5 * glyph_size_factor * Vector2i.ONE, char_fx.glyph_index
			)
			var pivot := glyph_size * Vector2(-1, 1)

			var scale := Vector2.ONE
			if scale_curve:
				scale *= scale_curve.sample_baked(rel_time)

			var translation := Vector2.ZERO
			if translation_curve:
				if translation_curve.curve_x:
					translation.x = translation_curve.curve_x.sample_baked(rel_time)
				if translation_curve.curve_y:
					translation.y = translation_curve.curve_y.sample_baked(rel_time)

			char_fx.transform = (
				char_fx
				. transform
				. translated_local(-pivot)
				. scaled_local(scale)
				. translated_local(pivot)
				. translated_local(translation)
			)

		if rel_time < 1:
			typing.emit()

	return true


func _set_force_visible() -> void:
	_force_visible = true
