extends Node2D

## JuggleBoard Game — reaction & chaining arcade.
##
## Layout: 5 corsie orizzontali, 5 palline colorate a riposo a destra.
## Un bersaglio in alto indica quale pallina premere. Toccata, la pallina
## scivola a sinistra e poi torna a destra. Prima che scada il timeout devi
## premere il nuovo bersaglio per concatenare. Più concateni, più punti fai
## e più la difficoltà (velocità) cresce con il tempo trascorso e il punteggio.
##
## Un nuovo bersaglio appare SOLO se c'è almeno una pallina a riposo.

const VIEW := Vector2(1280, 720)
const LANE_TOP := 210.0
const LANE_GAP := 98.0
const BALL_RADIUS := 40.0
const HOME_X := 1130.0
const LEFT_X := 300.0
const HIT_RADIUS := BALL_RADIUS * 1.4  # area di tocco/clic (maggiorata)

# I valori di calibrazione (velocità, moltiplicatori, decay) sono gestiti dal
# singleton Settings e modificabili dal menù di configurazione in-game.

const COLORS := {
	"rosso": Color("e74c3c"),
	"blu": Color("3498db"),
	"verde": Color("2ecc71"),
	"giallo": Color("f1c40f"),
	"viola": Color("9b59b6"),
}

# Note pentatoniche (Do4 Re4 Mi4 Sol4 La4): qualsiasi pallina toccata
# produce sempre un suono armonico e piacevole (sensory-friendly).
const BALL_NOTES := {
	"rosso": 261.63,
	"blu": 293.66,
	"verde": 329.63,
	"giallo": 392.00,
	"viola": 440.00,
}

var balls := {}            # color_name -> Ball
var target_color := ""     # bersaglio corrente (vuoto = nessun bersaglio)
var score := 0
var combo := 0.0   # float: decade col ritardo di reazione
var elapsed := 0.0
var window_timer := 0.0
var window_time := Settings.start_time
var next_milestone := 100

# UI
var target_dot: Node2D
var score_label: Label
var combo_label: Label
var help_label: Label
var milestone_label: Label
var flash: ColorRect

# Audio
var sound_launch: AudioStreamWAV
var sound_error: AudioStreamWAV
var sound_timeout: AudioStreamWAV
var sound_milestone: AudioStreamWAV
var ball_sounds := {}  # color_name -> AudioStreamWAV (tono pentatonico)
var bgm: AudioStreamPlayer
var options_menu: CanvasLayer


func _ready() -> void:
	_build_lanes()
	_build_balls()
	_build_ui()
	_init_audio()
	_setup_bgm()
	options_menu = load("res://scripts/options_menu.gd").new()
	add_child(options_menu)
	_new_game()


func _process(delta: float) -> void:
	elapsed += delta
	if target_color == "":
		return  # in attesa che una pallina torni a casa: nessun countdown
	if Settings.zen_mode:
		return  # modalità zen: nessun countdown né decay (niente pressione)
	# il combo decade in proporzione al tempo che aspetto prima di cliccare
	if combo > 0.0:
		combo = maxf(0.0, combo - _combo_decay() * delta)
	window_timer += delta
	if window_timer >= window_time:
		_on_timeout()


# Input globale: mouse click e touch tap, convertiti in coordinate di gioco.
func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
		_toggle_options()
		return
	var pos := Vector2.INF
	if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
		pos = event.position
	elif event is InputEventScreenTouch and event.pressed:
		pos = event.position
	if pos == Vector2.INF:
		return
	_handle_click(pos)


func _open_options() -> void:
	if options_menu:
		options_menu.show_menu()


func _toggle_options() -> void:
	if options_menu:
		if options_menu.visible:
			options_menu.hide_menu()
		else:
			options_menu.show_menu()


# ---------------------------------------------------------------- costruzione

