class_name ButtonSpinBox
extends Range

# This class should come with a scene like this:
# ButtonSpinBox    [this class]
#  ├ HBox          [type HBoxContainer]
#  │  ├ BtnL       [type Button]
#  │  ├ Content    [type HBoxContainer]
#  │  │  ├ Icon    [type TextureRect]
#  │  │  └ Label   [type Label]
#  │  └ BtnR       [type Button]
#  └ Timer         [type Timer]
# Adjust scene and code to your needs (layout, ui theme, preferences, workflow).

enum SpinBoxType { VALUE, ITEMS }
@export var spinbox_type: SpinBoxType = SpinBoxType.VALUE:
	get = get_spinbox_type, set = set_spinbox_type
@export var alignment: BoxContainer.AlignmentMode = BoxContainer.AlignmentMode.ALIGNMENT_CENTER
@export_group("Value SpinBox")
@export var value_format: String = "%.1f" # https://docs.godotengine.org/en/latest/tutorials/scripting/gdscript/gdscript_format_string.html#padding
@export var value_multi: float = 1.0
@export var prefix: String = ""
@export var suffix: String = ""
@export_group("Item SpinBox")
@export var items: Array[ButtonSpinBoxItem]
@onready var btnl: Button = $HBox/BtnL
@onready var btnr: Button = $HBox/BtnR
@onready var content: HBoxContainer = $HBox/Content
@onready var item_icon: TextureRect = $HBox/Content/Icon
@onready var value_label: Label = $HBox/Content/Label
@onready var timer: Timer = $Timer
const INTERVAL_START: float = 0.5
const INTERVAL_CONT: float = 0.05


func _ready() -> void:
	btnl.button_down.connect(_on_button_down)
	btnr.button_down.connect(_on_button_down)
	btnl.button_up.connect(_on_button_up)
	btnr.button_up.connect(_on_button_up)
	timer.timeout.connect(_on_timer_timeout)
	timer.one_shot = true
	timer.ignore_time_scale = true
	refresh()


func _value_changed(_new_value: float) -> void:
	update_content()
	update_buttons()


func _on_button_down() -> void:
	value = value + get_button_step()
	timer.start(INTERVAL_START)


func _on_button_up() -> void:
	timer.stop()


func _on_timer_timeout() -> void:
	value = value + get_button_step()
	timer.start(INTERVAL_CONT)


func get_button_step() -> float:
	var step_val: float = step if spinbox_type == SpinBoxType.VALUE else 1.0
	if btnl.button_pressed:
		return -step_val
	elif btnr.button_pressed:
		return step_val
	return 0.0


func get_spinbox_type() -> SpinBoxType:
	return spinbox_type


func set_spinbox_type(new_spinbox_type: SpinBoxType) -> void:
	if spinbox_type == new_spinbox_type:
		return
	spinbox_type = new_spinbox_type
	update_range()


func add_spinbox_item(text: String, icon: Texture2D) -> void:
	var item: ButtonSpinBoxItem = ButtonSpinBoxItem.new()
	item.text = text
	item.icon = icon
	items.append(item)
	update_range()


func remove_spinbox_item(idx: int) -> bool:
	if idx < 0 || idx >= items.size():
		return false
	items.remove_at(idx)
	update_range()
	return true


func get_item(v: float) -> ButtonSpinBoxItem:
	if items.is_empty():
		return null
	return items[clampi(roundi(v), 0, items.size()-1)]


func get_item_text(v: float) -> String:
	if items.is_empty():
		return ""
	return get_item(v).text


func get_item_icon(v: float) -> Texture2D:
	if items.is_empty():
		return null
	return get_item(v).icon


func clear_spinbox_items() -> void:
	items.clear()
	update_range()


func update_range() -> void:
	if spinbox_type == SpinBoxType.ITEMS:
		min_value = 0
		max_value = items.size() - 1
	value = clampf(value, min_value, max_value)


func update_content() -> void:
	content.alignment = alignment
	match spinbox_type:
		SpinBoxType.VALUE:
			item_icon.visible = false
			value_label.text = str(prefix, value_format % (value * value_multi), suffix)
		SpinBoxType.ITEMS:
			var item: ButtonSpinBoxItem = get_item(value)
			if item:
				item_icon.texture = item.icon
				item_icon.visible = (item.icon != null)
				value_label.text = item.text


func update_buttons() -> void:
	btnl.disabled = !allow_lesser && (is_equal_approx(value, min_value) || (value <= min_value))
	btnr.disabled = !allow_greater && (is_equal_approx(value, max_value) || (value >= max_value))


func refresh() -> void:
	update_range()
	update_content()
	update_buttons()
