## Class for converting beats to time (since start of music) and vice versa.
class_name SyncTrack extends Node


func initialize(tempo: Tempo) -> void:
	_set_bpm_data(tempo)
	_calculate_cache()


## Get the number of valid BPM changes.
func size() -> int:
	return min(_bpms.size(), _lengths.size() + 1)


## Get the BPM at the given time (since music start).
func bpm_at_time(time: float) -> float:
	var i: int = _get_bpm_index_at_time(time)
	if _types[i] == Tempo.BPM_TYPE.LINEAR:
		var delta = inverse_lerp(_elapsed_seconds[i], _elapsed_seconds[i+1], time)
		return lerp(_bpms[i], _bpms[i+1], delta)
	else:
		return _bpms.get(i)


## Get the beat at the given time (since music start).
func to_beat(time: float) -> float:
	var i: int = _get_bpm_index_at_time(time)
	
	# If i is not the last BPM:
	if i < _bpms.size() - 1:
		var delta = inverse_lerp(_elapsed_seconds[i], _elapsed_seconds[i+1], time)
		return lerp(_elapsed_beats[i], _elapsed_beats[i+1], delta)
	
	# Else i is the last BPM:
	else:
		var bps: float = _bpms[i] / 60.0
		var remaining_seconds: float = time - _elapsed_seconds[i]
		var remaining_beats: float = remaining_seconds * bps
		return _elapsed_beats[i] + remaining_beats


## Get the time (since music start) at the given beat.
func to_time(beat: float) -> float:
	var i: int = _get_bpm_index_at_beat(beat)
	
	if i == -1:
		return -1.0
	
	# Else if i is not the last BPM:
	elif i < _bpms.size() - 1:
		var delta = inverse_lerp(_elapsed_beats[i], _elapsed_beats[i+1], beat)
		return lerp(_elapsed_seconds[i], _elapsed_seconds[i+1], delta)
	
	# Else i is the last BPM:
	else:
		var bps: float = _bpms[i] / 60.0
		var remaining_beats: float = beat - _elapsed_beats[i]
		var remaining_seconds: float = remaining_beats / bps
		return _elapsed_seconds[i] + remaining_seconds


func _init(tempo: Tempo = Tempo.new()) -> void:
	initialize(tempo)


# IMPLEMENTATION

# === Note about BPM data ===
# The data for each BPM is stored in the same index
# across the different arrays.
# ===========================

## BPM array (Beats per Minute).
var _bpms: Array[float]
## The duration of each BPM in beats.
var _lengths: Array[float]
## The BpmData.BPM_TYPE of each BPM.
var _types: Array[Tempo.BPM_TYPE]

# Cache variables that make calculations easier.
## The duration of each BPM in seconds.
var _length_seconds: Array[float]
## Seconds passed before the corresponding BPM.
var _elapsed_seconds: Array[float] = [0.0]
## Beats passed before the corresponding BPM.
var _elapsed_beats: Array[float] = [0.0]


## Set the BPM data arrays.
func _set_bpm_data(tempo: Tempo) -> void:
	
	_clear_bpm_data()
	tempo.normalize()
	_bpms = tempo.bpms
	_lengths = tempo.lengths
	_types = tempo.types


func _calculate_cache() -> void:
	_clear_cache()
	for i: int in range(0, _bpms.size() - 1):
		var bps: float
		var length_in_beats: float = _lengths[i]
		if _types[i] == Tempo.BPM_TYPE.LINEAR:
			# Average BPS.
			bps = (_bpms[i] + _bpms[i+1]) / 120.0
		else:
			bps = _bpms[i] / 60.0
		var length_in_seconds: float = length_in_beats / bps
		
		_length_seconds.append(length_in_seconds)
		_elapsed_seconds.append(_elapsed_seconds.back() + length_in_seconds)
		_elapsed_beats.append(_elapsed_beats.back() + length_in_beats)
	
	if _length_seconds.size() > 0:
		_length_seconds[-1] = -1.0


func _clear_bpm_data() -> void:
	_bpms = []
	_lengths = []
	_types = []


func _clear_cache() -> void:
	_length_seconds = []
	_elapsed_seconds = [0.0]
	_elapsed_beats = [0.0]


func _get_bpm_index_at_time(time: float) -> int:
	assert(_elapsed_seconds.size() > 0)
	assert(_bpms.size() > 0)
	
	var _find_index: Callable = func (p_elapsed_seconds: float) -> bool:
		return time < p_elapsed_seconds
	
	var index: int = _elapsed_seconds.find_custom(_find_index)
	if index == -1:
		index = _bpms.size() - 1
	else:
		index -= 1
	return index


func _get_bpm_index_at_beat(beat: float) -> int:
	var _find_index: Callable = func (p_elapsed_beats: float) -> bool:
		return beat < p_elapsed_beats
	
	if _elapsed_beats.size() == 0:
		return -1
	
	var index: int = _elapsed_beats.find_custom(_find_index)
	if index > 0:
		index -= 1
	elif index == -1:
		index = max(0, _bpms.size()-1)
	return index


func _print_data() -> void:
	print("=== SyncTrack ===")
	print("BPMs: ", _bpms)
	print("BPM Lengths (Beats): ", _lengths)
	print("BPM Types: ", _types)
	print("Elapsed Seconds: ", _elapsed_seconds)
	print("Elapsed Beats: ", _elapsed_beats)
