extends Control
class_name MessageLabel


@export var theme_to_use: Theme
@export var wrap_enabled: bool = true
@export var line_spacing: float = 2.0
@export var box_size: Vector2 = Vector2(650, 140)

@export var horizontal_alignment: HorizontalAlignment = HORIZONTAL_ALIGNMENT_LEFT
@export var vertical_alignment: VerticalAlignment = VERTICAL_ALIGNMENT_CENTER
@export var override_font_color: Color = Color(0, 0, 0, 0)

var _font: Font
var _font_size: int = 16
var _font_color: Color = Color.WHITE

var _shadow_color: Color = Color(0, 0, 0, 0)
var _shadow_outline_size: int = 0
var _shadow_offset: Vector2 = Vector2.ZERO

var _font_ascent: float = 0.0
var _line_height: float = 0.0

var _lines: Array = []               # This can contain both text/emote elements
var _line_widths: Array[float] = []  # used for calculateing new lines


func _ready() -> void:
	_refresh_theme_metrics()
	custom_minimum_size = box_size
	size = box_size
	clear_all()

func _notification(what: int) -> void:
	if what == NOTIFICATION_THEME_CHANGED:
		_refresh_theme_metrics()
		_relayout_all()
		queue_redraw()
	elif what == NOTIFICATION_RESIZED:
		_relayout_all()
		queue_redraw()

#region public methods
func clear_all() -> void:
	_clear_emotes()
	_lines = [[]]
	_line_widths = [0.0]
	queue_redraw()

func append_char(character: String) -> void:
	if character == "\n":
		_new_line()
	else:
		_append_text_to_current_run(character)
		if wrap_enabled:
			_fix_overflow()
	queue_redraw()

func append_text(text: String) -> void:
	for i in range(text.length()):
		append_char(text.substr(i, 1))

func append_emote(emote: Control) -> void:
	_break_run()
	_place_segment(emote)
	if wrap_enabled:
		_fix_overflow()
	queue_redraw()

func finalize() -> void:
	queue_redraw()
#endregion


func _refresh_theme_metrics() -> void:
	if theme_to_use:
		theme = theme_to_use

	_font = get_theme_font("font", "Label")
	_font_size = get_theme_font_size("font_size", "Label")

	if _font == null:
		_font = ThemeDB.fallback_font
		_font_size = ThemeDB.fallback_font_size

	_font_color = get_theme_color("font_color", "Label")
	_shadow_color = get_theme_color("font_shadow_color", "Label")
	_shadow_outline_size = get_theme_constant("shadow_outline_size", "Label")
	_shadow_offset = Vector2(float(get_theme_constant("shadow_offset_x", "Label")),float(get_theme_constant("shadow_offset_y", "Label")))

	if override_font_color.a > 0.0:
		_font_color = override_font_color

	_font_ascent = _font.get_ascent(_font_size)
	_line_height = _font.get_height(_font_size) + line_spacing

func _clear_emotes() -> void:
	for child in get_children():
		if child is Control:
			child.queue_free()

func _new_line() -> void:
	_lines.append([])
	_line_widths.append(0.0)

func _break_run() -> void:
	if _lines.is_empty():
		_new_line()

func _append_text_to_current_run(character: String) -> void:
	if _lines.is_empty():
		_new_line()

	var current_line: Array = _lines[-1]

	if current_line.is_empty() or !(current_line[-1] is String):
		current_line.append("")

	current_line[-1] += character
	_line_widths[-1] = _measure_line_width(current_line)

func _place_segment(segment) -> void:
	if _lines.is_empty():
		_new_line()

	_lines[-1].append(segment)

	if segment is Control:
		# Ensure the control is parented correctly. Works fine without but safer
		if segment.get_parent() != self:
			if segment.get_parent():
				segment.get_parent().remove_child(segment)
			add_child(segment)
		segment.visible = true

	_line_widths[-1] = _measure_line_width(_lines[-1])


func _measure_text(text: String) -> float:
	if text.is_empty():
		return 0.0
	var text_line := TextLine.new()
	text_line.add_string(text, _font, _font_size)
	return text_line.get_size().x

func _measure_segment(segment) -> float:
	if segment is String:
		return _measure_text(segment)
	elif segment is Control:
		return segment.get_combined_minimum_size().x
	return 0.0

func _measure_line_width(line_segments: Array) -> float:
	var width := 0.0
	for segment in line_segments:
		width += _measure_segment(segment)
	return width


func _relayout_all() -> void:
	var all_segments: Array = []
	for line_segments in _lines:
		for segment in line_segments:
			all_segments.append(segment)

	_lines = [[]]
	_line_widths = [0.0]

	for segment in all_segments:
		if segment is String:
			for i in range(segment.length()):
				append_char(segment.substr(i, 1))
		elif segment is Control:
			append_emote(segment)


