WorldBuilder/TimeManager.cs
using System;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
using Clover.Objects;
using Sandbox.Audio;

namespace Clover.WorldBuilder;

[Category( "Clover/World" )]
public class TimeManager : Component, IWorldEvent
{
	public static TimeManager Instance =>
		Game.ActiveScene.GetAllComponents<TimeManager>().FirstOrDefault(); // TODO: optimize

	// public static DirectionalLight Sun =>
	// 	Game.ActiveScene.GetAllComponents<DirectionalLight>().FirstOrDefault( x => x.Tags.Has( "sun" ) );
	[Property] public DirectionalLight Sun { get; set; }
	[Property] public SkyBox2D SkyBox { get; set; }

	[Property] public Gradient SunlightGradient { get; set; }
	[Property] public Gradient AmbientGradient { get; set; }

	[ConVar( "clover_time_scale" )] public static int Speed { get; set; } = 1;

	[Property] public Curve SunPitchCurve { get; set; }
	[Property] public Curve SunYawCurve { get; set; }

	// public static DateTime Time => Speed != 1 ? DateTime.Now.AddSeconds( Sandbox.Time.Now * Speed ) : DateTime.Now;

	[Sync] public double GlobalTime { get; set; }

	[JsonIgnore]
	public static DateTime Time => Instance?.TimeOverride > -1
		? DateTime.Today.AddSeconds( Instance?.TimeOverride ?? 0 )
		: DateTime.FromOADate( Instance?.GlobalTime ?? DateTime.Now.ToOADate() );

	[Property, Range( -1, 86400 )] public float TimeOverride { get; set; } = -1;


	private const float SecondsPerDay = 86400f;

	public delegate void OnNewHourEventHandler( int hour );

	[Property] public OnNewHourEventHandler OnNewHour { get; set; }

	public delegate void OnNewMinuteEventHandler( int minute );

	[Property] public OnNewMinuteEventHandler OnNewMinute { get; set; }

	[Property] public SoundEvent HourChime { get; set; }

	public static bool IsNight => Time.Hour is < 6 or > 18;
	public static bool IsDay => !IsNight;

	public static int DayStartHour => 6;
	public static int DayEndHour => 18;

	protected override void OnStart()
	{
		OnNewHour += PlayChime;

		_lastHour = Time.Hour;

		OnNewMinute += ( minute ) =>
		{
			if ( Random.Shared.NextSingle() > 0.95f )
			{
				GiftCarrier.SpawnRandom();
			}
		};
	}

	private void PlayChime( int hour )
	{
		if ( TimeOverride > -1 ) return;
		Log.Info( $"Playing chime for hours {hour}" );
		// var chime = GetNode<AudioStreamPlayer>( "Chime" );
		/* for ( int i = 0; i < hour; i++ )
		{
			ToSignal( GetTree().CreateTimer( i * 1f ), Timer.SignalName.Timeout ).OnCompleted( () =>
			{
				Logger.Info( "DayNightCycle", $"Playing chime for hour {i}" );
				chime.Play();
			} );
		} */
		// chime.Play();

		Sound.Play( HourChime.ResourcePath, Mixer.FindMixerByName( "UI" ) );
	}

	private readonly List<Color> _skyColors = new()
	{
		Color.FromBytes( 1, 1, 3 ), // 00:00
		Color.FromBytes( 1, 1, 3 ), // 01:00
		Color.FromBytes( 1, 1, 3 ), // 02:00
		Color.FromBytes( 1, 1, 3 ), // 03:00
		Color.FromBytes( 1, 1, 3 ), // 04:00
		Color.FromBytes( 1, 1, 3 ), // 05:00
		Color.FromBytes( 1, 1, 3 ), // 06:00
		Color.FromBytes( 255, 100, 100 ), // 07:00 -- sunrise
		Color.FromBytes( 255, 255, 255 ), // 08:00
		Color.FromBytes( 255, 255, 255 ), // 09:00
		Color.FromBytes( 255, 255, 255 ), // 10:00
		Color.FromBytes( 255, 255, 255 ), // 11:00
		Color.FromBytes( 255, 255, 255 ), // 12:00
		Color.FromBytes( 255, 255, 255 ), // 13:00
		Color.FromBytes( 255, 255, 255 ), // 14:00
		Color.FromBytes( 255, 255, 255 ), // 15:00
		Color.FromBytes( 250, 240, 240 ), // 16:00
		Color.FromBytes( 220, 100, 100 ), // 17:00 -- sunset
		Color.FromBytes( 1, 1, 3 ), // 18:00
		Color.FromBytes( 1, 1, 3 ), // 19:00
		Color.FromBytes( 1, 1, 3 ), // 20:00
		Color.FromBytes( 1, 1, 3 ), // 21:00
		Color.FromBytes( 1, 1, 3 ), // 22:00
		Color.FromBytes( 1, 1, 3 ), // 23:00
	};

	private int _lastHour = -1;
	private int _lastMinute = -1;