func _build_lanes() -> void:
	for i in 5:
		var y := LANE_TOP + i * LANE_GAP
		var line := Line2D.new()
		line.add_point(Vector2(90, y))
		line.add_point(Vector2(VIEW.x - 90, y))
		line.width = 2
		line.default_color = Color(1, 1, 1, 0.12)
		add_child(line)
		var lbl := Label.new()
		lbl.text = COLORS.keys()[i].to_upper()
		lbl.position = Vector2(100, y - 44)
		lbl.add_theme_font_size_override("font_size", 16)
		lbl.add_theme_color_override("font_color", Color(1, 1, 1, 0.35))
		add_child(lbl)


func _build_balls() -> void:
	var names := COLORS.keys()
	for i in names.size():
		var cn: String = names[i]
		var home := Vector2(HOME_X, LANE_TOP + i * LANE_GAP)
		var left := Vector2(LEFT_X, LANE_TOP + i * LANE_GAP)
		var b: Area2D = load("res://scripts/ball.gd").new()
		add_child(b)
		b.setup(cn, COLORS[cn], home, left, BALL_RADIUS)
		b.returned.connect(_on_ball_returned)
		balls[cn] = b


func _build_ui() -> void:
	var layer := CanvasLayer.new()
	add_child(layer)

	var bar := ColorRect.new()
	bar.color = Color(0.08, 0.09, 0.12, 0.95)
	bar.position = Vector2.ZERO
	bar.size = Vector2(VIEW.x, 140)
	layer.add_child(bar)

	var edge := ColorRect.new()
	edge.color = Color(1, 1, 1, 0.15)
	edge.position = Vector2(0, 140)
	edge.size = Vector2(VIEW.x, 3)
	layer.add_child(edge)

	var tlabel := Label.new()
	tlabel.text = "BERSAGLIO"
	tlabel.position = Vector2(46, 26)
	tlabel.add_theme_font_size_override("font_size", 22)
	tlabel.add_theme_color_override("font_color", Color(1, 1, 1, 0.8))
	layer.add_child(tlabel)

	target_dot = load("res://scripts/target_dot.gd").new()
	target_dot.position = Vector2(128, 84)
	layer.add_child(target_dot)

	help_label = Label.new()
	help_label.text = "Tocca la pallina corrispondente al bersaglio per lanciarla!"
	help_label.position = Vector2(210, 96)
	help_label.add_theme_font_size_override("font_size", 18)
	help_label.add_theme_color_override("font_color", Color(1, 1, 1, 0.6))
	layer.add_child(help_label)

	score_label = Label.new()
	score_label.position = Vector2(VIEW.x - 320, 22)
	score_label.add_theme_font_size_override("font_size", 34)
	layer.add_child(score_label)

	combo_label = Label.new()
	combo_label.position = Vector2(VIEW.x - 320, 68)
	combo_label.add_theme_font_size_override("font_size", 20)
	combo_label.add_theme_color_override("font_color", Color(1, 1, 1, 0.65))
	layer.add_child(combo_label)

	# pulsante Opzioni
	var opts_btn := Button.new()
	opts_btn.text = "⚙ Opzioni (Esc)"
	opts_btn.position = Vector2(VIEW.x / 2 - 100, 26)
	opts_btn.pressed.connect(_open_options)
	layer.add_child(opts_btn)

	# feedback celebrativo alle milestone
	milestone_label = Label.new()
	milestone_label.position = Vector2(0, 160)
	milestone_label.size = Vector2(VIEW.x, 120)
	milestone_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	milestone_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
	milestone_label.add_theme_font_size_override("font_size", 56)
	milestone_label.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4))
	milestone_label.visible = false
	layer.add_child(milestone_label)

	flash = ColorRect.new()
	flash.color = Color(1, 1, 0.7)
	flash.position = Vector2.ZERO
	flash.size = VIEW
	flash.mouse_filter = Control.MOUSE_FILTER_IGNORE
	flash.modulate.a = 0.0
	flash.visible = false
	layer.add_child(flash)


# ------------------------------------------------------------------- audio