func _fix_overflow() -> void:
	var max_width := size.x
	if max_width <= 1.0:
		return

	while _line_widths[-1] > max_width and !_lines[-1].is_empty():
		var current_line: Array = _lines[-1]

		# If last segment is a control/emotecontainer, move it whole to the next line.
		if current_line[-1] is Control:
			var moved_segment = current_line.pop_back()
			_line_widths[-1] = _measure_line_width(current_line)
			_new_line()
			_lines[-1].append(moved_segment)
			_line_widths[-1] = _measure_line_width(_lines[-1])
			continue

		# Try to wrap at the last space.
		var wrap_point = _find_last_space_wrap_point(current_line)
		if wrap_point != null:
			var moved_segments := _split_line_at_space(current_line, wrap_point)
			_line_widths[-1] = _measure_line_width(current_line)
			_new_line()
			_lines[-1] = moved_segments
			_line_widths[-1] = _measure_line_width(moved_segments)
			continue

		var moved_unit = _pop_last_visible_unit(current_line)
		_line_widths[-1] = _measure_line_width(current_line)
		_new_line()
		if moved_unit != null:
			_lines[-1].append(moved_unit)
			_line_widths[-1] = _measure_line_width(_lines[-1])

func _find_last_space_wrap_point(line_segments: Array):
	for segment_index in range(line_segments.size() - 1, -1, -1):
		var segment = line_segments[segment_index]
		if segment is String:
			var char_index = segment.rfind(" ")
			if char_index != -1:
				return {"segment_index": segment_index, "char_index": char_index}
	return null


func _split_line_at_space(line_segments: Array, wrap_point: Dictionary) -> Array:
	var segment_index: int = wrap_point["segment_index"]
	var char_index: int = wrap_point["char_index"]
	var moved_segments: Array = []

	var text_segment: String = line_segments[segment_index]
	var left_text := text_segment.substr(0, char_index)
	var right_text := text_segment.substr(char_index + 1)

	line_segments[segment_index] = left_text

	for j in range(segment_index + 1, line_segments.size()):
		moved_segments.append(line_segments[j])

	line_segments.resize(segment_index + 1)

	if !right_text.is_empty():
		moved_segments.insert(0, right_text)

	if line_segments.size() > 0 and (line_segments[-1] is String) and (line_segments[-1] as String).is_empty():
		line_segments.pop_back()

	return moved_segments

func _pop_last_visible_unit(line_segments: Array):
	if line_segments.is_empty():
		return null

	var last_segment = line_segments[-1]

	if last_segment is Control:
		return line_segments.pop_back()

	if last_segment is String:
		var last_text: String = last_segment
		if last_text.length() <= 1:
			return line_segments.pop_back()

		var last_character := last_text.substr(last_text.length() - 1, 1)
		line_segments[-1] = last_text.substr(0, last_text.length() - 1)
		return last_character

	return null

func _draw() -> void:
	var total_height := float(_lines.size()) * _line_height
	var base_y := 0.0

	match vertical_alignment:
		VERTICAL_ALIGNMENT_TOP:
			base_y = _font_ascent
		VERTICAL_ALIGNMENT_BOTTOM:
			base_y = size.y - total_height + _font_ascent
		_:
			base_y = (size.y - total_height) * 0.5 + _font_ascent

	var y := base_y
	for line_index in range(_lines.size()):
		var line_segments: Array = _lines[line_index]
		var line_width := _line_widths[line_index]
		var x := 0.0

		match horizontal_alignment:
			HORIZONTAL_ALIGNMENT_CENTER:
				x = max(0.0, (size.x - line_width) * 0.5)
			HORIZONTAL_ALIGNMENT_RIGHT:
				x = max(0.0, size.x - line_width)
			_:
				x = 0.0

		for segment in line_segments:
			if segment is String:
				var text_segment: String = segment
				if !text_segment.is_empty():
					_draw_themed_string(Vector2(x, y), text_segment)
					x += _measure_text(text_segment)
			elif segment is Control:
				var control_segment: Control = segment
				var minimum_size := control_segment.get_combined_minimum_size()
				control_segment.position = Vector2(
					x,
					y - _font_ascent + (_line_height - minimum_size.y) * 0.5
				)
				x += minimum_size.x

		y += _line_height


# This is stupid but fixes the weird bug with using Twitcher's recommended way of displaying emotes in labels via bbcode tags and using visibile_ratio property
# Handling it all manually including the text drawing gives enough control to make everything work. This was a pain though. DinoDance makes it worth it
func _draw_themed_string(text_position: Vector2, text: String) -> void:
	if _shadow_color.a > 0.0 and _shadow_offset != Vector2.ZERO:
		var shadow_position := text_position + _shadow_offset
		if _shadow_outline_size > 0:
			draw_string_outline(
				_font,
				shadow_position,
				text,
				HORIZONTAL_ALIGNMENT_LEFT,
				-1.0,
				_font_size,
				_shadow_outline_size,
				_shadow_color
			)
		draw_string(
			_font,
			shadow_position,
			text,
			HORIZONTAL_ALIGNMENT_LEFT,
			-1.0,
			_font_size,
			_shadow_color
		)

	draw_string(_font, text_position, text, HORIZONTAL_ALIGNMENT_LEFT, -1.0, _font_size, _font_color)
