class_name WorldTimeService
extends Node

signal time_changed(time_hours: float, phase: Phase)
signal phase_changed(phase: Phase)
signal authoritative_time_set(time_hours: float)
signal natural_time_advanced(hours: float)

enum Phase {
	DAWN,
	DAY,
	DUSK,
	NIGHT,
}

const HOURS_PER_DAY: float = 24.0
const REAL_SECONDS_PER_CYCLE: float = 60.0 * 60.0
const HOURS_PER_REAL_SECOND: float = HOURS_PER_DAY / REAL_SECONDS_PER_CYCLE
const DAY_START_HOUR: float = 8.0
const NIGHT_START_HOUR: float = 20.0
const TRANSITION_HALF_HOURS: float = 0.5
const DAWN_START_HOUR: float = DAY_START_HOUR - TRANSITION_HALF_HOURS
const DAWN_END_HOUR: float = DAY_START_HOUR + TRANSITION_HALF_HOURS
const DUSK_START_HOUR: float = NIGHT_START_HOUR - TRANSITION_HALF_HOURS
const DUSK_END_HOUR: float = NIGHT_START_HOUR + TRANSITION_HALF_HOURS
const DEFAULT_START_HOUR: float = DAY_START_HOUR

var _time_hours: float = DEFAULT_START_HOUR
var _persistent_time_hours: float = DEFAULT_START_HOUR
var _persistence_tracking_enabled: bool = false
var _running: bool = false
var _phase: Phase = Phase.DAWN
var _last_emitted_clock_minute: int = floori(DEFAULT_START_HOUR * 60.0)


func _ready() -> void:
	set_process(false)


func _process(delta: float) -> void:
	advance_time(delta)


func begin_session(start_hour: float = DEFAULT_START_HOUR) -> void:
	_running = true
	set_process(true)
	_set_time_hours(start_hour, true)


func end_session() -> void:
	_running = false
	set_process(false)
	_set_time_hours(DEFAULT_START_HOUR, true)


func advance_time(real_seconds: float) -> void:
	if not _running or real_seconds <= 0.0:
		return
	var advanced_hours := real_seconds * HOURS_PER_REAL_SECOND
	_set_time_hours(
		_time_hours + advanced_hours,
		false,
	)
	natural_time_advanced.emit(advanced_hours)


func synchronize_time(authoritative_time_hours: float) -> void:
	if not is_finite(authoritative_time_hours):
		return
	_set_time_hours(authoritative_time_hours, true)


func set_authoritative_time(time_hours: float) -> bool:
	if (
		not is_finite(time_hours)
		or time_hours < 0.0
		or time_hours >= HOURS_PER_DAY
	):
		return false
	_set_time_hours(time_hours, true)
	authoritative_time_set.emit(_time_hours)
	return true


func set_persistence_tracking_enabled(enabled: bool) -> void:
	if _persistence_tracking_enabled == enabled:
		return
	if _persistence_tracking_enabled:
		_persistent_time_hours = _time_hours
	_persistence_tracking_enabled = enabled


func restore_persistent_time_hours(time_hours: float) -> bool:
	if (
		not is_finite(time_hours)
		or time_hours < 0.0
		or time_hours >= HOURS_PER_DAY
	):
		return false
	_persistent_time_hours = time_hours
	if _persistence_tracking_enabled:
		_set_time_hours(_persistent_time_hours, true)
	return true


func get_persistent_time_hours() -> float:
	return _persistent_time_hours


func get_time_hours() -> float:
	return _time_hours


func get_phase() -> Phase:
	return _phase


func is_night_period() -> bool:
	return _time_hours < DAY_START_HOUR or _time_hours >= NIGHT_START_HOUR


func is_transition() -> bool:
	return _phase in [Phase.DAWN, Phase.DUSK]


func get_clock_text() -> String:
	return format_clock_time(_time_hours)


static func phase_for_hour(time_hours: float) -> Phase:
	var hour: float = _normalized_hour(time_hours)
	if hour >= DAWN_START_HOUR and hour < DAWN_END_HOUR:
		return Phase.DAWN
	if hour >= DAWN_END_HOUR and hour < DUSK_START_HOUR:
		return Phase.DAY
	if hour >= DUSK_START_HOUR and hour < DUSK_END_HOUR:
		return Phase.DUSK
	return Phase.NIGHT


static func format_clock_time(time_hours: float) -> String:
	var hour: float = _normalized_hour(time_hours)
	var total_minutes: int = floori(hour * 60.0)
	var hour_24: int = floori(float(total_minutes) / 60.0)
	var minute: int = total_minutes % 60
	var hour_12: int = hour_24 % 12
	if hour_12 == 0:
		hour_12 = 12
	return "%d:%02d %s" % [
		hour_12,
		minute,
		"am" if hour_24 < 12 else "pm",
	]


func _set_time_hours(time_hours: float, force_emit: bool) -> void:
	var normalized: float = _normalized_hour(time_hours)
	var next_phase: Phase = phase_for_hour(normalized)
	var phase_was_changed: bool = next_phase != _phase
	var clock_minute: int = floori(normalized * 60.0)
	var clock_minute_changed: bool = (
		clock_minute != _last_emitted_clock_minute
	)
	_time_hours = normalized
	_phase = next_phase
	if _persistence_tracking_enabled:
		_persistent_time_hours = normalized
	if phase_was_changed:
		phase_changed.emit(_phase)
	if force_emit or clock_minute_changed or phase_was_changed:
		_last_emitted_clock_minute = clock_minute
		time_changed.emit(_time_hours, _phase)


static func _normalized_hour(time_hours: float) -> float:
	return fposmod(time_hours, HOURS_PER_DAY)