func _init_audio() -> void:
	# Suoni MORBIDI (sine, inviluppo dolce) per non sovrastimolare i bambini
	# neurodivergenti: niente onde quadre/sega, volumi attenuati.
	sound_launch = _create_tone(660.0, 0.18, 0.3)   # marimba morbido
	sound_error = _create_tone(220.0, 0.15, 0.16)   # "pop" basso e attenuato
	sound_timeout = _create_tone(330.0, 0.2, 0.16)  # tono dolce
	sound_milestone = _create_soft_arpeggio([523.25, 659.25, 783.99, 1046.5], 0.12)
	# tono pentatonico per ogni pallina colorata
	for cn in BALL_NOTES:
		ball_sounds[cn] = _create_tone(BALL_NOTES[cn], 0.2, 0.3)


func _setup_bgm() -> void:
	# Musica di sottofondo da circo: traccia lunga generata via API Lyria,
	# compressa in OGG Vorbis (loop abilitato nelle impostazioni di import),
	# con fallback alla versione procedurale se il file non è presente.
	bgm = AudioStreamPlayer.new()
	var stream: AudioStream = load("res://audio/circus.ogg")
	if stream:
		bgm.stream = stream
	else:
		bgm.stream = load("res://scripts/circus_music.gd").generate()
	bgm.volume_db = -14.0
	bgm.process_mode = Node.PROCESS_MODE_ALWAYS  # continua anche a menù aperto
	add_child(bgm)
	bgm.play()


func _create_soft_arpeggio(freqs: Array, note_dur: float = 0.12) -> AudioStreamWAV:
	var sample_rate := 22050
	var total := 0
	for f in freqs:
		total += int(sample_rate * note_dur)
	var data := PackedByteArray()
	data.resize(total)
	var idx := 0
	var attack := int(sample_rate * 0.01)
	for f in freqs:
		var n := int(sample_rate * note_dur)
		for i in n:
			var t := float(i) / sample_rate
			var env := 1.0
			if i < attack:
				env = float(i) / attack
			env *= exp(-3.0 * t / note_dur)
			var s := sin(t * f * TAU) * env
			var val := int(clampf(s * 0.3 * 127.0, -128.0, 127.0))
			data[idx] = val & 0xFF
			idx += 1
	var stream := AudioStreamWAV.new()
	stream.format = AudioStreamWAV.FORMAT_8_BITS
	stream.mix_rate = sample_rate
	stream.data = data
	return stream


func _create_tone(freq: float, duration: float, amp: float = 0.3) -> AudioStreamWAV:
	# Onda sinusoidale pura con attacco morbido e decadimento esponenziale:
	# timbro dolce, senza clic né armoniche spigolose.
	var sample_rate := 22050
	var num_samples := int(sample_rate * duration)
	var data := PackedByteArray()
	data.resize(num_samples)
	var attack := int(sample_rate * 0.01)
	for i in num_samples:
		var t := float(i) / sample_rate
		var env := 1.0
		if i < attack:
			env = float(i) / attack
		env *= exp(-3.0 * t / duration)
		var s := sin(t * freq * TAU)
		var val := int(clampf(s * env * amp * 127.0, -128.0, 127.0))
		data[i] = val & 0xFF
	var stream := AudioStreamWAV.new()
	stream.format = AudioStreamWAV.FORMAT_8_BITS
	stream.mix_rate = sample_rate
	stream.data = data
	return stream


func _play_sound(stream: AudioStreamWAV) -> void:
	if stream == null:
		return
	var player := AudioStreamPlayer.new()
	player.stream = stream
	add_child(player)
	player.finished.connect(player.queue_free)
	player.play()


# ------------------------------------------------------------------- gioco

func _new_game() -> void:
	score = 0
	combo = 0.0
	elapsed = 0.0
	window_timer = 0.0
	window_time = Settings.start_time
	next_milestone = 100
	for b in balls.values():
		b.reset()
	_pick_target()
	help_label.visible = true
	_update_hud()


