## Class for playing music and providing an interface to keep events in sync.
class_name Conductor extends Node

## If true, start immediately when ready.
@export var autostart: bool = false

## Music to play.
@export var music: Music

signal started
signal ticked(beat_update: BeatUpdate)
signal ticked_forward(new_beat: float)
#signal ticked_backward(new_beat: float)
signal finished


func start() -> void:
	assert(music != null)
	assert(_audio_player != null)
	
	_stop_playback_and_initialize()
	_active = true
	started.emit()


func is_active() -> bool:
	return _active


func current_time() -> float:
	return(_audio_player.get_playback_position() +
		AudioServer.get_time_since_last_mix() +
		_music_offset - _remaining_silence)


func previous_time() -> float:
	return _previous_time


func current_beat() -> float:
	return _sync_track.to_beat(current_time())


func previous_beat() -> float:
	return _previous_beat


func current_bpm() -> float:
	return _sync_track.bpm_at_time(current_time())


func add_time(seconds: float) -> void:
	if _audio_player == null:
		return
	_audio_player.seek(_audio_player.get_playback_position() + seconds)
	_previous_time = current_time()
	_previous_beat = current_beat()



# ====== IMPLEMENTATION ====== #

var _active: bool = false

var _previous_time: float = -1.0
var _previous_beat: float = -1.0

var _audio_player: AudioStreamPlayer = null
var _sync_track: SyncTrack = SyncTrack.new()

var _music_offset: float
var _music_playing: bool = false
var _remaining_silence: float = 0.0


func _ready() -> void:
	_audio_player = AudioStreamPlayer.new()
	add_child(_audio_player)
	_audio_player.finished.connect(_on_music_finished)
	
	if autostart:
		start()


func _on_music_finished() -> void:
	_music_playing = false
	_active = false
	finished.emit()


func _process(delta: float) -> void:
	if not _active:
		return
	
	if _music_playing:
		_beat_update()
	else:
		_update_silence(delta)


func _update_silence(elapsed_time: float) -> void:
	_remaining_silence -= elapsed_time
	
	if _remaining_silence < 0:
		_play_music_from_offset(-_remaining_silence)
		_remaining_silence = 0.0


func _beat_update() -> void:
	var _current_time = current_time()
	
	# _audio_player.get_playback_position() used in current_time()
	# is not perfect and may return a lesser value than last frame.
	# This makes sure the beat never updates backward.
	if _current_time <= _previous_time:
		return
	
	var _current_beat = _sync_track.to_beat(_current_time)
	ticked.emit(BeatUpdate.new(_current_beat, _previous_beat))
	ticked_forward.emit(_current_beat)
	
	_previous_beat = _current_beat
	_previous_time = _current_time


func _stop_playback_and_initialize() -> void:
	if _audio_player.playing:
		_audio_player.stop()
	
	_active = false
	_music_playing = false
	_previous_time = -1.0
	_previous_beat = -1.0
	_music_offset = music.offset
	_remaining_silence = _music_offset
	_audio_player.stream = music.stream
	_sync_track.initialize(music.tempo)


func _play_music_from_offset(offset: float) -> void:
	_audio_player.play(offset)
	_music_playing = true
