extends Node2D

# TODO Break this script up! Children nodes shoudl hold all the logic and this should just manage them!

const USERNAME_TEXT_FOLLOW_SPEED_WEIGHT: float = 0.7
const MESSAGE_TEXT_FOLLOW_SPEED_WEIGHT: float = 0.05
const TEXT_OFFSET: float = 8.0
const TEXT_VERTICAL_OFFSET: float = 148.0


const PLACEHOLDER_ICON = preload("uid://co42n3lptjubb")
const CHATTER_TEMPLATE_MATERIAL = preload("uid://doly3cv8lr16s")

@export var typing_characters_per_second: float = 18.0
@export var emote_pause_seconds: float = 0.45
@export var emote_scale: int = 1

@onready var username_label: Label = $UsernameLabel
@onready var message_label: MessageLabel = $MessageTextLabel
@onready var soft_body_node = $SoftBodyNode
@onready var speaking_bounce_timer: Timer = $SpeakingBounceTimer
@onready var chatter_voice = $ChatterVoice

@onready var speaking_bounce_target = get_tree().get_first_node_in_group("bounce_target")
var soft_body_mesh: QMeshPolygonNode

var chatter_name: String = ""
var chatter_color: Color = Color.WHITE
var sphere_size: float = 64.0

var twitch_message: TwitchChatMessage
var media_loader: TwitchMediaLoader

var _typing_running: bool = false
var _prefetch_running: bool = false
var _prefetched_emotes: Dictionary = {}
var _warmup_holder: Control


func _ready():
	_setup_ui()
	_setup_signals()
	_setup_warmup_system()
	_init_softbody()
	_throw_into_position(true)

func _process(_delta: float):
	_update_ui_positions()


#region Public Methods
func init_chatter_data(chat_name: String = "USERNAME_ERROR", chat_color: Color = Color.RED):
	chatter_name = chat_name
	chatter_color = chat_color


func update_chatter_display(chat_name: String = "USERNAME_ERROR", chat_color: Color = Color.RED):
	username_label.text = chat_name
	username_label["theme_override_colors/font_color"] = chat_color


func set_twitch_message(message: TwitchChatMessage, loader: TwitchMediaLoader):
	twitch_message = message
	media_loader = loader

	_start_prefetch_and_warmup()

	# Wait before displaying message to give ball enough time to get into position
	await get_tree().create_timer(2.0).timeout
	_start_display()
#endregion


#region Initialization
func _setup_ui():
	message_label.pivot_offset = message_label.size * 0.5


func _setup_signals():
	speaking_bounce_timer.timeout.connect(_on_speaking_bounce_timer_timeout)
	chatter_voice.finished_speaking.connect(_send_behind_webcam)


## Sets up a offscreen control which holds the emotes prior to displaying them so that they are actually ready
## otherwise animated emotes would be 'frozen' when they spawn in and take a second to start playing
func _setup_warmup_system():
	_warmup_holder = Control.new()
	_warmup_holder.name = "EmoteWarmup"
	add_child(_warmup_holder)


func _init_softbody():
	var new_softbody = QMeshPolygonNode.new()
	new_softbody.polygon_radius = sphere_size
	new_softbody.side_count = 6
	new_softbody.enable_vector_rendering = true
	new_softbody.enable_stroke = true
	new_softbody.stroke_width = 11.5
	new_softbody.stroke_offset = 6.0
	new_softbody.enable_curved_corners = true
	new_softbody.curve_length = 20.0
	new_softbody.fill_texture = PLACEHOLDER_ICON
	new_softbody.material = CHATTER_TEMPLATE_MATERIAL.duplicate()

	soft_body_node.set_body_rotation(randf_range(0, TAU), false)
	soft_body_mesh = new_softbody
	soft_body_node.add_child(new_softbody)
#endregion


#region Prefetch Emotes (This whole region is needed to fix animated emotes not playing immediately when drawing)
func _start_prefetch_and_warmup():
	if _prefetch_running or !twitch_message or !media_loader:
		return

	_prefetch_running = true
	_clear_prefetched_emotes()
	call_deferred("_prefetch_and_warmup_async")


func _clear_prefetched_emotes():
	for n in _prefetched_emotes.values():
		if is_instance_valid(n):
			n.queue_free()
	_prefetched_emotes.clear()