	protected override void OnFixedUpdate()
	{
		if ( !IsProxy )
		{
			if ( GlobalTime == 0 ) _lastHour = DateTime.Now.Hour;
			GlobalTime = DateTime.Now.ToOADate();
		}

		if ( Sun.IsValid() )
		{
			Sun.WorldRotation = CalculateSunRotation( Sun );
			// Sun.LightEnergy = CalculateSunEnergy( Sun );
			Sun.LightColor = CalculateSunLightColor();
			// Sun.SkyColor = (CalculateSunColor() * (IsDay ? 0.4f : 1f)).WithAlpha( 1 );
			Sun.SkyColor = CalculateSunAmbientColor();
			// GetTree().CallGroup( "debugdraw", "add_line", Sun.GlobalTransform.Origin, Sun.GlobalTransform.Origin + Sun.GlobalTransform.Basis.Z * 0.5f, new Color( 1, 1, 1 ), 0.2f );
		}
		else
		{
			// Log.Warning( "Sun not found." );
		}
		/*
		if ( IsInstanceValid( _environment ) )
		{
			_environment.Environment.BackgroundEnergyMultiplier = CalculateSunEnergy( Sun );
		}*/

		var hour = Time.Hour;
		if ( hour != _lastHour )
		{
			_lastHour = hour;
			OnNewHour?.Invoke( hour );
			Scene.RunEvent<ITimeEvent>( x => x.OnNewHour( hour ) );
		}

		var minute = Time.Minute;
		if ( minute != _lastMinute )
		{
			_lastMinute = minute;
			OnNewMinute?.Invoke( minute );
			Scene.RunEvent<ITimeEvent>( x => x.OnNewMinute( minute ) );
		}
	}

	private float DayFraction => (Time.Hour * 3600 + Time.Minute * 60 + Time.Second) / SecondsPerDay;

	public Color GetComputedSkyColor()
	{
		var baseIndex = MathF.Floor( DayFraction * _skyColors.Count );
		var nextIndex = (int)Math.Ceiling( DayFraction * _skyColors.Count ) % _skyColors.Count;
		var baseColor = _skyColors[(int)baseIndex];
		var nextColor = _skyColors[nextIndex];
		var lerp = DayFraction * _skyColors.Count - baseIndex;
		var color = Color.Lerp( baseColor, nextColor, lerp );
		return color;
	}

	public Color CalculateSunLightColor()
	{
		// return GetComputedSkyColor();

		return SunlightGradient.Evaluate( DayFraction );
	}

	public Color CalculateSunAmbientColor()
	{
		return AmbientGradient.Evaluate( DayFraction );

		/*// daytime is *1, nighttime is *3. lerp between *1 and *3 based on time of day

		var baseAmbientColor = GetComputedSkyColor();

		var lerp = MathF.Sin( MathF.PI * DayFraction );
		var color = Color.Lerp( baseAmbientColor * 1f, baseAmbientColor * 1f, lerp );

		return color.WithAlpha( 1f );*/
	}

	public Color CalculateFogColor()
	{
		return AmbientGradient.Evaluate( DayFraction ).WithAlpha( 0.2f );
	}

	private Rotation CalculateSunRotation( DirectionalLight sun )
	{
		var time = Time;
		var hours = time.Hour;
		var minutes = time.Minute;
		var seconds = time.Second;
		var msec = time.Millisecond;

		var totalSeconds = hours * 3600 + minutes * 60 + seconds + msec / 1000f;
		var totalSecondsInDay = 24 * 3600;

		var frac = totalSeconds / totalSecondsInDay;

		// var pitch = MathX.Lerp( 15, 50, frac );
		// var yaw = MathX.Lerp( 0, 180, frac );

		var pitch = SunPitchCurve.Evaluate( frac );
		var yaw = SunYawCurve.Evaluate( frac );

		var rotation = Rotation.FromYaw( yaw ) * Rotation.FromPitch( pitch );


		return rotation;
	}

	[Property] private Rotation _SunRotation => CalculateSunRotation( Sun );

	/// <summary>
	///  A value between 0 and 1 used to calculate the sun's energy (0 at night, 1 at midday).
	/// </summary>
	/// <param name="sun"></param>
	/// <returns></returns>
	/*private float CalculateSunEnergy( DirectionalLight3D sun )
	{
		var time = Time;
		var hours = time.Hour;
		var minutes = time.Minute;
		var seconds = time.Second;
		var msec = time.Millisecond;

		var totalSeconds = hours * 3600 + minutes * 60 + seconds + msec / 1000f;
		var totalSecondsInDay = 24 * 3600;

		var energy = Mathf.Abs( Mathf.Sin( Mathf.Pi * 2 * totalSeconds / totalSecondsInDay ) );

		return energy;

	}*/
	internal string GetDate()
	{
		return Time.ToString( "yyyy-MM-dd HH:mm:ss" );
	}

	internal string GetTime()
	{
		return Time.ToString( "h:mm tt" );
	}

	public void OnWorldChanged( World world )
	{
		if ( Sun.IsValid() )
		{
			Sun.Enabled = !world.Data.IsInside;
		}

		if ( SkyBox.IsValid() )
		{
			SkyBox.Enabled = !world.Data.IsInside;
		}
	}

	protected override void DrawGizmos()
	{
		base.DrawGizmos();

		if ( !Gizmo.IsSelected ) return;

		for ( var i = 0; i < 48; i++ )
		{
			var frac = i / 48f;
			var pitch = SunPitchCurve.Evaluate( frac );
			var yaw = SunYawCurve.Evaluate( frac );
			var rotation = Rotation.FromYaw( yaw ) * Rotation.FromPitch( pitch );
			var direction = rotation.Backward;
			Gizmo.Draw.LineSphere( Gizmo.Camera.Position + direction * 512f, 8f );
		}
	}
}

public interface ITimeEvent
{
	public void OnNewHour( int hour );
	public void OnNewMinute( int minute );
}