func _handle_click(pos: Vector2) -> void:
	var clicked: Area2D = null
	var best := HIT_RADIUS
	for b in balls.values():
		if not b.is_home():
			continue
		var d := pos.distance_to(b.position)
		if d < best:
			best = d
			clicked = b
	if clicked == null:
		return  # click su spazio vuoto

	if clicked.color_name == target_color:
		# corretto: suono pentatonico della pallina, punti, nuovo bersaglio
		combo = minf(float(Settings.max_combo), combo + 1.0)
		score += _points_for_combo()
		_play_sound(ball_sounds.get(clicked.color_name, sound_launch))
		var mt := _current_time()
		clicked.launch(mt)
		window_time = mt
		window_timer = 0.0
		_pick_target()
		_update_hud()
		_check_milestone()
	else:
		# tasto sbagliato: suono morbido, penalità gentile (combo -1) o reset
		_play_sound(sound_error)
		if Settings.gentle_errors:
			combo = maxf(0.0, combo - 1.0)
		else:
			combo = 0.0
		_update_hud()


func _on_timeout() -> void:
	# tempo scaduto: suono dolce, penalità gentile, nuovo bersaglio, nessun game over
	_play_sound(sound_timeout)
	if Settings.gentle_errors:
		combo = maxf(0.0, combo - 1.0)
	else:
		combo = 0.0
	window_timer = 0.0
	_pick_target()
	_update_hud()


func _on_ball_returned() -> void:
	# quando una pallina torna a casa e non c'è bersaglio, se ne può mostrare uno
	if target_color == "":
		_pick_target()


func _current_time() -> float:
	var d := _difficulty()
	return lerpf(Settings.start_time, Settings.min_time, d)


func _difficulty() -> float:
	# La difficoltà (e quindi la velocità massima) è legata unicamente al punteggio
	var sf := clampf(score / Settings.score_ramp, 0.0, 1.0)
	return _smoothstep(sf)


func _combo_decay() -> float:
	# Decadimento proporzionale: quasi nullo a combo basso / inizio partita,
	# cresce man mano che combo e difficoltà aumentano.
	var combo_pen := maxf(0.0, combo - Settings.combo_decay_threshold) * Settings.combo_decay_rate
	var diff_pen := _difficulty() * Settings.difficulty_decay_rate
	return combo_pen + diff_pen


func _points_for_combo() -> int:
	# Punti per lancio = combo corrente × valore punto configurato.
	# Es. combo 5 × 1 = 5 pt; combo 1 × 1 = 1 pt. Il valore punto è configurabile.
	return int(combo) * Settings.points_per_combo


func _smoothstep(x: float) -> float:
	return x * x * (3.0 - 2.0 * x)


func _pick_target() -> void:
	var homes := _home_names()
	if homes.is_empty():
		target_color = ""
		target_dot.visible = false
		return
	var pool := homes.duplicate()
	if target_color in pool and pool.size() > 1:
		pool.erase(target_color)
	target_color = pool[randi() % pool.size()]
	target_dot.set_color(COLORS[target_color])
	target_dot.visible = true


func _home_names() -> Array:
	var arr := []
	for cn in balls.keys():
		if balls[cn].is_home():
			arr.append(cn)
	return arr


func _update_hud() -> void:
	score_label.text = "PUNTI  %d" % score
	combo_label.text = "Combo x%d" % int(combo)


# ---------------------------------------------- feedback alle milestone

func _check_milestone() -> void:
	if score < next_milestone:
		return
	# supera più traguardi in una volta: celebra il più alto raggiunto
	var celebrated := next_milestone
	while next_milestone <= score:
		celebrated = next_milestone
		next_milestone *= 10
	_celebrate(celebrated)


func _celebrate(amount: int) -> void:
	_play_sound(sound_milestone)
	milestone_label.text = "%d PUNTI!" % amount
	milestone_label.visible = true
	milestone_label.modulate.a = 1.0
	milestone_label.scale = Vector2(0.6, 0.6)
	flash.visible = true
	flash.modulate.a = 0.18
	var tw := create_tween()
	tw.set_parallel(true)
	tw.tween_property(milestone_label, "scale", Vector2(1.3, 1.3), 0.4) \
		.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
	tw.tween_property(milestone_label, "modulate:a", 0.0, 0.8).set_delay(0.6)
	tw.tween_property(flash, "modulate:a", 0.0, 0.4)
	tw.chain().tween_callback(func():
		milestone_label.visible = false
		flash.visible = false
	)