func _prefetch_and_warmup_async():
	await twitch_message.load_emotes_from_fragment(media_loader)

	for i in twitch_message.message.fragments.size():
		var fragment = twitch_message.message.fragments[i]
		if fragment.type == TwitchChatMessage.FragmentType.emote:
			var sf: SpriteFrames = await fragment.emote.get_sprite_frames(media_loader, emote_scale)
			_prefetched_emotes[i] = _warm_emote_node(sf)
	_prefetch_running = false


func _warm_emote_node(sf: SpriteFrames) -> EmoteControl:
	var e := EmoteControl.new()
	e.frames = sf
	e.emote_scale = float(emote_scale)
	e.modulate.a = 0.0
	e.position = Vector2(-10000, -10000)

	_warmup_holder.add_child(e)
	return e
#endregion


#region Typing Logic
func _start_display():
	speaking_bounce_timer.start()
	if _typing_running: return

	_typing_running = true
	message_label.clear_all()

	if twitch_message and media_loader:
		await twitch_message.load_emotes_from_fragment(media_loader)
		await _type_fragments(twitch_message)

	_typing_running = false


func _type_fragments(chat_message: TwitchChatMessage):
	var typing_delay = 1.0 / max(typing_characters_per_second, 1.0)

	for fragment_index in chat_message.message.fragments.size():
		var fragment = chat_message.message.fragments[fragment_index]

		match fragment.type:
			TwitchChatMessage.FragmentType.text:
				await _append_text_slowly(HelperMethods.sanitize_links(fragment.text), typing_delay)

			TwitchChatMessage.FragmentType.mention:
				await _append_text_slowly("@" + fragment.mention.user_name, typing_delay)

			TwitchChatMessage.FragmentType.emote:
				var emote_control: EmoteControl = _prefetched_emotes.get(fragment_index, null)

				if !is_instance_valid(emote_control):
					var sprite_frames = await fragment.emote.get_sprite_frames(media_loader, emote_scale)
					emote_control = EmoteControl.new()
					emote_control.frames = sprite_frames
					emote_control.emote_scale = float(emote_scale)

				emote_control.modulate.a = 1.0
				emote_control.call_deferred("restart")
				message_label.append_emote(emote_control)

				await get_tree().create_timer(emote_pause_seconds).timeout

	message_label.finalize()


func _append_text_slowly(text: String, delay: float):
	for character in text:
		message_label.append_char(character)
		await get_tree().create_timer(delay).timeout
#endregion


#region Ball Display / Movement
func _update_ui_positions():
	if !soft_body_mesh: return

	var avg_pos = soft_body_mesh.get_average_position()

	# Update Username position
	var user_target = avg_pos - Vector2(username_label.size.x / 2, username_label.size.y / 2 + TEXT_OFFSET + sphere_size)
	username_label.global_position = username_label.global_position.lerp(user_target, USERNAME_TEXT_FOLLOW_SPEED_WEIGHT)

	# Update Message position
	var msg_target = avg_pos + Vector2(TEXT_OFFSET + sphere_size, -TEXT_VERTICAL_OFFSET)
	message_label.global_position = message_label.global_position.lerp(msg_target, MESSAGE_TEXT_FOLLOW_SPEED_WEIGHT)


func _throw_into_position(intro: bool):
	var force_dir = 18.0 if intro else -18.0
	for i in soft_body_mesh.get_particle_count():
		soft_body_mesh.get_particle_at(i).apply_force(Vector2(force_dir, 0))


func _on_speaking_bounce_timer_timeout():
	soft_body_node.set_collidable_layers_bit(56)
	speaking_bounce_timer.wait_time = randf_range(0.45, 0.85)

	var target_pos = speaking_bounce_target.global_position
	var particle_indices = range(soft_body_mesh.get_particle_count())

	# If short bounce, only move top particles (This will help rotate ball upright. Initial rotation is random)
	if speaking_bounce_timer.wait_time < 0.7:
		particle_indices = [4, 5]

	for i in particle_indices:
		var p = soft_body_mesh.get_particle_at(i)
		var direction = (target_pos - p.get_global_position()).normalized()
		p.apply_force(direction * 4.0)


func _send_behind_webcam():
	speaking_bounce_timer.stop()
	await get_tree().create_timer(1.0).timeout

	create_tween().tween_property(message_label, "modulate", Color.TRANSPARENT, 0.25)

	soft_body_node.set_collidable_layers_bit(24)
	_throw_into_position(false)

	await get_tree().create_timer(2.0).timeout
	queue_free()
#endregion
